nixamp 0.1.0 → 0.3.0
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/README.md +225 -0
- package/bin/nixamp.mjs +4 -1
- package/dist/accounts.d.ts +54 -0
- package/dist/accounts.js +160 -0
- package/dist/admin.d.ts +47 -0
- package/dist/admin.js +209 -0
- package/dist/broadcast.d.ts +96 -0
- package/dist/broadcast.js +193 -0
- package/dist/channels.d.ts +94 -0
- package/dist/channels.js +235 -0
- package/dist/connections.d.ts +72 -0
- package/dist/connections.js +128 -0
- package/dist/daemon.d.ts +39 -0
- package/dist/daemon.js +170 -0
- package/dist/directory.d.ts +63 -0
- package/dist/directory.js +111 -0
- package/dist/ingest.d.ts +80 -0
- package/dist/ingest.js +252 -0
- package/dist/main.js +103 -6
- package/dist/manage.js +30 -7
- package/dist/owner.d.ts +53 -0
- package/dist/owner.js +96 -0
- package/dist/paywall.d.ts +60 -0
- package/dist/paywall.js +162 -0
- package/dist/playlist.d.ts +16 -0
- package/dist/playlist.js +57 -2
- package/dist/publish.d.ts +36 -0
- package/dist/publish.js +90 -0
- package/dist/rtmp-in.d.ts +22 -0
- package/dist/rtmp-in.js +79 -0
- package/dist/server.d.ts +117 -4
- package/dist/server.js +923 -24
- package/dist/session.d.ts +29 -0
- package/dist/session.js +184 -0
- package/dist/share.d.ts +74 -0
- package/dist/share.js +172 -0
- package/dist/sources.d.ts +37 -0
- package/dist/sources.js +125 -0
- package/package.json +5 -2
- package/src/accounts.ts +193 -0
- package/src/admin.ts +243 -0
- package/src/broadcast.ts +264 -0
- package/src/channels.ts +281 -0
- package/src/connections.ts +158 -0
- package/src/daemon.ts +193 -0
- package/src/directory.ts +135 -0
- package/src/ingest.ts +297 -0
- package/src/main.ts +107 -6
- package/src/manage.ts +35 -7
- package/src/owner.ts +113 -0
- package/src/paywall.ts +198 -0
- package/src/playlist.ts +68 -2
- package/src/publish.ts +101 -0
- package/src/rtmp-in.ts +90 -0
- package/src/server.ts +1087 -23
- package/src/session.ts +209 -0
- package/src/share.ts +193 -0
- package/src/sources.ts +136 -0
- package/src/types/auth-system.d.ts +77 -0
- package/web/dist/assets/{index-BGKWWaIx.css → index-0wAv50Ay.css} +1 -1
- package/web/dist/assets/index-WYJ6R4uF.js +1 -0
- package/web/dist/index.html +37 -6
- package/web/dist/install.ps1 +214 -0
- package/web/dist/sw.js +3 -3
- package/web/dist/assets/index-Dhja5wxB.js +0 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Channels } from "./channels.ts";
|
|
2
|
+
export interface RtmpSlot {
|
|
3
|
+
port: number;
|
|
4
|
+
/** The channel a publisher on this port lands on. */
|
|
5
|
+
id: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Arm a listener per slot, and arm it again after each publisher leaves --
|
|
9
|
+
* otherwise a broadcaster who reconnects finds nothing listening.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RtmpListeners {
|
|
12
|
+
private readonly channels;
|
|
13
|
+
private readonly ffmpeg;
|
|
14
|
+
private readonly key;
|
|
15
|
+
private readonly running;
|
|
16
|
+
private stopped;
|
|
17
|
+
constructor(channels: Channels, ffmpeg: string[], key: string);
|
|
18
|
+
listen(slots: RtmpSlot[]): void;
|
|
19
|
+
private arm;
|
|
20
|
+
private done;
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
package/dist/rtmp-in.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTMP publishers, several at once.
|
|
3
|
+
*
|
|
4
|
+
* ffmpeg's RTMP listener serves one connection and exits, so N simultaneous
|
|
5
|
+
* publishers means N listeners on N ports. That is the honest cost of using
|
|
6
|
+
* ffmpeg as the RTMP server rather than implementing the protocol, and it buys
|
|
7
|
+
* a broadcaster that every phone and desktop app already speaks.
|
|
8
|
+
*
|
|
9
|
+
* Each listener decodes to MP3 itself, so its channel fans those bytes out
|
|
10
|
+
* without decoding them a second time.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
/**
|
|
14
|
+
* Arm a listener per slot, and arm it again after each publisher leaves --
|
|
15
|
+
* otherwise a broadcaster who reconnects finds nothing listening.
|
|
16
|
+
*/
|
|
17
|
+
export class RtmpListeners {
|
|
18
|
+
channels;
|
|
19
|
+
ffmpeg;
|
|
20
|
+
key;
|
|
21
|
+
running = new Map();
|
|
22
|
+
stopped = false;
|
|
23
|
+
constructor(channels, ffmpeg, key) {
|
|
24
|
+
this.channels = channels;
|
|
25
|
+
this.ffmpeg = ffmpeg;
|
|
26
|
+
this.key = key;
|
|
27
|
+
}
|
|
28
|
+
listen(slots) {
|
|
29
|
+
for (const slot of slots)
|
|
30
|
+
this.arm(slot);
|
|
31
|
+
}
|
|
32
|
+
arm(slot) {
|
|
33
|
+
if (this.stopped || this.running.has(slot.port))
|
|
34
|
+
return;
|
|
35
|
+
const [command, ...prefix] = this.ffmpeg;
|
|
36
|
+
const child = spawn(command, [
|
|
37
|
+
...prefix,
|
|
38
|
+
"-hide_banner",
|
|
39
|
+
"-loglevel", "error",
|
|
40
|
+
"-rtmp_listen", "1",
|
|
41
|
+
// Wait indefinitely: a stream that starts tomorrow is still the stream.
|
|
42
|
+
"-timeout", "-1",
|
|
43
|
+
"-f", "flv",
|
|
44
|
+
"-i", `rtmp://0.0.0.0:${slot.port}/live/${this.key}`,
|
|
45
|
+
"-vn",
|
|
46
|
+
"-c:a", "libmp3lame",
|
|
47
|
+
"-b:a", "192k",
|
|
48
|
+
"-y",
|
|
49
|
+
"-f", "mp3",
|
|
50
|
+
"pipe:1",
|
|
51
|
+
], { stdio: ["ignore", "pipe", "pipe"] });
|
|
52
|
+
this.running.set(slot.port, child);
|
|
53
|
+
// The first bytes are the only honest signal that a publisher turned up:
|
|
54
|
+
// ffmpeg does not announce a connect, and a healthy stream says nothing at
|
|
55
|
+
// -loglevel error.
|
|
56
|
+
let channel = null;
|
|
57
|
+
child.stdout?.on("data", (chunk) => {
|
|
58
|
+
channel ??= this.channels.attach(slot.id, "an RTMP publisher", "flv", "rtmp");
|
|
59
|
+
channel?.feed(chunk);
|
|
60
|
+
});
|
|
61
|
+
child.stdout?.on("error", () => child.kill("SIGKILL"));
|
|
62
|
+
child.on("error", () => this.done(slot, child, channel));
|
|
63
|
+
child.on("close", () => this.done(slot, child, channel));
|
|
64
|
+
}
|
|
65
|
+
done(slot, child, channel) {
|
|
66
|
+
if (this.running.get(slot.port) !== child)
|
|
67
|
+
return;
|
|
68
|
+
this.running.delete(slot.port);
|
|
69
|
+
channel?.close();
|
|
70
|
+
if (!this.stopped)
|
|
71
|
+
this.arm(slot);
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
this.stopped = true;
|
|
75
|
+
for (const child of this.running.values())
|
|
76
|
+
child.kill("SIGKILL");
|
|
77
|
+
this.running.clear();
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
|
+
import { Connections } from "./connections.ts";
|
|
3
|
+
import { Broadcaster, type Destination, type EncoderSettings } from "./broadcast.ts";
|
|
4
|
+
import { Ingest } from "./ingest.ts";
|
|
5
|
+
import { Channels } from "./channels.ts";
|
|
6
|
+
import { Accounts } from "./accounts.ts";
|
|
7
|
+
import { Owner } from "./owner.ts";
|
|
8
|
+
import { Directory } from "./directory.ts";
|
|
2
9
|
import { type Tools, type Track } from "./audio.ts";
|
|
3
10
|
import { type Command, type RemoteTrack, type Snapshot } from "./protocol.ts";
|
|
4
11
|
export declare const SERVE_BAND_COUNT = 24;
|
|
@@ -11,6 +18,57 @@ export interface ServeOptions {
|
|
|
11
18
|
web: string | null;
|
|
12
19
|
/** Stream the library's bytes to remotes. Off keeps the audio on this box. */
|
|
13
20
|
media: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Require the key from the share link. Off serves to anyone who can reach the
|
|
23
|
+
* port, which is what the public deployment wants and no private one does.
|
|
24
|
+
*/
|
|
25
|
+
key: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Ask the local firewall to let the port through, and put it back on the way
|
|
28
|
+
* out. Off by default because it changes the machine, not just this process.
|
|
29
|
+
*/
|
|
30
|
+
openPort: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Print one JSON line once listening. `nixamp daemon start` reads it rather
|
|
33
|
+
* than sleeping and hoping, so a daemon that failed to bind is reported as
|
|
34
|
+
* failed instead of started.
|
|
35
|
+
*/
|
|
36
|
+
announce: boolean;
|
|
37
|
+
/** Host the public directory. Only the deployment behind nixamp.com does. */
|
|
38
|
+
directory: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* List this stream at nixamp.com/directory. "ask" prompts, and is the
|
|
41
|
+
* default: publishing an address without being asked is not something a
|
|
42
|
+
* player gets to decide for you.
|
|
43
|
+
*/
|
|
44
|
+
publish: "ask" | "yes" | "no";
|
|
45
|
+
/** What to call it in the list. Defaults to this machine's hostname. */
|
|
46
|
+
name: string;
|
|
47
|
+
/**
|
|
48
|
+
* Charge for listening once the stream is busy. Off unless asked for, and
|
|
49
|
+
* useless without somewhere to pay: see NIXAMP_PAY_TO.
|
|
50
|
+
*/
|
|
51
|
+
x402: boolean;
|
|
52
|
+
/** The account id that may administer this server, if not the signed-in one. */
|
|
53
|
+
owner: string;
|
|
54
|
+
/** Accept a live stream from a phone or a desktop, over HTTP. */
|
|
55
|
+
ingest: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Also listen for RTMP publishers on this port, which is what OBS, Larix and
|
|
58
|
+
* anything else native speaks. 0 means do not.
|
|
59
|
+
*/
|
|
60
|
+
rtmpIn: number;
|
|
61
|
+
/**
|
|
62
|
+
* How many RTMP publishers may be live at once. ffmpeg's listener serves one
|
|
63
|
+
* connection per process, so this is a port and a process each: 1935, 1936,
|
|
64
|
+
* and so on. HTTP publishers are not limited by this.
|
|
65
|
+
*/
|
|
66
|
+
rtmpStreams: number;
|
|
67
|
+
/**
|
|
68
|
+
* RTMP destinations, as `name=rtmp://host/app/key` or `youtube=key` for one
|
|
69
|
+
* of the presets. Repeatable.
|
|
70
|
+
*/
|
|
71
|
+
rtmp: string[];
|
|
14
72
|
}
|
|
15
73
|
/**
|
|
16
74
|
* Flags are parsed by hand: three of them do not justify a dependency, and the
|
|
@@ -40,6 +98,12 @@ export interface Engine {
|
|
|
40
98
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
41
99
|
/** Absolute path of a track, or undefined when the index is not one. */
|
|
42
100
|
trackPath(index: number): string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Play something else instead. Re-streaming is the whole reason the admin
|
|
103
|
+
* view exists: point a running server at a URL without restarting it and
|
|
104
|
+
* dropping every listener.
|
|
105
|
+
*/
|
|
106
|
+
replace(tracks: Track[], root: string): void;
|
|
43
107
|
stop(): void;
|
|
44
108
|
}
|
|
45
109
|
export declare function toRemoteTracks(tracks: Track[]): RemoteTrack[];
|
|
@@ -49,8 +113,8 @@ export declare function toRemoteTracks(tracks: Track[]): RemoteTrack[];
|
|
|
49
113
|
* its way past so a remote can draw the same spectrum the TUI would.
|
|
50
114
|
*/
|
|
51
115
|
export declare class PlayerEngine implements Engine {
|
|
52
|
-
private
|
|
53
|
-
private
|
|
116
|
+
private tracks;
|
|
117
|
+
private root;
|
|
54
118
|
/** Frames a second pushed to remotes. */
|
|
55
119
|
private readonly fps;
|
|
56
120
|
private readonly listeners;
|
|
@@ -78,6 +142,7 @@ export declare class PlayerEngine implements Engine {
|
|
|
78
142
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
79
143
|
private push;
|
|
80
144
|
stop(): void;
|
|
145
|
+
replace(tracks: Track[], root: string): void;
|
|
81
146
|
}
|
|
82
147
|
/** An engine with no library behind it, for the hosted PWA. */
|
|
83
148
|
export declare class EmptyEngine implements Engine {
|
|
@@ -87,12 +152,54 @@ export declare class EmptyEngine implements Engine {
|
|
|
87
152
|
command(): void;
|
|
88
153
|
subscribe(listener: (snapshot: Snapshot) => void): () => void;
|
|
89
154
|
trackPath(): undefined;
|
|
155
|
+
replace(): void;
|
|
90
156
|
stop(): void;
|
|
91
157
|
}
|
|
92
158
|
export interface HandlerOptions {
|
|
93
159
|
web: string | null;
|
|
94
160
|
media: boolean;
|
|
95
161
|
version: string;
|
|
162
|
+
/** The key from the share link, or null to serve to anyone who can connect. */
|
|
163
|
+
key?: string | null;
|
|
164
|
+
/**
|
|
165
|
+
* A second key that may listen but not drive. The public directory hands
|
|
166
|
+
* this one out: a link that lets a stranger pause your music is not a link
|
|
167
|
+
* you can publish.
|
|
168
|
+
*/
|
|
169
|
+
listenKey?: string | null;
|
|
170
|
+
/** How to run ffmpeg, for the sources a browser cannot play by itself. */
|
|
171
|
+
ffmpeg?: string[];
|
|
172
|
+
/** Who is listening, for the admin view. */
|
|
173
|
+
connections?: Connections;
|
|
174
|
+
/**
|
|
175
|
+
* How to turn a source into tracks, for re-streaming. Injected rather than
|
|
176
|
+
* imported so the handler stays a plain function of a request.
|
|
177
|
+
*/
|
|
178
|
+
load: (source: string) => Promise<Track[]>;
|
|
179
|
+
/**
|
|
180
|
+
* The public directory, on the instance that hosts one. Only nixamp.com
|
|
181
|
+
* passes this; a nixamp on your laptop is a publisher, not a registry.
|
|
182
|
+
*/
|
|
183
|
+
directory?: Directory;
|
|
184
|
+
/** Answers a request itself when listening has to be paid for. */
|
|
185
|
+
paywall?: (request: IncomingMessage, response: ServerResponse, path: string) => Promise<boolean>;
|
|
186
|
+
/** Live audio coming in from a phone or a desktop. */
|
|
187
|
+
ingest?: Ingest;
|
|
188
|
+
/** Several live streams at once, each with its own audience. */
|
|
189
|
+
channels?: Channels;
|
|
190
|
+
/** Live audio going out to RTMP. */
|
|
191
|
+
broadcaster?: Broadcaster;
|
|
192
|
+
/** Where a broadcast should send, and what it should look like. */
|
|
193
|
+
broadcast?: () => {
|
|
194
|
+
destinations: Destination[];
|
|
195
|
+
settings: EncoderSettings;
|
|
196
|
+
};
|
|
197
|
+
/** Accounts, on the instance that keeps them. Only nixamp.com passes this. */
|
|
198
|
+
accounts?: Accounts;
|
|
199
|
+
/** True when this instance is reached over https, for the cookie's Secure. */
|
|
200
|
+
secureCookies?: boolean;
|
|
201
|
+
/** Who may administer this server. */
|
|
202
|
+
owner?: Owner;
|
|
96
203
|
}
|
|
97
204
|
/**
|
|
98
205
|
* The whole HTTP surface, as a plain function of a request — so a test can
|
|
@@ -100,6 +207,12 @@ export interface HandlerOptions {
|
|
|
100
207
|
*/
|
|
101
208
|
export declare function createHandler(engine: Engine, options: HandlerOptions): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
102
209
|
export declare function createServer(engine: Engine, options: HandlerOptions): Server;
|
|
103
|
-
/** Where a remote on another device should point its browser. */
|
|
104
|
-
export declare function addressesFor(host: string, port: number): string[];
|
|
105
210
|
export declare function serve(argv: string[], version?: string): Promise<void>;
|
|
211
|
+
/**
|
|
212
|
+
* `--rtmp youtube=<key>` or `--rtmp name=rtmp://host/app/key`.
|
|
213
|
+
*
|
|
214
|
+
* A key is a password, so it is taken from the command line or the environment
|
|
215
|
+
* and never from a request: a client that could name its own destination could
|
|
216
|
+
* point your broadcast at itself.
|
|
217
|
+
*/
|
|
218
|
+
export declare function parseDestinations(specs: string[]): Destination[];
|