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.
Files changed (65) hide show
  1. package/README.md +225 -0
  2. package/bin/nixamp.mjs +4 -1
  3. package/dist/accounts.d.ts +54 -0
  4. package/dist/accounts.js +160 -0
  5. package/dist/admin.d.ts +47 -0
  6. package/dist/admin.js +209 -0
  7. package/dist/broadcast.d.ts +96 -0
  8. package/dist/broadcast.js +193 -0
  9. package/dist/channels.d.ts +94 -0
  10. package/dist/channels.js +235 -0
  11. package/dist/connections.d.ts +72 -0
  12. package/dist/connections.js +128 -0
  13. package/dist/daemon.d.ts +39 -0
  14. package/dist/daemon.js +170 -0
  15. package/dist/directory.d.ts +63 -0
  16. package/dist/directory.js +111 -0
  17. package/dist/ingest.d.ts +80 -0
  18. package/dist/ingest.js +252 -0
  19. package/dist/main.js +103 -6
  20. package/dist/manage.js +30 -7
  21. package/dist/owner.d.ts +53 -0
  22. package/dist/owner.js +96 -0
  23. package/dist/paywall.d.ts +60 -0
  24. package/dist/paywall.js +162 -0
  25. package/dist/playlist.d.ts +16 -0
  26. package/dist/playlist.js +57 -2
  27. package/dist/publish.d.ts +36 -0
  28. package/dist/publish.js +90 -0
  29. package/dist/rtmp-in.d.ts +22 -0
  30. package/dist/rtmp-in.js +79 -0
  31. package/dist/server.d.ts +117 -4
  32. package/dist/server.js +923 -24
  33. package/dist/session.d.ts +29 -0
  34. package/dist/session.js +184 -0
  35. package/dist/share.d.ts +74 -0
  36. package/dist/share.js +172 -0
  37. package/dist/sources.d.ts +37 -0
  38. package/dist/sources.js +125 -0
  39. package/package.json +5 -2
  40. package/src/accounts.ts +193 -0
  41. package/src/admin.ts +243 -0
  42. package/src/broadcast.ts +264 -0
  43. package/src/channels.ts +281 -0
  44. package/src/connections.ts +158 -0
  45. package/src/daemon.ts +193 -0
  46. package/src/directory.ts +135 -0
  47. package/src/ingest.ts +297 -0
  48. package/src/main.ts +107 -6
  49. package/src/manage.ts +35 -7
  50. package/src/owner.ts +113 -0
  51. package/src/paywall.ts +198 -0
  52. package/src/playlist.ts +68 -2
  53. package/src/publish.ts +101 -0
  54. package/src/rtmp-in.ts +90 -0
  55. package/src/server.ts +1087 -23
  56. package/src/session.ts +209 -0
  57. package/src/share.ts +193 -0
  58. package/src/sources.ts +136 -0
  59. package/src/types/auth-system.d.ts +77 -0
  60. package/web/dist/assets/{index-BGKWWaIx.css → index-0wAv50Ay.css} +1 -1
  61. package/web/dist/assets/index-WYJ6R4uF.js +1 -0
  62. package/web/dist/index.html +37 -6
  63. package/web/dist/install.ps1 +214 -0
  64. package/web/dist/sw.js +3 -3
  65. 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
+ }
@@ -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 readonly tracks;
53
- private readonly root;
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[];