nixamp 0.5.10 → 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/attach.js +5 -1
- package/dist/main.js +3 -0
- package/dist/protocol.d.ts +34 -2
- package/dist/protocol.js +11 -0
- package/dist/server.d.ts +31 -2
- package/dist/server.js +63 -16
- package/dist/share.d.ts +1 -1
- package/dist/share.js +3 -3
- package/package.json +1 -1
- package/src/attach.ts +5 -1
- package/src/main.ts +3 -0
- package/src/protocol.ts +37 -2
- package/src/server.ts +84 -17
- package/src/share.ts +3 -2
- package/web/dist/assets/{hls-3VKVEQE3-Cj5Lwh7A.js → hls-3VKVEQE3-Ch_vCVqk.js} +1 -1
- package/web/dist/assets/index-B816i-Dk.js +1 -0
- package/web/dist/assets/{mpegts-CKFUPaK9.js → mpegts-DS9rVyQe.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-Bj_ZfP5s.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-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/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/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
|
@@ -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
|
|
@@ -111,7 +123,8 @@ export declare function parseRange(header: string | undefined, size: number): By
|
|
|
111
123
|
export declare function safeJoin(rootDir: string, urlPath: string): string | null;
|
|
112
124
|
/** What the HTTP layer needs from a player. Tests hand it a fake. */
|
|
113
125
|
export interface Engine {
|
|
114
|
-
|
|
126
|
+
/** `withTracks` false leaves the library out, for a frame that is only motion. */
|
|
127
|
+
snapshot(withTracks?: boolean): Snapshot;
|
|
115
128
|
command(command: Command): void;
|
|
116
129
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
117
130
|
/** Absolute path of a track, or undefined when the index is not one. */
|
|
@@ -161,7 +174,11 @@ export declare class PlayerEngine implements Engine {
|
|
|
161
174
|
fps?: number);
|
|
162
175
|
private readonly silent;
|
|
163
176
|
private consume;
|
|
164
|
-
|
|
177
|
+
/**
|
|
178
|
+
* The current state. `withTracks` carries the library, which is worth half a
|
|
179
|
+
* megabyte on a real one and is only news when it has changed.
|
|
180
|
+
*/
|
|
181
|
+
snapshot(withTracks?: boolean): Snapshot;
|
|
165
182
|
trackPath(index: number): string | undefined;
|
|
166
183
|
command(command: Command): void;
|
|
167
184
|
private clamp;
|
|
@@ -169,6 +186,13 @@ export declare class PlayerEngine implements Engine {
|
|
|
169
186
|
private start;
|
|
170
187
|
private halt;
|
|
171
188
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
189
|
+
/**
|
|
190
|
+
* Send the state to everyone watching.
|
|
191
|
+
*
|
|
192
|
+
* The library goes only when `listChanged` says it has, which is what turned
|
|
193
|
+
* five megabytes a second into a few kilobytes: an analyser tick has nothing
|
|
194
|
+
* to say about the track list, and it fires twelve times a second.
|
|
195
|
+
*/
|
|
172
196
|
private push;
|
|
173
197
|
stop(): void;
|
|
174
198
|
replace(tracks: Track[], root: string): void;
|
|
@@ -238,6 +262,11 @@ export interface HandlerOptions {
|
|
|
238
262
|
signIn?: SignIn;
|
|
239
263
|
/** True when this instance is reached over https, for the cookie's Secure. */
|
|
240
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
|
+
};
|
|
241
270
|
/**
|
|
242
271
|
* True when a proxy sits in front, so `x-forwarded-for` names the caller.
|
|
243
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 = {
|
|
@@ -348,10 +363,15 @@ export class PlayerEngine {
|
|
|
348
363
|
this.pending = joined.subarray(at);
|
|
349
364
|
this.dirty = true;
|
|
350
365
|
}
|
|
351
|
-
|
|
366
|
+
/**
|
|
367
|
+
* The current state. `withTracks` carries the library, which is worth half a
|
|
368
|
+
* megabyte on a real one and is only news when it has changed.
|
|
369
|
+
*/
|
|
370
|
+
snapshot(withTracks = true) {
|
|
352
371
|
return {
|
|
353
372
|
revision: this.revision,
|
|
354
|
-
tracks: toRemoteTracks(this.tracks),
|
|
373
|
+
...(withTracks ? { tracks: toRemoteTracks(this.tracks) } : {}),
|
|
374
|
+
trackCount: this.tracks.length,
|
|
355
375
|
index: this.state.index,
|
|
356
376
|
playing: this.state.playing,
|
|
357
377
|
position: this.state.position,
|
|
@@ -445,11 +465,18 @@ export class PlayerEngine {
|
|
|
445
465
|
}
|
|
446
466
|
};
|
|
447
467
|
}
|
|
448
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Send the state to everyone watching.
|
|
470
|
+
*
|
|
471
|
+
* The library goes only when `listChanged` says it has, which is what turned
|
|
472
|
+
* five megabytes a second into a few kilobytes: an analyser tick has nothing
|
|
473
|
+
* to say about the track list, and it fires twelve times a second.
|
|
474
|
+
*/
|
|
475
|
+
push(listChanged = false) {
|
|
449
476
|
this.revision++;
|
|
450
477
|
if (this.listeners.size === 0)
|
|
451
478
|
return;
|
|
452
|
-
const snapshot = this.snapshot();
|
|
479
|
+
const snapshot = this.snapshot(listChanged);
|
|
453
480
|
for (const listener of this.listeners)
|
|
454
481
|
listener(snapshot);
|
|
455
482
|
}
|
|
@@ -468,7 +495,7 @@ export class PlayerEngine {
|
|
|
468
495
|
this.state.index = 0;
|
|
469
496
|
this.state.position = 0;
|
|
470
497
|
this.state.note = "";
|
|
471
|
-
this.push();
|
|
498
|
+
this.push(true);
|
|
472
499
|
}
|
|
473
500
|
retag(tracks, root) {
|
|
474
501
|
// Dropped rather than applied if the library moved underneath: somebody
|
|
@@ -480,8 +507,9 @@ export class PlayerEngine {
|
|
|
480
507
|
return;
|
|
481
508
|
this.tracks = tracks;
|
|
482
509
|
// 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.
|
|
510
|
+
// say, and every remote finds out because a snapshot goes out -- carrying
|
|
511
|
+
// the list, since the titles are the whole point of this one.
|
|
512
|
+
this.push(true);
|
|
485
513
|
}
|
|
486
514
|
}
|
|
487
515
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
@@ -1540,7 +1568,7 @@ export function createHandler(engine, options) {
|
|
|
1540
1568
|
json(response, 404, { error: "no such track" });
|
|
1541
1569
|
return;
|
|
1542
1570
|
}
|
|
1543
|
-
watch(request, response, "media", engine.snapshot().tracks[index]?.title ?? file);
|
|
1571
|
+
watch(request, response, "media", engine.snapshot().tracks?.[index]?.title ?? file);
|
|
1544
1572
|
// A browser asks for every track here, and a matroska or an avi handed
|
|
1545
1573
|
// to it raw is bytes it cannot play. Seeking is what this route is for
|
|
1546
1574
|
// and transcoding gives it up, but an unseekable film beats a silent
|
|
@@ -1572,11 +1600,11 @@ export function createHandler(engine, options) {
|
|
|
1572
1600
|
return;
|
|
1573
1601
|
}
|
|
1574
1602
|
const current = engine.snapshot();
|
|
1575
|
-
if (current.
|
|
1603
|
+
if (current.trackCount === 0) {
|
|
1576
1604
|
json(response, 404, { error: "nothing is playing" });
|
|
1577
1605
|
return;
|
|
1578
1606
|
}
|
|
1579
|
-
watch(request, response, "stream", current.tracks[current.index]?.title ?? "live");
|
|
1607
|
+
watch(request, response, "stream", current.tracks?.[current.index]?.title ?? "live");
|
|
1580
1608
|
liveAudio(request, response, engine, options.ffmpeg ?? ["ffmpeg"]);
|
|
1581
1609
|
return;
|
|
1582
1610
|
}
|
|
@@ -1591,7 +1619,7 @@ export function createHandler(engine, options) {
|
|
|
1591
1619
|
json(response, 403, { error: "media streaming is off" });
|
|
1592
1620
|
return;
|
|
1593
1621
|
}
|
|
1594
|
-
watch(request, response, "stream", engine.snapshot().tracks[index]?.title ?? source);
|
|
1622
|
+
watch(request, response, "stream", engine.snapshot().tracks?.[index]?.title ?? source);
|
|
1595
1623
|
transcode(request, response, source, options.ffmpeg ?? ["ffmpeg"]);
|
|
1596
1624
|
return;
|
|
1597
1625
|
}
|
|
@@ -1885,14 +1913,20 @@ function sendFile(request, response, file) {
|
|
|
1885
1913
|
}
|
|
1886
1914
|
export function createServer(engine, options) {
|
|
1887
1915
|
const handle = createHandler(engine, options);
|
|
1888
|
-
|
|
1916
|
+
const onRequest = (request, response) => {
|
|
1889
1917
|
handle(request, response).catch(() => {
|
|
1890
1918
|
if (!response.headersSent)
|
|
1891
1919
|
json(response, 500, { error: "server error" });
|
|
1892
1920
|
else
|
|
1893
1921
|
response.end();
|
|
1894
1922
|
});
|
|
1895
|
-
}
|
|
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);
|
|
1896
1930
|
}
|
|
1897
1931
|
export async function serve(argv, version = "0.1.0") {
|
|
1898
1932
|
const options = parseServeArgs(argv);
|
|
@@ -2099,6 +2133,18 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2099
2133
|
});
|
|
2100
2134
|
});
|
|
2101
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;
|
|
2102
2148
|
const server = createServer(engine, {
|
|
2103
2149
|
web,
|
|
2104
2150
|
media: options.media,
|
|
@@ -2114,6 +2160,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2114
2160
|
paywall,
|
|
2115
2161
|
ffmpeg: tools.ffmpeg,
|
|
2116
2162
|
ffprobe: tools.ffprobe,
|
|
2163
|
+
...(tls ? { tls } : {}),
|
|
2117
2164
|
load: (next) => loadSource(tools, next),
|
|
2118
2165
|
...(directory ? { directory } : {}),
|
|
2119
2166
|
...(follows ? { follows, vapidPublicKey } : {}),
|
|
@@ -2168,12 +2215,12 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2168
2215
|
// is and nothing local looks public, ask. What comes back is a fact about the
|
|
2169
2216
|
// router and not about this port -- the port still has to be forwarded -- so
|
|
2170
2217
|
// it is marked as a guess and everything that prints it says so.
|
|
2171
|
-
const localAddresses = reachableAddresses(options.host, port, options.publicUrl);
|
|
2218
|
+
const localAddresses = reachableAddresses(options.host, port, options.publicUrl, tls ? "https" : "http");
|
|
2172
2219
|
const guessedPublic = options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
|
|
2173
2220
|
? await lookupPublicIp()
|
|
2174
2221
|
: "";
|
|
2175
2222
|
const addresses = guessedPublic
|
|
2176
|
-
? reachableAddresses(options.host, port,
|
|
2223
|
+
? reachableAddresses(options.host, port, `${tls ? "https" : "http"}://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`, tls ? "https" : "http")
|
|
2177
2224
|
: localAddresses;
|
|
2178
2225
|
// Listening on every interface proves the socket is open here and nothing
|
|
2179
2226
|
// about the path between here and the phone.
|
|
@@ -2341,7 +2388,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2341
2388
|
},
|
|
2342
2389
|
nowPlaying: () => {
|
|
2343
2390
|
const snapshot = engine.snapshot();
|
|
2344
|
-
return snapshot.tracks[snapshot.index]?.title ?? "";
|
|
2391
|
+
return snapshot.tracks?.[snapshot.index]?.title ?? "";
|
|
2345
2392
|
},
|
|
2346
2393
|
onConfig: (remote) => {
|
|
2347
2394
|
const next = applyRemoteConfig(paywallConfig, remote?.x402);
|
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/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/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/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[] };
|