nixamp 0.2.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 (51) hide show
  1. package/README.md +171 -0
  2. package/dist/accounts.d.ts +54 -0
  3. package/dist/accounts.js +160 -0
  4. package/dist/broadcast.d.ts +96 -0
  5. package/dist/broadcast.js +193 -0
  6. package/dist/channels.d.ts +94 -0
  7. package/dist/channels.js +235 -0
  8. package/dist/connections.d.ts +6 -0
  9. package/dist/connections.js +13 -0
  10. package/dist/directory.d.ts +63 -0
  11. package/dist/directory.js +111 -0
  12. package/dist/ingest.d.ts +80 -0
  13. package/dist/ingest.js +252 -0
  14. package/dist/main.js +21 -0
  15. package/dist/manage.js +2 -1
  16. package/dist/owner.d.ts +53 -0
  17. package/dist/owner.js +96 -0
  18. package/dist/paywall.d.ts +60 -0
  19. package/dist/paywall.js +162 -0
  20. package/dist/publish.d.ts +36 -0
  21. package/dist/publish.js +90 -0
  22. package/dist/rtmp-in.d.ts +22 -0
  23. package/dist/rtmp-in.js +79 -0
  24. package/dist/server.d.ts +79 -0
  25. package/dist/server.js +609 -10
  26. package/dist/session.d.ts +29 -0
  27. package/dist/session.js +184 -0
  28. package/dist/share.d.ts +16 -0
  29. package/dist/share.js +19 -0
  30. package/package.json +5 -2
  31. package/src/accounts.ts +193 -0
  32. package/src/broadcast.ts +264 -0
  33. package/src/channels.ts +281 -0
  34. package/src/connections.ts +13 -0
  35. package/src/directory.ts +135 -0
  36. package/src/ingest.ts +297 -0
  37. package/src/main.ts +21 -0
  38. package/src/manage.ts +2 -1
  39. package/src/owner.ts +113 -0
  40. package/src/paywall.ts +198 -0
  41. package/src/publish.ts +101 -0
  42. package/src/rtmp-in.ts +90 -0
  43. package/src/server.ts +702 -10
  44. package/src/session.ts +209 -0
  45. package/src/share.ts +27 -0
  46. package/src/types/auth-system.d.ts +77 -0
  47. package/web/dist/assets/{index-BGKWWaIx.css → index-0wAv50Ay.css} +1 -1
  48. package/web/dist/assets/index-WYJ6R4uF.js +1 -0
  49. package/web/dist/index.html +37 -6
  50. package/web/dist/sw.js +3 -3
  51. package/web/dist/assets/index-Dhja5wxB.js +0 -1
