nixamp 0.5.10 → 0.5.11
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/attach.js +5 -1
- package/dist/protocol.d.ts +34 -2
- package/dist/protocol.js +11 -0
- package/dist/server.d.ts +14 -2
- package/dist/server.js +25 -12
- package/package.json +1 -1
- package/src/attach.ts +5 -1
- package/src/protocol.ts +37 -2
- package/src/server.ts +28 -13
- package/web/dist/assets/{hls-3VKVEQE3-Cj5Lwh7A.js → hls-3VKVEQE3-Bug6MFON.js} +1 -1
- package/web/dist/assets/index-CRfW9BIP.js +1 -0
- package/web/dist/assets/{mpegts-CKFUPaK9.js → mpegts-BtEekTJ6.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-Bj_ZfP5s.js → mpegts-LO6RVLD6-DW1kEon1.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +5 -5
- package/web/dist/assets/index-ABOb54bx.js +0 -1
package/dist/attach.js
CHANGED
|
@@ -23,7 +23,11 @@ import { KEY_HEADER } from "./share.js";
|
|
|
23
23
|
const RECONNECT_MS = 1000;
|
|
24
24
|
/** A remote track has no path, because no filesystem path leaves the machine. */
|
|
25
25
|
export function applySnapshot(state, snapshot) {
|
|
26
|
-
|
|
26
|
+
// A frame without a list is not an empty library, it is a frame with nothing
|
|
27
|
+
// new to say about it -- which is every frame but the first.
|
|
28
|
+
if (snapshot.tracks !== undefined) {
|
|
29
|
+
state.tracks = snapshot.tracks.map((track) => ({ path: "", ...track }));
|
|
30
|
+
}
|
|
27
31
|
state.index = snapshot.index;
|
|
28
32
|
state.playing = snapshot.playing;
|
|
29
33
|
state.position = snapshot.position;
|
package/dist/protocol.d.ts
CHANGED
|
@@ -25,7 +25,24 @@ export interface RemoteTrack {
|
|
|
25
25
|
export interface Snapshot {
|
|
26
26
|
/** Bumped on every push so a client can drop an out-of-order frame. */
|
|
27
27
|
revision: number;
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The library, sent when it is news and left out when it is not.
|
|
30
|
+
*
|
|
31
|
+
* It used to ride in every frame. At twelve frames a second over a library of
|
|
32
|
+
* five thousand, that is five megabytes a second of JSON for a client to
|
|
33
|
+
* parse on the thread that is also decoding the audio -- which is exactly
|
|
34
|
+
* what it sounded like. It is sent on the first frame of a subscription and
|
|
35
|
+
* again whenever the list actually changes; absent means "the same as
|
|
36
|
+
* before", and a client keeps what it had.
|
|
37
|
+
*/
|
|
38
|
+
tracks?: RemoteTrack[];
|
|
39
|
+
/**
|
|
40
|
+
* How many tracks there are, in every frame.
|
|
41
|
+
*
|
|
42
|
+
* A client that has not received a list yet still has to draw something, and
|
|
43
|
+
* a count is four bytes rather than half a megabyte.
|
|
44
|
+
*/
|
|
45
|
+
trackCount: number;
|
|
29
46
|
index: number;
|
|
30
47
|
playing: boolean;
|
|
31
48
|
position: number;
|
|
@@ -61,4 +78,19 @@ export declare const COMMAND_TYPES: readonly ["play", "toggle", "stop", "next",
|
|
|
61
78
|
export declare function parseCommand(input: unknown): Command | null;
|
|
62
79
|
/** The name a remote shows for a track. */
|
|
63
80
|
export declare function remoteName(track: RemoteTrack): string;
|
|
64
|
-
export declare function emptySnapshot():
|
|
81
|
+
export declare function emptySnapshot(): FullSnapshot;
|
|
82
|
+
/**
|
|
83
|
+
* Fold a frame into what the client already had.
|
|
84
|
+
*
|
|
85
|
+
* A frame without `tracks` is not a frame with no tracks: it is a frame that
|
|
86
|
+
* had nothing new to say about them. Every client needs this, so none of them
|
|
87
|
+
* should write it twice.
|
|
88
|
+
*/
|
|
89
|
+
export declare function merge(previous: FullSnapshot, incoming: Snapshot): FullSnapshot;
|
|
90
|
+
/**
|
|
91
|
+
* A snapshot a client has already folded, so the library is known to be there.
|
|
92
|
+
* Every reader wants this one; only the wire carries the other.
|
|
93
|
+
*/
|
|
94
|
+
export type FullSnapshot = Snapshot & {
|
|
95
|
+
tracks: RemoteTrack[];
|
|
96
|
+
};
|
package/dist/protocol.js
CHANGED
|
@@ -41,6 +41,7 @@ export function emptySnapshot() {
|
|
|
41
41
|
return {
|
|
42
42
|
revision: 0,
|
|
43
43
|
tracks: [],
|
|
44
|
+
trackCount: 0,
|
|
44
45
|
index: 0,
|
|
45
46
|
playing: false,
|
|
46
47
|
position: 0,
|
|
@@ -51,3 +52,13 @@ export function emptySnapshot() {
|
|
|
51
52
|
root: "",
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Fold a frame into what the client already had.
|
|
57
|
+
*
|
|
58
|
+
* A frame without `tracks` is not a frame with no tracks: it is a frame that
|
|
59
|
+
* had nothing new to say about them. Every client needs this, so none of them
|
|
60
|
+
* should write it twice.
|
|
61
|
+
*/
|
|
62
|
+
export function merge(previous, incoming) {
|
|
63
|
+
return { ...incoming, tracks: incoming.tracks ?? previous.tracks };
|
|
64
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -111,7 +111,8 @@ export declare function parseRange(header: string | undefined, size: number): By
|
|
|
111
111
|
export declare function safeJoin(rootDir: string, urlPath: string): string | null;
|
|
112
112
|
/** What the HTTP layer needs from a player. Tests hand it a fake. */
|
|
113
113
|
export interface Engine {
|
|
114
|
-
|
|
114
|
+
/** `withTracks` false leaves the library out, for a frame that is only motion. */
|
|
115
|
+
snapshot(withTracks?: boolean): Snapshot;
|
|
115
116
|
command(command: Command): void;
|
|
116
117
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
117
118
|
/** Absolute path of a track, or undefined when the index is not one. */
|
|
@@ -161,7 +162,11 @@ export declare class PlayerEngine implements Engine {
|
|
|
161
162
|
fps?: number);
|
|
162
163
|
private readonly silent;
|
|
163
164
|
private consume;
|
|
164
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The current state. `withTracks` carries the library, which is worth half a
|
|
167
|
+
* megabyte on a real one and is only news when it has changed.
|
|
168
|
+
*/
|
|
169
|
+
snapshot(withTracks?: boolean): Snapshot;
|
|
165
170
|
trackPath(index: number): string | undefined;
|
|
166
171
|
command(command: Command): void;
|
|
167
172
|
private clamp;
|
|
@@ -169,6 +174,13 @@ export declare class PlayerEngine implements Engine {
|
|
|
169
174
|
private start;
|
|
170
175
|
private halt;
|
|
171
176
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
177
|
+
/**
|
|
178
|
+
* Send the state to everyone watching.
|
|
179
|
+
*
|
|
180
|
+
* The library goes only when `listChanged` says it has, which is what turned
|
|
181
|
+
* five megabytes a second into a few kilobytes: an analyser tick has nothing
|
|
182
|
+
* to say about the track list, and it fires twelve times a second.
|
|
183
|
+
*/
|
|
172
184
|
private push;
|
|
173
185
|
stop(): void;
|
|
174
186
|
replace(tracks: Track[], root: string): void;
|
package/dist/server.js
CHANGED
|
@@ -348,10 +348,15 @@ export class PlayerEngine {
|
|
|
348
348
|
this.pending = joined.subarray(at);
|
|
349
349
|
this.dirty = true;
|
|
350
350
|
}
|
|
351
|
-
|
|
351
|
+
/**
|
|
352
|
+
* The current state. `withTracks` carries the library, which is worth half a
|
|
353
|
+
* megabyte on a real one and is only news when it has changed.
|
|
354
|
+
*/
|
|
355
|
+
snapshot(withTracks = true) {
|
|
352
356
|
return {
|
|
353
357
|
revision: this.revision,
|
|
354
|
-
tracks: toRemoteTracks(this.tracks),
|
|
358
|
+
...(withTracks ? { tracks: toRemoteTracks(this.tracks) } : {}),
|
|
359
|
+
trackCount: this.tracks.length,
|
|
355
360
|
index: this.state.index,
|
|
356
361
|
playing: this.state.playing,
|
|
357
362
|
position: this.state.position,
|
|
@@ -445,11 +450,18 @@ export class PlayerEngine {
|
|
|
445
450
|
}
|
|
446
451
|
};
|
|
447
452
|
}
|
|
448
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Send the state to everyone watching.
|
|
455
|
+
*
|
|
456
|
+
* The library goes only when `listChanged` says it has, which is what turned
|
|
457
|
+
* five megabytes a second into a few kilobytes: an analyser tick has nothing
|
|
458
|
+
* to say about the track list, and it fires twelve times a second.
|
|
459
|
+
*/
|
|
460
|
+
push(listChanged = false) {
|
|
449
461
|
this.revision++;
|
|
450
462
|
if (this.listeners.size === 0)
|
|
451
463
|
return;
|
|
452
|
-
const snapshot = this.snapshot();
|
|
464
|
+
const snapshot = this.snapshot(listChanged);
|
|
453
465
|
for (const listener of this.listeners)
|
|
454
466
|
listener(snapshot);
|
|
455
467
|
}
|
|
@@ -468,7 +480,7 @@ export class PlayerEngine {
|
|
|
468
480
|
this.state.index = 0;
|
|
469
481
|
this.state.position = 0;
|
|
470
482
|
this.state.note = "";
|
|
471
|
-
this.push();
|
|
483
|
+
this.push(true);
|
|
472
484
|
}
|
|
473
485
|
retag(tracks, root) {
|
|
474
486
|
// Dropped rather than applied if the library moved underneath: somebody
|
|
@@ -480,8 +492,9 @@ export class PlayerEngine {
|
|
|
480
492
|
return;
|
|
481
493
|
this.tracks = tracks;
|
|
482
494
|
// No stop, no index reset: the only thing that changes is what the titles
|
|
483
|
-
// say, and every remote finds out because a snapshot goes out
|
|
484
|
-
this.
|
|
495
|
+
// say, and every remote finds out because a snapshot goes out -- carrying
|
|
496
|
+
// the list, since the titles are the whole point of this one.
|
|
497
|
+
this.push(true);
|
|
485
498
|
}
|
|
486
499
|
}
|
|
487
500
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
@@ -1540,7 +1553,7 @@ export function createHandler(engine, options) {
|
|
|
1540
1553
|
json(response, 404, { error: "no such track" });
|
|
1541
1554
|
return;
|
|
1542
1555
|
}
|
|
1543
|
-
watch(request, response, "media", engine.snapshot().tracks[index]?.title ?? file);
|
|
1556
|
+
watch(request, response, "media", engine.snapshot().tracks?.[index]?.title ?? file);
|
|
1544
1557
|
// A browser asks for every track here, and a matroska or an avi handed
|
|
1545
1558
|
// to it raw is bytes it cannot play. Seeking is what this route is for
|
|
1546
1559
|
// and transcoding gives it up, but an unseekable film beats a silent
|
|
@@ -1572,11 +1585,11 @@ export function createHandler(engine, options) {
|
|
|
1572
1585
|
return;
|
|
1573
1586
|
}
|
|
1574
1587
|
const current = engine.snapshot();
|
|
1575
|
-
if (current.
|
|
1588
|
+
if (current.trackCount === 0) {
|
|
1576
1589
|
json(response, 404, { error: "nothing is playing" });
|
|
1577
1590
|
return;
|
|
1578
1591
|
}
|
|
1579
|
-
watch(request, response, "stream", current.tracks[current.index]?.title ?? "live");
|
|
1592
|
+
watch(request, response, "stream", current.tracks?.[current.index]?.title ?? "live");
|
|
1580
1593
|
liveAudio(request, response, engine, options.ffmpeg ?? ["ffmpeg"]);
|
|
1581
1594
|
return;
|
|
1582
1595
|
}
|
|
@@ -1591,7 +1604,7 @@ export function createHandler(engine, options) {
|
|
|
1591
1604
|
json(response, 403, { error: "media streaming is off" });
|
|
1592
1605
|
return;
|
|
1593
1606
|
}
|
|
1594
|
-
watch(request, response, "stream", engine.snapshot().tracks[index]?.title ?? source);
|
|
1607
|
+
watch(request, response, "stream", engine.snapshot().tracks?.[index]?.title ?? source);
|
|
1595
1608
|
transcode(request, response, source, options.ffmpeg ?? ["ffmpeg"]);
|
|
1596
1609
|
return;
|
|
1597
1610
|
}
|
|
@@ -2341,7 +2354,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2341
2354
|
},
|
|
2342
2355
|
nowPlaying: () => {
|
|
2343
2356
|
const snapshot = engine.snapshot();
|
|
2344
|
-
return snapshot.tracks[snapshot.index]?.title ?? "";
|
|
2357
|
+
return snapshot.tracks?.[snapshot.index]?.title ?? "";
|
|
2345
2358
|
},
|
|
2346
2359
|
onConfig: (remote) => {
|
|
2347
2360
|
const next = applyRemoteConfig(paywallConfig, remote?.x402);
|
package/package.json
CHANGED
package/src/attach.ts
CHANGED
|
@@ -26,7 +26,11 @@ const RECONNECT_MS = 1000;
|
|
|
26
26
|
|
|
27
27
|
/** A remote track has no path, because no filesystem path leaves the machine. */
|
|
28
28
|
export function applySnapshot(state: State, snapshot: Snapshot): void {
|
|
29
|
-
|
|
29
|
+
// A frame without a list is not an empty library, it is a frame with nothing
|
|
30
|
+
// new to say about it -- which is every frame but the first.
|
|
31
|
+
if (snapshot.tracks !== undefined) {
|
|
32
|
+
state.tracks = snapshot.tracks.map((track) => ({ path: "", ...track }));
|
|
33
|
+
}
|
|
30
34
|
state.index = snapshot.index;
|
|
31
35
|
state.playing = snapshot.playing;
|
|
32
36
|
state.position = snapshot.position;
|
package/src/protocol.ts
CHANGED
|
@@ -27,7 +27,24 @@ export interface RemoteTrack {
|
|
|
27
27
|
export interface Snapshot {
|
|
28
28
|
/** Bumped on every push so a client can drop an out-of-order frame. */
|
|
29
29
|
revision: number;
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The library, sent when it is news and left out when it is not.
|
|
32
|
+
*
|
|
33
|
+
* It used to ride in every frame. At twelve frames a second over a library of
|
|
34
|
+
* five thousand, that is five megabytes a second of JSON for a client to
|
|
35
|
+
* parse on the thread that is also decoding the audio -- which is exactly
|
|
36
|
+
* what it sounded like. It is sent on the first frame of a subscription and
|
|
37
|
+
* again whenever the list actually changes; absent means "the same as
|
|
38
|
+
* before", and a client keeps what it had.
|
|
39
|
+
*/
|
|
40
|
+
tracks?: RemoteTrack[];
|
|
41
|
+
/**
|
|
42
|
+
* How many tracks there are, in every frame.
|
|
43
|
+
*
|
|
44
|
+
* A client that has not received a list yet still has to draw something, and
|
|
45
|
+
* a count is four bytes rather than half a megabyte.
|
|
46
|
+
*/
|
|
47
|
+
trackCount: number;
|
|
31
48
|
index: number;
|
|
32
49
|
playing: boolean;
|
|
33
50
|
position: number;
|
|
@@ -83,10 +100,11 @@ export function remoteName(track: RemoteTrack): string {
|
|
|
83
100
|
return track.artist ? `${track.artist} — ${track.title}` : track.title;
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
export function emptySnapshot():
|
|
103
|
+
export function emptySnapshot(): FullSnapshot {
|
|
87
104
|
return {
|
|
88
105
|
revision: 0,
|
|
89
106
|
tracks: [],
|
|
107
|
+
trackCount: 0,
|
|
90
108
|
index: 0,
|
|
91
109
|
playing: false,
|
|
92
110
|
position: 0,
|
|
@@ -97,3 +115,20 @@ export function emptySnapshot(): Snapshot {
|
|
|
97
115
|
root: "",
|
|
98
116
|
};
|
|
99
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fold a frame into what the client already had.
|
|
121
|
+
*
|
|
122
|
+
* A frame without `tracks` is not a frame with no tracks: it is a frame that
|
|
123
|
+
* had nothing new to say about them. Every client needs this, so none of them
|
|
124
|
+
* should write it twice.
|
|
125
|
+
*/
|
|
126
|
+
export function merge(previous: FullSnapshot, incoming: Snapshot): FullSnapshot {
|
|
127
|
+
return { ...incoming, tracks: incoming.tracks ?? previous.tracks };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A snapshot a client has already folded, so the library is known to be there.
|
|
132
|
+
* Every reader wants this one; only the wire carries the other.
|
|
133
|
+
*/
|
|
134
|
+
export type FullSnapshot = Snapshot & { tracks: RemoteTrack[] };
|
package/src/server.ts
CHANGED
|
@@ -362,7 +362,8 @@ export function safeJoin(rootDir: string, urlPath: string): string | null {
|
|
|
362
362
|
|
|
363
363
|
/** What the HTTP layer needs from a player. Tests hand it a fake. */
|
|
364
364
|
export interface Engine {
|
|
365
|
-
|
|
365
|
+
/** `withTracks` false leaves the library out, for a frame that is only motion. */
|
|
366
|
+
snapshot(withTracks?: boolean): Snapshot;
|
|
366
367
|
command(command: Command): void;
|
|
367
368
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
368
369
|
/** Absolute path of a track, or undefined when the index is not one. */
|
|
@@ -476,10 +477,15 @@ export class PlayerEngine implements Engine {
|
|
|
476
477
|
this.dirty = true;
|
|
477
478
|
}
|
|
478
479
|
|
|
479
|
-
|
|
480
|
+
/**
|
|
481
|
+
* The current state. `withTracks` carries the library, which is worth half a
|
|
482
|
+
* megabyte on a real one and is only news when it has changed.
|
|
483
|
+
*/
|
|
484
|
+
snapshot(withTracks = true): Snapshot {
|
|
480
485
|
return {
|
|
481
486
|
revision: this.revision,
|
|
482
|
-
tracks: toRemoteTracks(this.tracks),
|
|
487
|
+
...(withTracks ? { tracks: toRemoteTracks(this.tracks) } : {}),
|
|
488
|
+
trackCount: this.tracks.length,
|
|
483
489
|
index: this.state.index,
|
|
484
490
|
playing: this.state.playing,
|
|
485
491
|
position: this.state.position,
|
|
@@ -569,10 +575,17 @@ export class PlayerEngine implements Engine {
|
|
|
569
575
|
};
|
|
570
576
|
}
|
|
571
577
|
|
|
572
|
-
|
|
578
|
+
/**
|
|
579
|
+
* Send the state to everyone watching.
|
|
580
|
+
*
|
|
581
|
+
* The library goes only when `listChanged` says it has, which is what turned
|
|
582
|
+
* five megabytes a second into a few kilobytes: an analyser tick has nothing
|
|
583
|
+
* to say about the track list, and it fires twelve times a second.
|
|
584
|
+
*/
|
|
585
|
+
private push(listChanged = false): void {
|
|
573
586
|
this.revision++;
|
|
574
587
|
if (this.listeners.size === 0) return;
|
|
575
|
-
const snapshot = this.snapshot();
|
|
588
|
+
const snapshot = this.snapshot(listChanged);
|
|
576
589
|
for (const listener of this.listeners) listener(snapshot);
|
|
577
590
|
}
|
|
578
591
|
|
|
@@ -592,7 +605,7 @@ export class PlayerEngine implements Engine {
|
|
|
592
605
|
this.state.index = 0;
|
|
593
606
|
this.state.position = 0;
|
|
594
607
|
this.state.note = "";
|
|
595
|
-
this.push();
|
|
608
|
+
this.push(true);
|
|
596
609
|
}
|
|
597
610
|
|
|
598
611
|
retag(tracks: Track[], root: string): void {
|
|
@@ -603,8 +616,9 @@ export class PlayerEngine implements Engine {
|
|
|
603
616
|
if (tracks.some((track, at) => track.path !== this.tracks[at]?.path)) return;
|
|
604
617
|
this.tracks = tracks;
|
|
605
618
|
// No stop, no index reset: the only thing that changes is what the titles
|
|
606
|
-
// say, and every remote finds out because a snapshot goes out
|
|
607
|
-
this.
|
|
619
|
+
// say, and every remote finds out because a snapshot goes out -- carrying
|
|
620
|
+
// the list, since the titles are the whole point of this one.
|
|
621
|
+
this.push(true);
|
|
608
622
|
}
|
|
609
623
|
}
|
|
610
624
|
|
|
@@ -614,6 +628,7 @@ export class EmptyEngine implements Engine {
|
|
|
614
628
|
snapshot(): Snapshot {
|
|
615
629
|
return { ...emptySnapshot(), note: this.note };
|
|
616
630
|
}
|
|
631
|
+
|
|
617
632
|
command(): void {}
|
|
618
633
|
subscribe(listener: (snapshot: Snapshot) => void): () => void {
|
|
619
634
|
listener(this.snapshot());
|
|
@@ -1811,7 +1826,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1811
1826
|
json(response, 404, { error: "no such track" });
|
|
1812
1827
|
return;
|
|
1813
1828
|
}
|
|
1814
|
-
watch(request, response, "media", engine.snapshot().tracks[index]?.title ?? file);
|
|
1829
|
+
watch(request, response, "media", engine.snapshot().tracks?.[index]?.title ?? file);
|
|
1815
1830
|
// A browser asks for every track here, and a matroska or an avi handed
|
|
1816
1831
|
// to it raw is bytes it cannot play. Seeking is what this route is for
|
|
1817
1832
|
// and transcoding gives it up, but an unseekable film beats a silent
|
|
@@ -1842,11 +1857,11 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1842
1857
|
return;
|
|
1843
1858
|
}
|
|
1844
1859
|
const current = engine.snapshot();
|
|
1845
|
-
if (current.
|
|
1860
|
+
if (current.trackCount === 0) {
|
|
1846
1861
|
json(response, 404, { error: "nothing is playing" });
|
|
1847
1862
|
return;
|
|
1848
1863
|
}
|
|
1849
|
-
watch(request, response, "stream", current.tracks[current.index]?.title ?? "live");
|
|
1864
|
+
watch(request, response, "stream", current.tracks?.[current.index]?.title ?? "live");
|
|
1850
1865
|
liveAudio(request, response, engine, options.ffmpeg ?? ["ffmpeg"]);
|
|
1851
1866
|
return;
|
|
1852
1867
|
}
|
|
@@ -1862,7 +1877,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
1862
1877
|
json(response, 403, { error: "media streaming is off" });
|
|
1863
1878
|
return;
|
|
1864
1879
|
}
|
|
1865
|
-
watch(request, response, "stream", engine.snapshot().tracks[index]?.title ?? source);
|
|
1880
|
+
watch(request, response, "stream", engine.snapshot().tracks?.[index]?.title ?? source);
|
|
1866
1881
|
transcode(request, response, source, options.ffmpeg ?? ["ffmpeg"]);
|
|
1867
1882
|
return;
|
|
1868
1883
|
}
|
|
@@ -2681,7 +2696,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
2681
2696
|
},
|
|
2682
2697
|
nowPlaying: () => {
|
|
2683
2698
|
const snapshot = engine.snapshot();
|
|
2684
|
-
return snapshot.tracks[snapshot.index]?.title ?? "";
|
|
2699
|
+
return snapshot.tracks?.[snapshot.index]?.title ?? "";
|
|
2685
2700
|
},
|
|
2686
2701
|
onConfig: (remote) => {
|
|
2687
2702
|
const next = applyRemoteConfig(paywallConfig, (remote as { x402?: unknown })?.x402);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./index-
|
|
1
|
+
import{t as e}from"./index-CRfW9BIP.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-Bug6MFON.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-DW1kEon1.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 x(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}}var j=.14,M=.02;function oe(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 se(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 ce(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 le(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,y=()=>{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.`},b=()=>(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=y(),n.album.textContent=b();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===``,ue(),n.glyphs.textContent=d.map(M).join(``);let[ee,x]=m()?o.levels:h.levels();n.levels.textContent=`L${`▮`.repeat(Math.round(ee*6)).padEnd(6,`·`)} R${`▮`.repeat(Math.round(x*6)).padEnd(6,`·`)}`}let R=``;function ue(){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=oe(24,e.length)),d=ce(d,se(e,p)),f=N(f,d))}if(s){let e=getComputedStyle(document.documentElement);le(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:y(),album:b(),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=ee(Array.from(e.files??[]));if(t.length===0){l=`Nothing playable in that selection.`,L();return}x(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()=>{if(s=`connecting`,L(),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??[],fe(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(pe(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,de=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.`,de(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`},fe=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,pe(e.ownerId,e.name)),n.recentList.append(t)}},J=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}},pe=(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),J())}catch{}finally{n.disabled=!1}})()}),n},me=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},he=()=>`serviceWorker`in navigator&&`PushManager`in window&&`Notification`in window,ge=async()=>{if(!he())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:me(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}},_e=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`}},ve=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=he()&&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 ge();n.notifyWeb.checked=e,await Y({wantsWeb:e});return}await _e(),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?(ve(),J()):(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};
|