nixamp 0.5.11 → 0.5.12
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/main.js +3 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +38 -4
- package/dist/share.d.ts +1 -1
- package/dist/share.js +3 -3
- package/package.json +1 -1
- package/src/main.ts +3 -0
- package/src/server.ts +56 -4
- package/src/share.ts +3 -2
- package/web/dist/assets/{hls-3VKVEQE3-Bug6MFON.js → hls-3VKVEQE3-Ch_vCVqk.js} +1 -1
- package/web/dist/assets/index-B816i-Dk.js +1 -0
- package/web/dist/assets/{mpegts-BtEekTJ6.js → mpegts-DS9rVyQe.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-DW1kEon1.js → mpegts-LO6RVLD6-DxPAVtZQ.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +5 -5
- package/web/dist/assets/index-CRfW9BIP.js +0 -1
package/dist/main.js
CHANGED
|
@@ -72,6 +72,9 @@ Options for serve:
|
|
|
72
72
|
its own interfaces. Also NIXAMP_PUBLIC_URL
|
|
73
73
|
--no-lookup do not ask ipinfo.io what this machine's public address is
|
|
74
74
|
when nothing local looks public
|
|
75
|
+
--tls-cert FILE --tls-key FILE serve https rather than http. Needed by
|
|
76
|
+
anyone opening this from a page that is itself https, since
|
|
77
|
+
a browser refuses every request from https to http
|
|
75
78
|
--ingest accept a live stream in at POST /api/ingest
|
|
76
79
|
--rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
|
|
77
80
|
--rtmp-streams N how many may publish at once (default 3, a port each)
|
package/dist/server.d.ts
CHANGED
|
@@ -56,6 +56,18 @@ export interface ServeOptions {
|
|
|
56
56
|
* listing is skipped and the printed links only work inside the house.
|
|
57
57
|
*/
|
|
58
58
|
publicUrl: string;
|
|
59
|
+
/**
|
|
60
|
+
* A certificate and its key, to serve https rather than http.
|
|
61
|
+
*
|
|
62
|
+
* Needed by anybody whose nixamp is opened from a page that is itself https:
|
|
63
|
+
* a browser refuses every request from an https page to an http one --
|
|
64
|
+
* fetch, event stream and media alike -- and no header on either side lifts
|
|
65
|
+
* that. It is deliberately not required: a nixamp on 192.168.1.5 cannot have
|
|
66
|
+
* a certificate for that address, and forcing one would put a browser
|
|
67
|
+
* warning in front of everybody at home to fix a problem they do not have.
|
|
68
|
+
*/
|
|
69
|
+
tlsCert: string;
|
|
70
|
+
tlsKey: string;
|
|
59
71
|
/**
|
|
60
72
|
* Ask an outside service what this machine's public address is, when no
|
|
61
73
|
* interface holds one and none was given. Behind NAT that is the only way to
|
|
@@ -250,6 +262,11 @@ export interface HandlerOptions {
|
|
|
250
262
|
signIn?: SignIn;
|
|
251
263
|
/** True when this instance is reached over https, for the cookie's Secure. */
|
|
252
264
|
secureCookies?: boolean;
|
|
265
|
+
/** A certificate and key in PEM, when this server is to speak https itself. */
|
|
266
|
+
tls?: {
|
|
267
|
+
cert: string;
|
|
268
|
+
key: string;
|
|
269
|
+
};
|
|
253
270
|
/**
|
|
254
271
|
* True when a proxy sits in front, so `x-forwarded-for` names the caller.
|
|
255
272
|
* False everywhere else on purpose: the header is trivially forged, and
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createReadStream, statSync } from "node:fs";
|
|
13
13
|
import { createServer as createHttpServer } from "node:http";
|
|
14
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
14
15
|
import { hostname } from "node:os";
|
|
15
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
17
|
import { readFileSync } from "node:fs";
|
|
@@ -70,6 +71,8 @@ export function parseServeArgs(argv) {
|
|
|
70
71
|
name: "",
|
|
71
72
|
publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
|
|
72
73
|
lookup: true,
|
|
74
|
+
tlsCert: process.env["NIXAMP_TLS_CERT"] ?? "",
|
|
75
|
+
tlsKey: process.env["NIXAMP_TLS_KEY"] ?? "",
|
|
73
76
|
x402: false,
|
|
74
77
|
owner: "",
|
|
75
78
|
ingest: false,
|
|
@@ -78,6 +81,11 @@ export function parseServeArgs(argv) {
|
|
|
78
81
|
rtmp: [],
|
|
79
82
|
};
|
|
80
83
|
let sawRoot = false;
|
|
84
|
+
const bothOrNeither = () => {
|
|
85
|
+
if (Boolean(options.tlsCert) !== Boolean(options.tlsKey)) {
|
|
86
|
+
throw new Error("nixamp serve: --tls-cert and --tls-key go together");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
81
89
|
for (let i = 0; i < argv.length; i++) {
|
|
82
90
|
const arg = argv[i];
|
|
83
91
|
const value = () => {
|
|
@@ -130,6 +138,12 @@ export function parseServeArgs(argv) {
|
|
|
130
138
|
}
|
|
131
139
|
options.publicUrl = given.replace(/\/+$/, "");
|
|
132
140
|
}
|
|
141
|
+
else if (arg === "--tls-cert") {
|
|
142
|
+
options.tlsCert = value();
|
|
143
|
+
}
|
|
144
|
+
else if (arg === "--tls-key") {
|
|
145
|
+
options.tlsKey = value();
|
|
146
|
+
}
|
|
133
147
|
else if (arg === "--no-lookup") {
|
|
134
148
|
options.lookup = false;
|
|
135
149
|
}
|
|
@@ -175,6 +189,7 @@ export function parseServeArgs(argv) {
|
|
|
175
189
|
sawRoot = true;
|
|
176
190
|
}
|
|
177
191
|
}
|
|
192
|
+
bothOrNeither();
|
|
178
193
|
return options;
|
|
179
194
|
}
|
|
180
195
|
const TYPES = {
|
|
@@ -1898,14 +1913,20 @@ function sendFile(request, response, file) {
|
|
|
1898
1913
|
}
|
|
1899
1914
|
export function createServer(engine, options) {
|
|
1900
1915
|
const handle = createHandler(engine, options);
|
|
1901
|
-
|
|
1916
|
+
const onRequest = (request, response) => {
|
|
1902
1917
|
handle(request, response).catch(() => {
|
|
1903
1918
|
if (!response.headersSent)
|
|
1904
1919
|
json(response, 500, { error: "server error" });
|
|
1905
1920
|
else
|
|
1906
1921
|
response.end();
|
|
1907
1922
|
});
|
|
1908
|
-
}
|
|
1923
|
+
};
|
|
1924
|
+
// https when there is a certificate to serve it with, and the same handler
|
|
1925
|
+
// either way: nothing above this line knows or cares which it got.
|
|
1926
|
+
if (options.tls) {
|
|
1927
|
+
return createHttpsServer({ cert: options.tls.cert, key: options.tls.key }, onRequest);
|
|
1928
|
+
}
|
|
1929
|
+
return createHttpServer(onRequest);
|
|
1909
1930
|
}
|
|
1910
1931
|
export async function serve(argv, version = "0.1.0") {
|
|
1911
1932
|
const options = parseServeArgs(argv);
|
|
@@ -2112,6 +2133,18 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2112
2133
|
});
|
|
2113
2134
|
});
|
|
2114
2135
|
}
|
|
2136
|
+
// Read before listening, so a missing or unreadable certificate is a sentence
|
|
2137
|
+
// now rather than a connection that resets later.
|
|
2138
|
+
const tls = options.tlsCert
|
|
2139
|
+
? (() => {
|
|
2140
|
+
try {
|
|
2141
|
+
return { cert: readFileSync(options.tlsCert, "utf8"), key: readFileSync(options.tlsKey, "utf8") };
|
|
2142
|
+
}
|
|
2143
|
+
catch (error) {
|
|
2144
|
+
throw new Error(`nixamp serve: could not read the certificate: ${error.message}`);
|
|
2145
|
+
}
|
|
2146
|
+
})()
|
|
2147
|
+
: undefined;
|
|
2115
2148
|
const server = createServer(engine, {
|
|
2116
2149
|
web,
|
|
2117
2150
|
media: options.media,
|
|
@@ -2127,6 +2160,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2127
2160
|
paywall,
|
|
2128
2161
|
ffmpeg: tools.ffmpeg,
|
|
2129
2162
|
ffprobe: tools.ffprobe,
|
|
2163
|
+
...(tls ? { tls } : {}),
|
|
2130
2164
|
load: (next) => loadSource(tools, next),
|
|
2131
2165
|
...(directory ? { directory } : {}),
|
|
2132
2166
|
...(follows ? { follows, vapidPublicKey } : {}),
|
|
@@ -2181,12 +2215,12 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2181
2215
|
// is and nothing local looks public, ask. What comes back is a fact about the
|
|
2182
2216
|
// router and not about this port -- the port still has to be forwarded -- so
|
|
2183
2217
|
// it is marked as a guess and everything that prints it says so.
|
|
2184
|
-
const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2218
|
+
const localAddresses = reachableAddresses(options.host, port, options.publicUrl, tls ? "https" : "http");
|
|
2185
2219
|
const guessedPublic = options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
|
|
2186
2220
|
? await lookupPublicIp()
|
|
2187
2221
|
: "";
|
|
2188
2222
|
const addresses = guessedPublic
|
|
2189
|
-
? reachableAddresses(options.host, port,
|
|
2223
|
+
? reachableAddresses(options.host, port, `${tls ? "https" : "http"}://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`, tls ? "https" : "http")
|
|
2190
2224
|
: localAddresses;
|
|
2191
2225
|
// Listening on every interface proves the socket is open here and nothing
|
|
2192
2226
|
// about the path between here and the phone.
|
package/dist/share.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare function lookupPublicIp(send?: typeof fetch, timeoutMs?: number):
|
|
|
45
45
|
* somewhere else can open. It is labelled for what it is, because the key in
|
|
46
46
|
* the link is then the only thing between a stranger and the library.
|
|
47
47
|
*/
|
|
48
|
-
export declare function reachableAddresses(host: string, port: number, publicUrl?: string): {
|
|
48
|
+
export declare function reachableAddresses(host: string, port: number, publicUrl?: string, scheme?: "http" | "https"): {
|
|
49
49
|
label: string;
|
|
50
50
|
url: string;
|
|
51
51
|
}[];
|
package/dist/share.js
CHANGED
|
@@ -115,11 +115,11 @@ export async function lookupPublicIp(send = fetch, timeoutMs = 2500) {
|
|
|
115
115
|
* somewhere else can open. It is labelled for what it is, because the key in
|
|
116
116
|
* the link is then the only thing between a stranger and the library.
|
|
117
117
|
*/
|
|
118
|
-
export function reachableAddresses(host, port, publicUrl = "") {
|
|
118
|
+
export function reachableAddresses(host, port, publicUrl = "", scheme = "http") {
|
|
119
119
|
const link = (address) => {
|
|
120
120
|
// A bare IPv6 address needs brackets before it is a URL.
|
|
121
121
|
const authority = address.includes(":") ? `[${address}]` : address;
|
|
122
|
-
return
|
|
122
|
+
return `${scheme}://${authority}:${port}`;
|
|
123
123
|
};
|
|
124
124
|
// An address somebody told us about, because it is one this machine cannot
|
|
125
125
|
// know: a tunnel, a reverse proxy, or a router forwarding a port. It goes
|
|
@@ -145,7 +145,7 @@ export function reachableAddresses(host, port, publicUrl = "") {
|
|
|
145
145
|
found.sort((x, y) => order[x.kind] - order[y.kind]);
|
|
146
146
|
return [
|
|
147
147
|
...told,
|
|
148
|
-
{ label: "here", url:
|
|
148
|
+
{ label: "here", url: `${scheme}://localhost:${port}` },
|
|
149
149
|
...found.map(({ label, url }) => ({ label, url })),
|
|
150
150
|
];
|
|
151
151
|
}
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -96,6 +96,9 @@ Options for serve:
|
|
|
96
96
|
its own interfaces. Also NIXAMP_PUBLIC_URL
|
|
97
97
|
--no-lookup do not ask ipinfo.io what this machine's public address is
|
|
98
98
|
when nothing local looks public
|
|
99
|
+
--tls-cert FILE --tls-key FILE serve https rather than http. Needed by
|
|
100
|
+
anyone opening this from a page that is itself https, since
|
|
101
|
+
a browser refuses every request from https to http
|
|
99
102
|
--ingest accept a live stream in at POST /api/ingest
|
|
100
103
|
--rtmp-in N also listen for RTMP publishers (OBS, Larix) from port N
|
|
101
104
|
--rtmp-streams N how many may publish at once (default 3, a port each)
|
package/src/server.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createReadStream, statSync } from "node:fs";
|
|
13
13
|
import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
14
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
14
15
|
import { hostname, networkInterfaces } from "node:os";
|
|
15
16
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
17
|
import { readFileSync } from "node:fs";
|
|
@@ -131,6 +132,18 @@ export interface ServeOptions {
|
|
|
131
132
|
* listing is skipped and the printed links only work inside the house.
|
|
132
133
|
*/
|
|
133
134
|
publicUrl: string;
|
|
135
|
+
/**
|
|
136
|
+
* A certificate and its key, to serve https rather than http.
|
|
137
|
+
*
|
|
138
|
+
* Needed by anybody whose nixamp is opened from a page that is itself https:
|
|
139
|
+
* a browser refuses every request from an https page to an http one --
|
|
140
|
+
* fetch, event stream and media alike -- and no header on either side lifts
|
|
141
|
+
* that. It is deliberately not required: a nixamp on 192.168.1.5 cannot have
|
|
142
|
+
* a certificate for that address, and forcing one would put a browser
|
|
143
|
+
* warning in front of everybody at home to fix a problem they do not have.
|
|
144
|
+
*/
|
|
145
|
+
tlsCert: string;
|
|
146
|
+
tlsKey: string;
|
|
134
147
|
/**
|
|
135
148
|
* Ask an outside service what this machine's public address is, when no
|
|
136
149
|
* interface holds one and none was given. Behind NAT that is the only way to
|
|
@@ -188,6 +201,8 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
188
201
|
name: "",
|
|
189
202
|
publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
|
|
190
203
|
lookup: true,
|
|
204
|
+
tlsCert: process.env["NIXAMP_TLS_CERT"] ?? "",
|
|
205
|
+
tlsKey: process.env["NIXAMP_TLS_KEY"] ?? "",
|
|
191
206
|
x402: false,
|
|
192
207
|
owner: "",
|
|
193
208
|
ingest: false,
|
|
@@ -196,6 +211,11 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
196
211
|
rtmp: [],
|
|
197
212
|
};
|
|
198
213
|
let sawRoot = false;
|
|
214
|
+
const bothOrNeither = (): void => {
|
|
215
|
+
if (Boolean(options.tlsCert) !== Boolean(options.tlsKey)) {
|
|
216
|
+
throw new Error("nixamp serve: --tls-cert and --tls-key go together");
|
|
217
|
+
}
|
|
218
|
+
};
|
|
199
219
|
for (let i = 0; i < argv.length; i++) {
|
|
200
220
|
const arg = argv[i] as string;
|
|
201
221
|
const value = (): string => {
|
|
@@ -236,6 +256,10 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
236
256
|
throw new Error("nixamp serve: --public-url must be a URL, e.g. https://nixamp.example.com");
|
|
237
257
|
}
|
|
238
258
|
options.publicUrl = given.replace(/\/+$/, "");
|
|
259
|
+
} else if (arg === "--tls-cert") {
|
|
260
|
+
options.tlsCert = value();
|
|
261
|
+
} else if (arg === "--tls-key") {
|
|
262
|
+
options.tlsKey = value();
|
|
239
263
|
} else if (arg === "--no-lookup") {
|
|
240
264
|
options.lookup = false;
|
|
241
265
|
} else if (arg === "--name") {
|
|
@@ -271,6 +295,7 @@ export function parseServeArgs(argv: string[]): ServeOptions {
|
|
|
271
295
|
sawRoot = true;
|
|
272
296
|
}
|
|
273
297
|
}
|
|
298
|
+
bothOrNeither();
|
|
274
299
|
return options;
|
|
275
300
|
}
|
|
276
301
|
|
|
@@ -739,6 +764,8 @@ export interface HandlerOptions {
|
|
|
739
764
|
signIn?: SignIn;
|
|
740
765
|
/** True when this instance is reached over https, for the cookie's Secure. */
|
|
741
766
|
secureCookies?: boolean;
|
|
767
|
+
/** A certificate and key in PEM, when this server is to speak https itself. */
|
|
768
|
+
tls?: { cert: string; key: string };
|
|
742
769
|
/**
|
|
743
770
|
* True when a proxy sits in front, so `x-forwarded-for` names the caller.
|
|
744
771
|
* False everywhere else on purpose: the header is trivially forged, and
|
|
@@ -2201,12 +2228,19 @@ function sendFile(request: IncomingMessage, response: ServerResponse, file: stri
|
|
|
2201
2228
|
|
|
2202
2229
|
export function createServer(engine: Engine, options: HandlerOptions): Server {
|
|
2203
2230
|
const handle = createHandler(engine, options);
|
|
2204
|
-
|
|
2231
|
+
const onRequest = (request: IncomingMessage, response: ServerResponse): void => {
|
|
2205
2232
|
handle(request, response).catch(() => {
|
|
2206
2233
|
if (!response.headersSent) json(response, 500, { error: "server error" });
|
|
2207
2234
|
else response.end();
|
|
2208
2235
|
});
|
|
2209
|
-
}
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
// https when there is a certificate to serve it with, and the same handler
|
|
2239
|
+
// either way: nothing above this line knows or cares which it got.
|
|
2240
|
+
if (options.tls) {
|
|
2241
|
+
return createHttpsServer({ cert: options.tls.cert, key: options.tls.key }, onRequest) as unknown as Server;
|
|
2242
|
+
}
|
|
2243
|
+
return createHttpServer(onRequest);
|
|
2210
2244
|
}
|
|
2211
2245
|
|
|
2212
2246
|
|
|
@@ -2434,6 +2468,18 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2434
2468
|
});
|
|
2435
2469
|
}
|
|
2436
2470
|
|
|
2471
|
+
// Read before listening, so a missing or unreadable certificate is a sentence
|
|
2472
|
+
// now rather than a connection that resets later.
|
|
2473
|
+
const tls = options.tlsCert
|
|
2474
|
+
? (() => {
|
|
2475
|
+
try {
|
|
2476
|
+
return { cert: readFileSync(options.tlsCert, "utf8"), key: readFileSync(options.tlsKey, "utf8") };
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
throw new Error(`nixamp serve: could not read the certificate: ${(error as Error).message}`);
|
|
2479
|
+
}
|
|
2480
|
+
})()
|
|
2481
|
+
: undefined;
|
|
2482
|
+
|
|
2437
2483
|
const server = createServer(engine, {
|
|
2438
2484
|
web,
|
|
2439
2485
|
media: options.media,
|
|
@@ -2449,6 +2495,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2449
2495
|
paywall,
|
|
2450
2496
|
ffmpeg: tools.ffmpeg,
|
|
2451
2497
|
ffprobe: tools.ffprobe,
|
|
2498
|
+
...(tls ? { tls } : {}),
|
|
2452
2499
|
load: (next) => loadSource(tools, next),
|
|
2453
2500
|
...(directory ? { directory } : {}),
|
|
2454
2501
|
...(follows ? { follows, vapidPublicKey } : {}),
|
|
@@ -2514,13 +2561,18 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2514
2561
|
// is and nothing local looks public, ask. What comes back is a fact about the
|
|
2515
2562
|
// router and not about this port -- the port still has to be forwarded -- so
|
|
2516
2563
|
// it is marked as a guess and everything that prints it says so.
|
|
2517
|
-
const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2564
|
+
const localAddresses = reachableAddresses(options.host, port, options.publicUrl, tls ? "https" : "http");
|
|
2518
2565
|
const guessedPublic =
|
|
2519
2566
|
options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
|
|
2520
2567
|
? await lookupPublicIp()
|
|
2521
2568
|
: "";
|
|
2522
2569
|
const addresses = guessedPublic
|
|
2523
|
-
? reachableAddresses(
|
|
2570
|
+
? reachableAddresses(
|
|
2571
|
+
options.host,
|
|
2572
|
+
port,
|
|
2573
|
+
`${tls ? "https" : "http"}://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`,
|
|
2574
|
+
tls ? "https" : "http",
|
|
2575
|
+
)
|
|
2524
2576
|
: localAddresses;
|
|
2525
2577
|
|
|
2526
2578
|
// Listening on every interface proves the socket is open here and nothing
|
package/src/share.ts
CHANGED
|
@@ -132,11 +132,12 @@ export function reachableAddresses(
|
|
|
132
132
|
host: string,
|
|
133
133
|
port: number,
|
|
134
134
|
publicUrl = "",
|
|
135
|
+
scheme: "http" | "https" = "http",
|
|
135
136
|
): { label: string; url: string }[] {
|
|
136
137
|
const link = (address: string): string => {
|
|
137
138
|
// A bare IPv6 address needs brackets before it is a URL.
|
|
138
139
|
const authority = address.includes(":") ? `[${address}]` : address;
|
|
139
|
-
return
|
|
140
|
+
return `${scheme}://${authority}:${port}`;
|
|
140
141
|
};
|
|
141
142
|
|
|
142
143
|
// An address somebody told us about, because it is one this machine cannot
|
|
@@ -162,7 +163,7 @@ export function reachableAddresses(
|
|
|
162
163
|
found.sort((x, y) => order[x.kind] - order[y.kind]);
|
|
163
164
|
return [
|
|
164
165
|
...told,
|
|
165
|
-
{ label: "here", url:
|
|
166
|
+
{ label: "here", url: `${scheme}://localhost:${port}` },
|
|
166
167
|
...found.map(({ label, url }) => ({ label, url })),
|
|
167
168
|
];
|
|
168
169
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./index-
|
|
1
|
+
import{t as e}from"./index-B816i-Dk.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-Ch_vCVqk.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-DxPAVtZQ.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 y(e){return e.filter(e=>_(e.name,e.type)).sort((e,t)=>v(b(e),b(t))).map(e=>({title:n(e.name),artist:``,album:x(b(e)),duration:0,url:URL.createObjectURL(e),video:i(e.name,e.type),objectUrl:!0}))}function b(e){return e.webkitRelativePath||e.name}function x(e){let t=e.split(`/`);return t.length>1?t[t.length-2]:``}function ee(e){for(let t of e)t.objectUrl&&URL.revokeObjectURL(t.url)}var S=2048;function C(e){return e===`audio`}var te=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(w(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=S,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=e.video||!C(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 w(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 T(){return{revision:0,tracks:[],trackCount:0,index:0,playing:!1,position:0,bars:[],levels:[0,0],silent:!0,note:``,root:``}}function ne(e,t){return{...t,tracks:t.tracks??e.tracks}}function E(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 D(e,t){return`${e===``?``:E(e)}${t.startsWith(`/`)?t:`/${t}`}`}function O(e,t){return D(e,`/api/media/${t}`)}function k(e){if(typeof e!=`object`||!e)return null;let t=e,n=T(),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}:{}}}):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 re=class{handlers;source=null;base=``;lastRevision=-1;constructor(e){this.handlers=e}get address(){return this.base}get connected(){return this.source!==null}connect(e){let t=E(e);this.close(),this.base=t,this.lastRevision=-1,this.handlers.onStatus(`connecting`);let n=new EventSource(D(t,`/api/events`));this.source=n,n.onopen=()=>this.handlers.onStatus(`live`),n.onmessage=e=>{let t=k(A(e.data));t&&(t.revision<this.lastRevision||(this.lastRevision=t.revision,this.handlers.onStatus(`live`),this.handlers.onSnapshot(t)))},n.onerror=()=>{this.handlers.onStatus(`error`,`reconnecting…`)}}async send(e){if(this.base===``&&!this.connected)return;let t=await fetch(D(this.base,`/api/command`),{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=k(await t.json());n&&this.handlers.onSnapshot(n)}media(e){return O(this.base,e)}close(){this.source?.close(),this.source=null}};function A(e){try{return JSON.parse(e)}catch{return null}}async function ie(e,t){try{let n=await fetch(D(e,`/api/state`),{signal:t});return n.ok?k(await n.json()):null}catch{return null}}async function ae(e,t){try{let n=await fetch(D(e,`/api/health`),{signal:t});if(!n.ok)return null;let r=await n.json();return r.name===`nixamp`?r.version??`unknown`:null}catch{return null}}function oe(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 se(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 ce(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 le(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 ue(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 P=`nixamp.remote`,F=`nixamp.volume`;function I(e){let t=document.getElementById(e);if(!t)throw Error(`nixamp: #${e} is missing from the shell`);return t}function L(){let n={status:I(`status`),source:I(`source`),install:I(`install`),video:I(`video`),audio:I(`audio`),title:I(`title-line`),album:I(`album-line`),elapsed:I(`elapsed`),total:I(`total`),seek:I(`seek`),canvas:I(`spectrum`),glyphs:I(`glyphs`),levels:I(`levels`),playlist:I(`playlist`),playlistTitle:I(`playlist-panel`),note:I(`note`),files:I(`files`),folder:I(`folder`),remoteUrl:I(`remote-url`),remoteForm:I(`remote-form`),remoteState:I(`remote-state`),disconnect:I(`disconnect`),browse:I(`browse`),accountForm:I(`account-form`),accountEmail:I(`account-email`),accountPassword:I(`account-password`),accountSubmit:I(`account-submit`),accountToggle:I(`account-toggle`),accountProviders:I(`account-providers`),accountPanel:I(`account-panel`),accountElsewhere:I(`account-elsewhere`),accountSignOut:I(`account-signout`),accountNote:I(`account-note`),adminPanel:I(`admin-panel`),adminNote:I(`admin-note`),adminConnections:I(`admin-connections`),adminRestream:I(`admin-restream`),adminSource:I(`admin-source`),directory:I(`directory`),recentNote:I(`recent-note`),recentList:I(`recent-list`),followingNote:I(`following-note`),followingList:I(`following-list`),notifyPanel:I(`notify-panel`),notifyNote:I(`notify-note`),notifyWeb:I(`notify-web`),notifyEmail:I(`notify-email`),notifySms:I(`notify-sms`),notifyPhone:I(`notify-phone`),notifyPhoneForm:I(`notify-phone-form`),notifyPhoneNote:I(`notify-phone-note`),directoryNote:I(`directory-note`),directoryList:I(`directory-list`),listenHere:I(`listen-here`),volume:I(`volume`),prev:I(`prev`),playPause:I(`play-pause`),stop:I(`stop`),next:I(`next`)},r=`local`,i=[],a=0,o=T(),s=`idle`,c=``,l=`Pick files, or connect to a nixamp running somewhere else.`,u=!1,d=Array(24).fill(0),f=Array(24).fill(0),p=[],m=()=>r===`remote`&&!n.listenHere.checked,h=new te({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:()=>A(1),onState:()=>L(),onError:e=>{l=e,L()}}),g=new re({onSnapshot:e=>{o=ne(o,e),m()&&(d=e.bars.length>0?e.bars:d,f=N(f,d)),L()},onStatus:(e,t)=>{s=e,c=t??``,L()}}),_=()=>r===`remote`?o.tracks.length:i.length,v=()=>r===`remote`?o.index:a,b=()=>{if(r===`remote`){let e=o.tracks[o.index];return e?t(e):`Nothing loaded.`}let e=i[a];return e?t(e):`Nothing loaded.`},x=()=>(r===`remote`?o.tracks[o.index]:i[a])?.album||`—`,S=()=>m()?o.tracks[o.index]?.duration??0:h.duration,C=()=>m()?o.position:h.position,w=()=>m()?o.playing:h.playing;async function D(e){if(r===`remote`){if(m()){await g.send({type:`play`,index:e});return}await g.send({type:`select`,index:e}),await O(e);return}let t=i[e];t&&(a=e,await h.load(t,!0),B(t.video),V(),L())}async function O(e){let t=o.tracks[e];t&&(await h.load({title:t.title,artist:t.artist,album:t.album,duration:t.duration,url:g.media(e),video:t.video===!0,objectUrl:!1},!0),B(t.video===!0),V())}async function k(){if(m()){await g.send({type:`toggle`});return}_()!==0&&(h.playing?h.pause():h.position>0?await h.play():await D(v()),L())}async function A(e){let t=_();if(t!==0){if(m()){await g.send({type:e>0?`next`:`prev`});return}await D((v()+e+t)%t)}}async function j(){if(m()){await g.send({type:`stop`});return}h.stop(),d=Array(24).fill(0),f=[...d],L()}let M=e=>`▁▂▃▄▅▆▇█`[Math.max(0,Math.min(7,Math.round(e*7)))];function L(){let t=_(),a=w();n.status.textContent=a?`▶ PLAYING`:`■ STOPPED`,n.status.dataset.playing=String(a),n.title.textContent=b(),n.album.textContent=x();let f=C(),p=S();n.elapsed.textContent=e(f),n.total.textContent=p>0?e(p):`--:--`,u||(n.seek.value=String(p>0?Math.round(f/p*1e3):0),n.seek.disabled=p<=0||m()),n.playPause.textContent=a?`❚❚`:`▶`,n.playPause.setAttribute(`aria-label`,a?`Pause`:`Play`),n.playlistTitle.dataset.title=`Playlist (${t})`,n.source.textContent=r===`remote`?`remote · ${g.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 v=r===`remote`&&o.note!==``?o.note:l;n.note.textContent=v,n.note.hidden=v===``,de(),n.glyphs.textContent=d.map(M).join(``);let[y,ee]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(y*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(ee*6)).padEnd(6,`·`)}`}let R=``;function de(){let a=r===`remote`?o.tracks.map(e=>[t(e),e.duration]):i.map(e=>[t(e),e.duration]),s=`${r}:${a.map(([e,t])=>`${e}@${t}`).join(`|`)}`;s!==R&&(R=s,n.playlist.replaceChildren(...a.map(([t,n],r)=>{let i=document.createElement(`li`);i.className=`row`,i.dataset.index=String(r);let a=document.createElement(`span`);a.className=`n`,a.textContent=String(r+1).padStart(2,` `);let o=document.createElement(`span`);o.className=`name`,o.textContent=t;let s=document.createElement(`span`);return s.className=`time`,s.textContent=n>0?e(n):`--:--`,i.append(a,o,s),i})));let c=v(),l=w();Array.from(n.playlist.children).forEach((e,t)=>{let n=e;n.classList.toggle(`selected`,t===c),n.classList.toggle(`playing`,t===c&&l)}),n.playlist.children[c]?.scrollIntoView({block:`nearest`})}function z(){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(m())f=N(f,d);else{let e=h.read();e.length>0&&(p.length!==25&&(p=se(24,e.length)),d=le(d,ce(e,p)),f=N(f,d))}if(s){let e=getComputedStyle(document.documentElement);ue(s,{width:t.width,height:t.height},d,f,{bar:e.getPropertyValue(`--green`).trim()||`#4af689`,peak:e.getPropertyValue(`--green-dim`).trim()||`#227a4a`,background:`transparent`})}if(w()){n.glyphs.textContent=d.map(M).join(``);let[t,r]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(t*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(r*6)).padEnd(6,`·`)}`,n.elapsed.textContent=e(C());let i=S();!u&&i>0&&(n.seek.value=String(Math.round(C()/i*1e3)))}requestAnimationFrame(z)}function B(e){n.video.hidden=!e}function V(){`mediaSession`in navigator&&(navigator.mediaSession.metadata=new MediaMetadata({title:b(),album:x(),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 A(1)),navigator.mediaSession.setActionHandler(`previoustrack`,()=>void A(-1)))}n.playlist.addEventListener(`click`,e=>{let t=e.target.closest(`li`),n=Number(t?.dataset.index);Number.isInteger(n)&&D(n)}),n.prev.addEventListener(`click`,()=>void A(-1)),n.next.addEventListener(`click`,()=>void A(1)),n.stop.addEventListener(`click`,()=>void j()),n.playPause.addEventListener(`click`,()=>void k()),n.seek.addEventListener(`input`,()=>{u=!0}),n.seek.addEventListener(`change`,()=>{let e=S();e>0&&h.seek(Number(n.seek.value)/1e3*e),u=!1}),n.volume.addEventListener(`input`,()=>{let e=Number(n.volume.value)/100;h.volume=e;try{localStorage.setItem(F,String(e))}catch{}});let H=e=>{e.addEventListener(`change`,()=>{let t=y(Array.from(e.files??[]));if(t.length===0){l=`Nothing playable in that selection.`,L();return}ee(i),i=t,a=0,r=`local`,g.close(),l=``,D(0)})};H(n.files),H(n.folder),n.remoteForm.addEventListener(`submit`,e=>{e.preventDefault();let t=E(n.remoteUrl.value);if(t===``){l=`That is not an address.`,L();return}(async()=>{s=`connecting`,L();let e=oe(t);if(e){s=`error`,c=e,l=e,r=`local`,L();return}if(await ae(t)===null){s=`error`,c=`no nixamp answered there`,r=`local`,L();return}r=`remote`,l=``;try{localStorage.setItem(P,t)}catch{}g.connect(t),L()})()});let U=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??[],pe(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&&Z&&t.ownerId!==Z&&e.append(J(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),U()}let W=null,fe=e=>{n.adminConnections.replaceChildren();let t=document.createElement(`tr`);for(let e of[`Where`,`Network`,`Kind`,`Client`,`Track`,`Sent`]){let n=document.createElement(`th`);n.textContent=e,t.append(n)}n.adminConnections.append(t);for(let t of e.slice(0,40)){let e=document.createElement(`tr`);t.endedAt!==null&&(e.className=`ended`);let r=[[t.address,``],[t.network,`network-${t.network}`],[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)}},G=async()=>{try{let e=await fetch(`/api/connections`);if(!e.ok)return;let t=await e.json();n.adminNote.textContent=`${t.active??0} listening now.`,fe(t.connections??[])}catch{n.adminNote.textContent=`lost touch with the server`}},K=async()=>{let e=!1,t=null;try{let n=await fetch(`/api/admin`);if(n.ok){let r=await n.json();e=r.allowed===!0,t=r.as??null}}catch{e=!1}n.adminPanel.hidden=!e,W&&clearInterval(W),W=null,e&&(n.adminNote.textContent=t===`owner`?`You own this server.`:`You hold this server's control link.`,G(),W=setInterval(()=>void G(),2e3))};n.adminRestream.addEventListener(`submit`,e=>{e.preventDefault();let t=n.adminSource.value.trim();t&&(async()=>{try{let e=await fetch(`/api/source`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({source:t})}),r=await e.json();n.adminNote.textContent=e.ok?`Now serving ${t}.`:r.error??`that did not work`,e.ok&&(n.adminSource.value=``)}catch{n.adminNote.textContent=`could not reach the server`}})()});let q=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`},pe=e=>{n.recentList.replaceChildren();let t=Z?e.filter(e=>e.ownerId&&e.ownerId!==Z):[];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 ${q(e.endedAt)}`:`ended ${q(e.endedAt)}`,r.append(i,a),t.append(r,J(e.ownerId,e.name)),n.recentList.append(t)}},me=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}},J=(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),me())}catch{}finally{n.disabled=!1}})()}),n},he=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},ge=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,_e=async()=>{if(!ge())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:he(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}},ve=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{}},Y=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`}},ye=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=ge()&&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 _e();n.notifyWeb.checked=e,await Y({wantsWeb:e});return}await ve(),await Y({wantsWeb:!1}),n.notifyNote.textContent=`Turn on “On this device” to be told here.`})()}),n.notifyEmail.addEventListener(`change`,()=>{Y({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 Y({wantsSms:n.notifySms.checked})})()}),n.notifyPhoneForm.addEventListener(`submit`,e=>{e.preventDefault(),Y({phone:n.notifyPhone.value.trim()})});let X=!1,Z=``,Q=e=>{let t=e!==null;n.notifyPanel.hidden=!t,t?(ye(),me()):(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}.`:X?`Create an account on nixamp.com.`:`Sign in to nixamp.com to publish and get paid.`,n.accountSubmit.textContent=X?`Create account`:`Sign in`,n.accountToggle.textContent=X?`I have one`:`Create one`,n.accountPassword.autocomplete=X?`new-password`:`current-password`};n.accountToggle.addEventListener(`click`,()=>{X=!X,Q(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/${X?`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}Z=i.account?.id??``,n.accountPassword.value=``,Q(i.account?.email??t),K()}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{}Z=``,Q(null),K()})()}),(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)}})(),(async()=>{try{let e=await fetch(`/api/v1/auth/me`),t=await e.json();Z=e.ok?t.account?.id??``:``,Q(e.ok?t.account?.email??`you`:null)}catch{Z=``,Q(null)}})(),K(),n.browse.addEventListener(`click`,()=>{if(!n.directory.hidden){n.directory.hidden=!0;return}U(),n.directory.scrollIntoView({behavior:`smooth`,block:`nearest`})}),n.disconnect.addEventListener(`click`,()=>{g.close(),r=`local`,s=`idle`,c=``,L()}),n.listenHere.addEventListener(`change`,()=>{r===`remote`&&(async()=>{n.listenHere.checked?(await g.send({type:`stop`}),await O(o.index)):h.stop(),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`:j();return;case`n`:case`ArrowRight`:A(1);return;case`p`:case`ArrowLeft`:A(-1);return;case`ArrowDown`:e.preventDefault(),D(Math.min(_()-1,v()+1));return;case`ArrowUp`:e.preventDefault(),D(Math.max(0,v()-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(F);e!==null&&(n.volume.value=String(Math.round(Number(e)*100)),h.volume=Number(e));let t=localStorage.getItem(P);t&&(n.remoteUrl.value=t)}catch{}(async()=>{if(n.remoteUrl.value!==``)return;let e=globalThis.location.origin;if(await ae(e)===null)return;let t=await ie(e);t&&t.trackCount!==0&&(n.remoteUrl.value=e,r=`remote`,l=``,g.connect(e),L())})(),L(),requestAnimationFrame(z)}L(),`serviceWorker`in navigator&&globalThis.addEventListener(`load`,()=>{navigator.serviceWorker.register(`/sw.js`).catch(()=>{})});export{c as t};
|