@@ -0,0 +1,94 @@
1
+ import type { Readable } from "node:stream";
2
+ /** Somewhere for a channel's audio to go. A response, in practice. */
3
+ export interface Listener {
4
+ write(chunk: Buffer): boolean;
5
+ end(): void;
6
+ }
7
+ export interface ChannelInfo {
8
+ id: string;
9
+ /** What the publisher called itself. */
10
+ name: string;
11
+ /** The container it is sending, e.g. webm from a browser, flv over RTMP. */
12
+ format: string;
13
+ /** How it arrived. */
14
+ via: "http" | "rtmp";
15
+ startedAt: number;
16
+ bytes: number;
17
+ listeners: number;
18
+ }
19
+ /** A name that can sit in a URL and be read back in a list. */
20
+ export declare function cleanId(value: unknown, fallback?: string): string;
21
+ export interface ChannelOptions {
22
+ ffmpeg: string[];
23
+ onStart?: (info: ChannelInfo) => void;
24
+ onEnd?: (info: ChannelInfo) => void;
25
+ }
26
+ /**
27
+ * One live source, and its audience.
28
+ *
29
+ * Everything a listener is sent has been through ffmpeg, so a publisher cannot
30
+ * decide what bytes reach a browser by choosing what to send.
31
+ */
32
+ export declare class Channel {
33
+ readonly info: ChannelInfo;
34
+ private readonly options;
35
+ private readonly onGone;
36
+ readonly listeners: Set<Listener>;
37
+ private child;
38
+ private closing;
39
+ constructor(info: ChannelInfo, options: ChannelOptions, onGone: (id: string) => void);
40
+ start(format: string): void;
41
+ /** Feed the source. */
42
+ write(chunk: Buffer): boolean;
43
+ pump(body: Readable): Promise<void>;
44
+ /**
45
+ * Audio that is already in its final form, from a source we did not spawn.
46
+ * The bytes still only reach a listener after something decoded them; it was
47
+ * simply a different process that did it.
48
+ */
49
+ feed(chunk: Buffer): void;
50
+ /** Write to everyone, and drop anybody whose socket has gone. */
51
+ private send;
52
+ listen(listener: Listener): () => void;
53
+ close(): void;
54
+ }
55
+ /**
56
+ * Every channel currently live.
57
+ *
58
+ * A channel exists while somebody is publishing to it and disappears when they
59
+ * stop, so the list is what is actually on rather than what was once
60
+ * configured.
61
+ */
62
+ export declare class Channels {
63
+ private readonly options;
64
+ private readonly open;
65
+ constructor(options: ChannelOptions);
66
+ list(): ChannelInfo[];
67
+ get count(): number;
68
+ /** Total listeners across every channel. */
69
+ get listeners(): number;
70
+ has(id: string): boolean;
71
+ /**
72
+ * Claim a channel and start decoding into it. Null when that channel is
73
+ * already being published to: two publishers on one channel would be two
74
+ * songs at once, which is never what anybody meant. Publishing to a
75
+ * *different* channel is exactly what this class exists for.
76
+ */
77
+ publish(id: string, name: string, format: string, via: ChannelInfo["via"]): Channel | null;
78
+ /** Attach a listener, or null when nothing is playing on that channel. */
79
+ listen(id: string, listener: Listener): (() => void) | null;
80
+ /** Feed a channel that already exists, for a publisher sending chunks. */
81
+ writeTo(id: string, chunk: Buffer): boolean;
82
+ /**
83
+ * A channel fed by audio somebody else is already decoding.
84
+ *
85
+ * An RTMP listener is an ffmpeg with a publisher on one end, and it produces
86
+ * MP3 on its own. Spawning a second ffmpeg to decode what the first one just
87
+ * decoded would double the work to arrive at the same bytes.
88
+ */
89
+ attach(id: string, name: string, format: string, via: ChannelInfo["via"]): Channel | null;
90
+ stop(id: string): boolean;
91
+ stopAll(): void;
92
+ }
93
+ /** A channel id nobody chose, for a publisher that did not name one. */
94
+ export declare function generatedId(): string;
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Several streams at once.
3
+ *
4
+ * A channel is one live source and everybody listening to it. Two or three
5
+ * devices can publish at the same time -- a phone, a desktop, a second window
6
+ * -- and each has its own audience, so a listener picks which one to hear.
7
+ *
8
+ * The fan-out is the point. One ffmpeg decodes a publisher's bytes once, and
9
+ * the MP3 it produces is written to every listener attached to that channel.
10
+ * A decode per listener would cost a CPU core each and, for a live stream,
11
+ * would not even agree with itself about what "now" is.
12
+ *
13
+ * A listener joining halfway through gets the stream from that moment, which is
14
+ * what live means. MP3 frames are self-describing, so a player finds the next
15
+ * frame boundary and carries on; there is nothing to catch up on.
16
+ */
17
+ import { spawn } from "node:child_process";
18
+ import { randomBytes } from "node:crypto";
19
+ /** A name that can sit in a URL and be read back in a list. */
20
+ export function cleanId(value, fallback = "main") {
21
+ if (typeof value !== "string")
22
+ return fallback;
23
+ const id = value.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
24
+ return id.slice(0, 40) || fallback;
25
+ }
26
+ /**
27
+ * One live source, and its audience.
28
+ *
29
+ * Everything a listener is sent has been through ffmpeg, so a publisher cannot
30
+ * decide what bytes reach a browser by choosing what to send.
31
+ */
32
+ export class Channel {
33
+ info;
34
+ options;
35
+ onGone;
36
+ listeners = new Set();
37
+ child = null;
38
+ closing = false;
39
+ constructor(info, options, onGone) {
40
+ this.info = info;
41
+ this.options = options;
42
+ this.onGone = onGone;
43
+ }
44
+ start(format) {
45
+ const [command, ...prefix] = this.options.ffmpeg;
46
+ const child = spawn(command, [
47
+ ...prefix,
48
+ "-hide_banner",
49
+ "-loglevel", "error",
50
+ // Stated, because ffmpeg mis-probes a live unseekable pipe: it reads a
51
+ // few kilobytes, guesses, and guesses wrong.
52
+ "-f", format,
53
+ "-i", "pipe:0",
54
+ "-vn",
55
+ "-c:a", "libmp3lame",
56
+ "-b:a", "192k",
57
+ "-f", "mp3",
58
+ "pipe:1",
59
+ ], { stdio: ["pipe", "pipe", "pipe"] });
60
+ child.stdout?.on("data", (chunk) => {
61
+ this.info.bytes += chunk.byteLength;
62
+ this.send(chunk);
63
+ });
64
+ // A publisher that hangs up mid-write breaks the pipe, and an unhandled
65
+ // EPIPE takes the whole server with it.
66
+ child.stdin?.on("error", () => this.close());
67
+ child.stdout?.on("error", () => this.close());
68
+ child.on("error", () => this.close());
69
+ child.on("close", () => this.close());
70
+ this.child = child;
71
+ this.options.onStart?.(this.info);
72
+ }
73
+ /** Feed the source. */
74
+ write(chunk) {
75
+ return this.child?.stdin?.write(chunk) ?? false;
76
+ }
77
+ async pump(body) {
78
+ for await (const chunk of body) {
79
+ if (this.closing)
80
+ return;
81
+ if (!this.write(chunk)) {
82
+ await new Promise((done) => this.child?.stdin?.once("drain", done) ?? done(null));
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Audio that is already in its final form, from a source we did not spawn.
88
+ * The bytes still only reach a listener after something decoded them; it was
89
+ * simply a different process that did it.
90
+ */
91
+ feed(chunk) {
92
+ this.info.bytes += chunk.byteLength;
93
+ this.send(chunk);
94
+ }
95
+ /** Write to everyone, and drop anybody whose socket has gone. */
96
+ send(chunk) {
97
+ for (const listener of this.listeners) {
98
+ try {
99
+ listener.write(chunk);
100
+ }
101
+ catch {
102
+ // One listener's broken socket is not the channel's problem.
103
+ this.listeners.delete(listener);
104
+ }
105
+ }
106
+ this.info.listeners = this.listeners.size;
107
+ }
108
+ listen(listener) {
109
+ this.listeners.add(listener);
110
+ this.info.listeners = this.listeners.size;
111
+ return () => {
112
+ this.listeners.delete(listener);
113
+ this.info.listeners = this.listeners.size;
114
+ };
115
+ }
116
+ close() {
117
+ if (this.closing)
118
+ return;
119
+ this.closing = true;
120
+ const child = this.child;
121
+ this.child = null;
122
+ try {
123
+ child?.stdin?.end();
124
+ }
125
+ catch {
126
+ // Already broken, which is usually why we are here.
127
+ }
128
+ child?.kill("SIGKILL");
129
+ // Listeners are ended rather than left hanging on a stream that stopped.
130
+ for (const listener of this.listeners) {
131
+ try {
132
+ listener.end();
133
+ }
134
+ catch {
135
+ // Gone already.
136
+ }
137
+ }
138
+ this.listeners.clear();
139
+ this.info.listeners = 0;
140
+ this.options.onEnd?.(this.info);
141
+ this.onGone(this.info.id);
142
+ }
143
+ }
144
+ /**
145
+ * Every channel currently live.
146
+ *
147
+ * A channel exists while somebody is publishing to it and disappears when they
148
+ * stop, so the list is what is actually on rather than what was once
149
+ * configured.
150
+ */
151
+ export class Channels {
152
+ options;
153
+ open = new Map();
154
+ constructor(options) {
155
+ this.options = options;
156
+ }
157
+ list() {
158
+ return [...this.open.values()]
159
+ .map((channel) => channel.info)
160
+ .sort((a, b) => a.startedAt - b.startedAt);
161
+ }
162
+ get count() {
163
+ return this.open.size;
164
+ }
165
+ /** Total listeners across every channel. */
166
+ get listeners() {
167
+ let total = 0;
168
+ for (const channel of this.open.values())
169
+ total += channel.listeners.size;
170
+ return total;
171
+ }
172
+ has(id) {
173
+ return this.open.has(id);
174
+ }
175
+ /**
176
+ * Claim a channel and start decoding into it. Null when that channel is
177
+ * already being published to: two publishers on one channel would be two
178
+ * songs at once, which is never what anybody meant. Publishing to a
179
+ * *different* channel is exactly what this class exists for.
180
+ */
181
+ publish(id, name, format, via) {
182
+ if (this.open.has(id))
183
+ return null;
184
+ const channel = new Channel({
185
+ id,
186
+ name: name || "a device",
187
+ format,
188
+ via,
189
+ startedAt: Date.now(),
190
+ bytes: 0,
191
+ listeners: 0,
192
+ }, this.options, (gone) => this.open.delete(gone));
193
+ this.open.set(id, channel);
194
+ channel.start(format);
195
+ return channel;
196
+ }
197
+ /** Attach a listener, or null when nothing is playing on that channel. */
198
+ listen(id, listener) {
199
+ const channel = this.open.get(id);
200
+ return channel ? channel.listen(listener) : null;
201
+ }
202
+ /** Feed a channel that already exists, for a publisher sending chunks. */
203
+ writeTo(id, chunk) {
204
+ return this.open.get(id)?.write(chunk) ?? false;
205
+ }
206
+ /**
207
+ * A channel fed by audio somebody else is already decoding.
208
+ *
209
+ * An RTMP listener is an ffmpeg with a publisher on one end, and it produces
210
+ * MP3 on its own. Spawning a second ffmpeg to decode what the first one just
211
+ * decoded would double the work to arrive at the same bytes.
212
+ */
213
+ attach(id, name, format, via) {
214
+ if (this.open.has(id))
215
+ return null;
216
+ const channel = new Channel({ id, name: name || "a device", format, via, startedAt: Date.now(), bytes: 0, listeners: 0 }, this.options, (gone) => this.open.delete(gone));
217
+ this.open.set(id, channel);
218
+ return channel;
219
+ }
220
+ stop(id) {
221
+ const channel = this.open.get(id);
222
+ if (!channel)
223
+ return false;
224
+ channel.close();
225
+ return true;
226
+ }
227
+ stopAll() {
228
+ for (const channel of [...this.open.values()])
229
+ channel.close();
230
+ }
231
+ }
232
+ /** A channel id nobody chose, for a publisher that did not name one. */
233
+ export function generatedId() {
234
+ return `s${randomBytes(3).toString("hex")}`;
235
+ }
@@ -60,6 +60,12 @@ export declare class Connections {
60
60
  * whatever the sort happened to do.
61
61
  */
62
62
  list(): Connection[];
63
+ /**
64
+ * Live connections that are actually hearing something. The state feed and
65
+ * the page are not listeners, and counting them would put a stream over the
66
+ * free allowance with nobody listening to it.
67
+ */
68
+ get listening(): number;
63
69
  get active(): number;
64
70
  /** Drop the oldest finished entries once there are more than we keep. */
65
71
  private prune;
@@ -97,6 +97,19 @@ export class Connections {
97
97
  .sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0) || b.id - a.id);
98
98
  return [...live, ...done];
99
99
  }
100
+ /**
101
+ * Live connections that are actually hearing something. The state feed and
102
+ * the page are not listeners, and counting them would put a stream over the
103
+ * free allowance with nobody listening to it.
104
+ */
105
+ get listening() {
106
+ let count = 0;
107
+ for (const item of this.items.values()) {
108
+ if (item.endedAt === null && (item.kind === "stream" || item.kind === "media"))
109
+ count++;
110
+ }
111
+ return count;
112
+ }
100
113
  get active() {
101
114
  let count = 0;
102
115
  for (const item of this.items.values())
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The public directory.
3
+ *
4
+ * A nixamp that agrees to be listed announces itself to nixamp.com every so
5
+ * often and is forgotten when it stops. There is no database behind it: an
6
+ * entry lives for a few minutes and a heartbeat renews it, so a restart of the
7
+ * directory costs one heartbeat rather than a migration, and a stream that
8
+ * dies falls out of the list without anyone having to notice.
9
+ *
10
+ * What is published is the *listen* link. The control key never leaves the
11
+ * machine it was minted on.
12
+ */
13
+ /** How long an entry survives without a heartbeat. */
14
+ export declare const TTL_MS: number;
15
+ /** How often a publisher renews. Comfortably inside the TTL. */
16
+ export declare const HEARTBEAT_MS: number;
17
+ export declare const DEFAULT_DIRECTORY = "https://nixamp.com";
18
+ export interface Listing {
19
+ /** Assigned by the directory, so a publisher cannot claim someone else's. */
20
+ id: string;
21
+ name: string;
22
+ /** The listen link, which is what a browser opens. */
23
+ url: string;
24
+ tracks: number;
25
+ nowPlaying: string;
26
+ /** Set by the directory from the request, never by the publisher. */
27
+ updatedAt: number;
28
+ }
29
+ /** What a publisher sends. Everything else about a listing is ours to decide. */
30
+ export interface Announcement {
31
+ id?: string;
32
+ name: string;
33
+ url: string;
34
+ tracks: number;
35
+ nowPlaying: string;
36
+ }
37
+ /** Trim and flatten, so one publisher cannot draw a box in someone's terminal. */
38
+ export declare function clean(value: unknown, max: number): string;
39
+ /**
40
+ * A URL we are willing to list. It has to be somewhere a browser can go, and
41
+ * it must not be a loopback or link-local address: those are only reachable
42
+ * from the machine that published them, so listing one is an entry nobody but
43
+ * the publisher can ever open.
44
+ */
45
+ export declare function publishable(raw: string): URL | null;
46
+ export declare function parseAnnouncement(input: unknown): Announcement | null;
47
+ /**
48
+ * The registry. In memory on purpose: see the note at the top of the file.
49
+ * One entry per URL, so a publisher restarting does not leave a ghost of
50
+ * itself behind next to the entry that replaced it.
51
+ */
52
+ export declare class Directory {
53
+ private readonly ttl;
54
+ private readonly now;
55
+ private readonly items;
56
+ private sequence;
57
+ constructor(ttl?: number, now?: () => number);
58
+ announce(announcement: Announcement): Listing;
59
+ withdraw(id: string): void;
60
+ list(): Listing[];
61
+ /** Forget anything that stopped renewing. */
62
+ private sweep;
63
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The public directory.
3
+ *
4
+ * A nixamp that agrees to be listed announces itself to nixamp.com every so
5
+ * often and is forgotten when it stops. There is no database behind it: an
6
+ * entry lives for a few minutes and a heartbeat renews it, so a restart of the
7
+ * directory costs one heartbeat rather than a migration, and a stream that
8
+ * dies falls out of the list without anyone having to notice.
9
+ *
10
+ * What is published is the *listen* link. The control key never leaves the
11
+ * machine it was minted on.
12
+ */
13
+ /** How long an entry survives without a heartbeat. */
14
+ export const TTL_MS = 4 * 60 * 1000;
15
+ /** How often a publisher renews. Comfortably inside the TTL. */
16
+ export const HEARTBEAT_MS = 90 * 1000;
17
+ export const DEFAULT_DIRECTORY = "https://nixamp.com";
18
+ const MAX_NAME = 60;
19
+ const MAX_TRACK = 120;
20
+ /** Trim and flatten, so one publisher cannot draw a box in someone's terminal. */
21
+ export function clean(value, max) {
22
+ if (typeof value !== "string")
23
+ return "";
24
+ // Control characters include the escape that starts an ANSI sequence, and
25
+ // this text is rendered in a terminal as well as a browser.
26
+ return value.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, max);
27
+ }
28
+ /**
29
+ * A URL we are willing to list. It has to be somewhere a browser can go, and
30
+ * it must not be a loopback or link-local address: those are only reachable
31
+ * from the machine that published them, so listing one is an entry nobody but
32
+ * the publisher can ever open.
33
+ */
34
+ export function publishable(raw) {
35
+ let url;
36
+ try {
37
+ url = new URL(raw);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ if (url.protocol !== "http:" && url.protocol !== "https:")
43
+ return null;
44
+ const host = url.hostname.replace(/^\[|\]$/g, "");
45
+ if (host === "localhost" || host === "::1" || host.endsWith(".localhost"))
46
+ return null;
47
+ if (/^127\./.test(host) || /^169\.254\./.test(host))
48
+ return null;
49
+ return url;
50
+ }
51
+ export function parseAnnouncement(input) {
52
+ if (typeof input !== "object" || input === null)
53
+ return null;
54
+ const record = input;
55
+ const url = typeof record["url"] === "string" ? record["url"] : "";
56
+ if (publishable(url) === null)
57
+ return null;
58
+ const name = clean(record["name"], MAX_NAME);
59
+ const tracks = Number(record["tracks"]);
60
+ return {
61
+ ...(typeof record["id"] === "string" ? { id: clean(record["id"], 40) } : {}),
62
+ name: name || "a nixamp",
63
+ url,
64
+ tracks: Number.isFinite(tracks) && tracks >= 0 ? Math.min(1_000_000, Math.floor(tracks)) : 0,
65
+ nowPlaying: clean(record["nowPlaying"], MAX_TRACK),
66
+ };
67
+ }
68
+ /**
69
+ * The registry. In memory on purpose: see the note at the top of the file.
70
+ * One entry per URL, so a publisher restarting does not leave a ghost of
71
+ * itself behind next to the entry that replaced it.
72
+ */
73
+ export class Directory {
74
+ ttl;
75
+ now;
76
+ items = new Map();
77
+ sequence = 0;
78
+ constructor(ttl = TTL_MS, now = Date.now) {
79
+ this.ttl = ttl;
80
+ this.now = now;
81
+ }
82
+ announce(announcement) {
83
+ this.sweep();
84
+ const existing = [...this.items.values()].find((item) => item.url === announcement.url);
85
+ const id = existing?.id ?? `s${++this.sequence}${this.now().toString(36)}`;
86
+ const listing = {
87
+ id,
88
+ name: announcement.name,
89
+ url: announcement.url,
90
+ tracks: announcement.tracks,
91
+ nowPlaying: announcement.nowPlaying,
92
+ updatedAt: this.now(),
93
+ };
94
+ this.items.set(id, listing);
95
+ return listing;
96
+ }
97
+ withdraw(id) {
98
+ this.items.delete(id);
99
+ }
100
+ list() {
101
+ this.sweep();
102
+ return [...this.items.values()].sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id));
103
+ }
104
+ /** Forget anything that stopped renewing. */
105
+ sweep() {
106
+ const cutoff = this.now() - this.ttl;
107
+ for (const [id, item] of this.items)
108
+ if (item.updatedAt < cutoff)
109
+ this.items.delete(id);
110
+ }
111
+ }
@@ -0,0 +1,80 @@
1
+ import type { Readable } from "node:stream";
2
+ /** A live source is one session at a time: two would be two songs at once. */
3
+ export interface IngestSession {
4
+ id: string;
5
+ /** What the sender called itself. */
6
+ name: string;
7
+ /** The container the sender is producing, e.g. webm from MediaRecorder. */
8
+ format: string;
9
+ startedAt: number;
10
+ bytes: number;
11
+ }
12
+ export interface IngestStatus {
13
+ live: boolean;
14
+ session: IngestSession | null;
15
+ /** Where a native broadcaster should publish, when one is being listened for. */
16
+ rtmp: {
17
+ port: number;
18
+ path: string;
19
+ } | null;
20
+ }
21
+ /**
22
+ * ffmpeg's `-f` is a demuxer name, and passing an unknown one is how a stream
23
+ * dies four seconds in with a message nobody sees. An unrecognised container is
24
+ * refused up front instead.
25
+ */
26
+ export declare function normaliseFormat(value: unknown): string | null;
27
+ export interface IngestOptions {
28
+ ffmpeg: string[];
29
+ /** Where the decoded audio should go: a file ffmpeg writes, or a pipe. */
30
+ sink: string;
31
+ /** Called when a session starts, so the player can switch to it. */
32
+ onStart: (session: IngestSession) => void;
33
+ /** Called when it ends, cleanly or otherwise. */
34
+ onEnd: (session: IngestSession, error: string) => void;
35
+ /** Encoded audio, as it arrives from a live publisher. */
36
+ onAudio?: (chunk: Buffer) => void;
37
+ }
38
+ /**
39
+ * The live input.
40
+ *
41
+ * One session at a time: a second sender is refused rather than mixed, because
42
+ * mixing two uninvited streams is never what anybody meant.
43
+ */
44
+ export declare class Ingest {
45
+ private readonly options;
46
+ private child;
47
+ private session;
48
+ private closing;
49
+ /** The RTMP listener, which outlives any one publisher. */
50
+ private rtmpChild;
51
+ private rtmpPort;
52
+ private rtmpKey;
53
+ constructor(options: IngestOptions);
54
+ status(): IngestStatus;
55
+ get live(): boolean;
56
+ /**
57
+ * Open a session. Returns the session, or null when one is already running:
58
+ * the caller answers 409, because "somebody else is already broadcasting" is
59
+ * a different problem from "your request was wrong".
60
+ */
61
+ open(name: string, format: string): IngestSession | null;
62
+ /** Feed it. Returns false once the session is over. */
63
+ write(chunk: Buffer): boolean;
64
+ /** Pipe a whole request body in, for a sender that can stream one. */
65
+ pump(body: Readable): Promise<void>;
66
+ /**
67
+ * Wait for an RTMP publisher, and keep waiting after each one leaves.
68
+ *
69
+ * ffmpeg is the RTMP server here: `-rtmp_listen 1` binds the port and blocks
70
+ * until somebody publishes. It serves one publisher and exits, so the
71
+ * listener is started again afterwards -- otherwise a broadcaster who
72
+ * reconnects finds nothing listening.
73
+ */
74
+ listenRtmp(port: number, key: string): void;
75
+ /** Stop waiting for publishers. */
76
+ stopRtmp(): void;
77
+ private armRtmp;
78
+ /** End the session, whoever ended it. */
79
+ close(error?: string): void;
80
+ }