nixamp 0.7.36 → 0.7.37
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/channels.d.ts +19 -0
- package/dist/channels.js +35 -0
- package/dist/server.d.ts +11 -1
- package/dist/server.js +47 -10
- package/package.json +1 -1
- package/src/channels.ts +55 -0
- package/src/server.ts +61 -11
- package/web/dist/install.sh +29 -2
- package/web/dist/sw.js +1 -1
package/dist/channels.d.ts
CHANGED
|
@@ -198,5 +198,24 @@ export declare class Channels {
|
|
|
198
198
|
stop(id: string): boolean;
|
|
199
199
|
stopAll(): void;
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The channels a server pulls itself, remembered across a restart.
|
|
203
|
+
*
|
|
204
|
+
* A server is restarted to pick up a new version, which is to say often, and
|
|
205
|
+
* every restart used to take CNN off the air until somebody noticed and put
|
|
206
|
+
* it back by hand. A publisher's stream cannot be remembered -- it restarts at
|
|
207
|
+
* the publisher's end -- but a pulled one is a name and a URL, and a name and
|
|
208
|
+
* a URL can be written down.
|
|
209
|
+
*
|
|
210
|
+
* Keyed by port, like the keys, because two servers on one machine are two
|
|
211
|
+
* different line-ups.
|
|
212
|
+
*/
|
|
213
|
+
export interface RememberedChannel {
|
|
214
|
+
id: string;
|
|
215
|
+
name: string;
|
|
216
|
+
source: string;
|
|
217
|
+
}
|
|
218
|
+
export declare function rememberedChannels(dir: string, port: number): RememberedChannel[];
|
|
219
|
+
export declare function rememberChannels(dir: string, port: number, list: RememberedChannel[]): void;
|
|
201
220
|
/** A channel id nobody chose, for a publisher that did not name one. */
|
|
202
221
|
export declare function generatedId(): string;
|
package/dist/channels.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
18
|
import { randomBytes } from "node:crypto";
|
|
19
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
19
21
|
import { Fragments } from "./fragments.js";
|
|
20
22
|
/** How long to wait before dialling a dropped source again. */
|
|
21
23
|
export const REDIAL = 2000;
|
|
@@ -529,6 +531,39 @@ export class Channels {
|
|
|
529
531
|
channel.close();
|
|
530
532
|
}
|
|
531
533
|
}
|
|
534
|
+
const REMEMBERED = "channels.json";
|
|
535
|
+
export function rememberedChannels(dir, port) {
|
|
536
|
+
try {
|
|
537
|
+
const all = JSON.parse(readFileSync(join(dir, REMEMBERED), "utf8"));
|
|
538
|
+
const list = all[String(port)];
|
|
539
|
+
if (!Array.isArray(list))
|
|
540
|
+
return [];
|
|
541
|
+
return list.filter((one) => typeof one === "object" && one !== null &&
|
|
542
|
+
typeof one.id === "string" &&
|
|
543
|
+
typeof one.name === "string" &&
|
|
544
|
+
typeof one.source === "string");
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
export function rememberChannels(dir, port, list) {
|
|
551
|
+
let all = {};
|
|
552
|
+
try {
|
|
553
|
+
all = JSON.parse(readFileSync(join(dir, REMEMBERED), "utf8"));
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// First time, or unreadable: start again rather than refuse to remember.
|
|
557
|
+
}
|
|
558
|
+
all[String(port)] = list;
|
|
559
|
+
try {
|
|
560
|
+
mkdirSync(dir, { recursive: true });
|
|
561
|
+
writeFileSync(join(dir, REMEMBERED), JSON.stringify(all, null, 2));
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// A state directory that cannot be written costs a memory, not a stream.
|
|
565
|
+
}
|
|
566
|
+
}
|
|
532
567
|
/** A channel id nobody chose, for a publisher that did not name one. */
|
|
533
568
|
export function generatedId() {
|
|
534
569
|
return `s${randomBytes(3).toString("hex")}`;
|
package/dist/server.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type IncomingMessage, type Server, type ServerResponse } from "node:htt
|
|
|
2
2
|
import { Connections } from "./connections.ts";
|
|
3
3
|
import { Broadcaster, type Destination, type EncoderSettings } from "./broadcast.ts";
|
|
4
4
|
import { Ingest } from "./ingest.ts";
|
|
5
|
-
import { Channels } from "./channels.ts";
|
|
5
|
+
import { Channels, type Channel, type RememberedChannel } from "./channels.ts";
|
|
6
6
|
import { Accounts } from "./accounts.ts";
|
|
7
7
|
import { Handles } from "./handles.ts";
|
|
8
8
|
import { OpenDirs } from "./opendirs.ts";
|
|
@@ -336,6 +336,14 @@ export declare class PlayerEngine implements Engine {
|
|
|
336
336
|
* looking at what is on wants "that album from the web", not every track in
|
|
337
337
|
* it. An entry names where to start, so clicking it plays.
|
|
338
338
|
*/
|
|
339
|
+
/**
|
|
340
|
+
* Probe a source and start carrying it as a channel of its own.
|
|
341
|
+
*
|
|
342
|
+
* Shared by the request that puts one on and the boot that puts remembered
|
|
343
|
+
* ones back, so that both agree on what a source is encoded as. Null when
|
|
344
|
+
* that channel id is already on.
|
|
345
|
+
*/
|
|
346
|
+
export declare function pullChannel(channels: Channels, ffprobe: string[], id: string, name: string, source: string): Promise<Channel | null>;
|
|
339
347
|
export declare function liveOnes(engine: Engine): {
|
|
340
348
|
name: string;
|
|
341
349
|
at: number;
|
|
@@ -403,6 +411,8 @@ export interface HandlerOptions {
|
|
|
403
411
|
ingest?: Ingest;
|
|
404
412
|
/** Several live streams at once, each with its own audience. */
|
|
405
413
|
channels?: Channels;
|
|
414
|
+
/** Write down the channels this server pulls, so a restart puts them back. */
|
|
415
|
+
rememberChannels?: (list: RememberedChannel[]) => void;
|
|
406
416
|
/** Live audio going out to RTMP. */
|
|
407
417
|
broadcaster?: Broadcaster;
|
|
408
418
|
/** Where a broadcast should send, and what it should look like. */
|
package/dist/server.js
CHANGED
|
@@ -19,7 +19,7 @@ import { readFileSync } from "node:fs";
|
|
|
19
19
|
import { Connections } from "./connections.js";
|
|
20
20
|
import { Broadcaster, DEFAULT_ENCODER, PRESETS, redact, } from "./broadcast.js";
|
|
21
21
|
import { Ingest, normaliseFormat } from "./ingest.js";
|
|
22
|
-
import { Channels, cleanId, generatedId } from "./channels.js";
|
|
22
|
+
import { Channels, cleanId, generatedId, rememberChannels, rememberedChannels, } from "./channels.js";
|
|
23
23
|
import { RtmpListeners } from "./rtmp-in.js";
|
|
24
24
|
import { Accounts, clearedCookie, sessionCookie, tokenFrom } from "./accounts.js";
|
|
25
25
|
import { anonymousHandle, Handles } from "./handles.js";
|
|
@@ -707,6 +707,23 @@ export class PlayerEngine {
|
|
|
707
707
|
* looking at what is on wants "that album from the web", not every track in
|
|
708
708
|
* it. An entry names where to start, so clicking it plays.
|
|
709
709
|
*/
|
|
710
|
+
/**
|
|
711
|
+
* Probe a source and start carrying it as a channel of its own.
|
|
712
|
+
*
|
|
713
|
+
* Shared by the request that puts one on and the boot that puts remembered
|
|
714
|
+
* ones back, so that both agree on what a source is encoded as. Null when
|
|
715
|
+
* that channel id is already on.
|
|
716
|
+
*/
|
|
717
|
+
export async function pullChannel(channels, ffprobe, id, name, source) {
|
|
718
|
+
const codecs = await codecsOf({ ffmpeg: [], ffprobe, play: null }, source);
|
|
719
|
+
const kind = codecs.video === "" ? "audio" : "video";
|
|
720
|
+
const encode = kind === "video"
|
|
721
|
+
? videoArgs(codecs)
|
|
722
|
+
// No picture in it, so none is invented: MP3 is the thing every browser
|
|
723
|
+
// plays and the thing a listener can join halfway through.
|
|
724
|
+
: ["-vn", "-c:a", "libmp3lame", "-b:a", "192k", "-f", "mp3"];
|
|
725
|
+
return channels.pull(id, name, source, encode, kind);
|
|
726
|
+
}
|
|
710
727
|
export function liveOnes(engine) {
|
|
711
728
|
const tracks = engine.snapshot().tracks ?? [];
|
|
712
729
|
const found = new Map();
|
|
@@ -1857,6 +1874,13 @@ export function createHandler(engine, options) {
|
|
|
1857
1874
|
// Asked once: the second call would answer false, having just stopped
|
|
1858
1875
|
// the thing it was asking about.
|
|
1859
1876
|
const stopped = channels.stop(id);
|
|
1877
|
+
// Taken off on purpose is forgotten on purpose: it must not come back
|
|
1878
|
+
// at the next restart.
|
|
1879
|
+
if (stopped && options.rememberChannels) {
|
|
1880
|
+
options.rememberChannels(channels.list()
|
|
1881
|
+
.filter((one) => one.via === "pull" && one.source)
|
|
1882
|
+
.map((one) => ({ id: one.id, name: one.name, source: one.source })));
|
|
1883
|
+
}
|
|
1860
1884
|
json(response, stopped ? 200 : 404, { ok: stopped });
|
|
1861
1885
|
return;
|
|
1862
1886
|
}
|
|
@@ -1927,19 +1951,20 @@ export function createHandler(engine, options) {
|
|
|
1927
1951
|
json(response, 409, { error: "that channel is already on" });
|
|
1928
1952
|
return;
|
|
1929
1953
|
}
|
|
1930
|
-
const
|
|
1931
|
-
const codecs = await codecsOf({ ffmpeg: [], ffprobe: probe, play: null }, source);
|
|
1932
|
-
const kind = codecs.video === "" ? "audio" : "video";
|
|
1933
|
-
const encode = kind === "video"
|
|
1934
|
-
? videoArgs(codecs)
|
|
1935
|
-
// No picture in it, so none is invented: MP3 is the thing every
|
|
1936
|
-
// browser plays and the thing a listener can join halfway through.
|
|
1937
|
-
: ["-vn", "-c:a", "libmp3lame", "-b:a", "192k", "-f", "mp3"];
|
|
1938
|
-
const channel = channels.pull(wanted, called, source, encode, kind);
|
|
1954
|
+
const channel = await pullChannel(channels, options.ffprobe ?? ["ffprobe"], wanted, called, source);
|
|
1939
1955
|
if (!channel) {
|
|
1940
1956
|
json(response, 409, { error: "that channel is already on" });
|
|
1941
1957
|
return;
|
|
1942
1958
|
}
|
|
1959
|
+
// Written down, so a restart puts it back on the air.
|
|
1960
|
+
if (options.rememberChannels) {
|
|
1961
|
+
options.rememberChannels([
|
|
1962
|
+
...channels.list()
|
|
1963
|
+
.filter((one) => one.via === "pull" && one.source && one.id !== wanted)
|
|
1964
|
+
.map((one) => ({ id: one.id, name: one.name, source: one.source })),
|
|
1965
|
+
{ id: wanted, name: called, source },
|
|
1966
|
+
]);
|
|
1967
|
+
}
|
|
1943
1968
|
json(response, 200, { ok: true, channel: channel.info });
|
|
1944
1969
|
return;
|
|
1945
1970
|
}
|
|
@@ -2810,6 +2835,17 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2810
2835
|
onStart: (info) => console.log(` ${info.name} is publishing to "${info.id}" (${info.format} over ${info.via}).`),
|
|
2811
2836
|
onEnd: (info) => console.log(` "${info.id}" stopped.`),
|
|
2812
2837
|
});
|
|
2838
|
+
// The channels this server was carrying when it was last stopped, put back
|
|
2839
|
+
// on. A server is restarted to pick up a new version, which is often, and
|
|
2840
|
+
// every restart used to take CNN off the air until somebody noticed.
|
|
2841
|
+
const remembering = (list) => rememberChannels(stateDir(), options.port, list);
|
|
2842
|
+
for (const one of rememberedChannels(stateDir(), options.port)) {
|
|
2843
|
+
console.log(` Putting "${one.id}" (${one.name}) back on the air.`);
|
|
2844
|
+
void pullChannel(channels, tools.ffprobe, one.id, one.name, one.source).then((channel) => {
|
|
2845
|
+
if (!channel)
|
|
2846
|
+
console.log(` "${one.id}" is already on.`);
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2813
2849
|
const destinations = parseDestinations(options.rtmp);
|
|
2814
2850
|
const broadcaster = new Broadcaster(tools.ffmpeg);
|
|
2815
2851
|
const ingest = options.ingest
|
|
@@ -2994,6 +3030,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
2994
3030
|
media: options.media,
|
|
2995
3031
|
owner,
|
|
2996
3032
|
channels,
|
|
3033
|
+
rememberChannels: remembering,
|
|
2997
3034
|
publishUrls: () => publishUrls,
|
|
2998
3035
|
serverName: options.name || hostname(),
|
|
2999
3036
|
homeSource: root,
|
package/package.json
CHANGED
package/src/channels.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
18
18
|
import { randomBytes } from "node:crypto";
|
|
19
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
19
21
|
import type { Readable } from "node:stream";
|
|
20
22
|
import { Fragments } from "./fragments.ts";
|
|
21
23
|
|
|
@@ -599,6 +601,59 @@ export class Channels {
|
|
|
599
601
|
}
|
|
600
602
|
}
|
|
601
603
|
|
|
604
|
+
/**
|
|
605
|
+
* The channels a server pulls itself, remembered across a restart.
|
|
606
|
+
*
|
|
607
|
+
* A server is restarted to pick up a new version, which is to say often, and
|
|
608
|
+
* every restart used to take CNN off the air until somebody noticed and put
|
|
609
|
+
* it back by hand. A publisher's stream cannot be remembered -- it restarts at
|
|
610
|
+
* the publisher's end -- but a pulled one is a name and a URL, and a name and
|
|
611
|
+
* a URL can be written down.
|
|
612
|
+
*
|
|
613
|
+
* Keyed by port, like the keys, because two servers on one machine are two
|
|
614
|
+
* different line-ups.
|
|
615
|
+
*/
|
|
616
|
+
export interface RememberedChannel {
|
|
617
|
+
id: string;
|
|
618
|
+
name: string;
|
|
619
|
+
source: string;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const REMEMBERED = "channels.json";
|
|
623
|
+
|
|
624
|
+
export function rememberedChannels(dir: string, port: number): RememberedChannel[] {
|
|
625
|
+
try {
|
|
626
|
+
const all = JSON.parse(readFileSync(join(dir, REMEMBERED), "utf8")) as Record<string, unknown>;
|
|
627
|
+
const list = all[String(port)];
|
|
628
|
+
if (!Array.isArray(list)) return [];
|
|
629
|
+
return list.filter(
|
|
630
|
+
(one): one is RememberedChannel =>
|
|
631
|
+
typeof one === "object" && one !== null &&
|
|
632
|
+
typeof (one as RememberedChannel).id === "string" &&
|
|
633
|
+
typeof (one as RememberedChannel).name === "string" &&
|
|
634
|
+
typeof (one as RememberedChannel).source === "string",
|
|
635
|
+
);
|
|
636
|
+
} catch {
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function rememberChannels(dir: string, port: number, list: RememberedChannel[]): void {
|
|
642
|
+
let all: Record<string, unknown> = {};
|
|
643
|
+
try {
|
|
644
|
+
all = JSON.parse(readFileSync(join(dir, REMEMBERED), "utf8")) as Record<string, unknown>;
|
|
645
|
+
} catch {
|
|
646
|
+
// First time, or unreadable: start again rather than refuse to remember.
|
|
647
|
+
}
|
|
648
|
+
all[String(port)] = list;
|
|
649
|
+
try {
|
|
650
|
+
mkdirSync(dir, { recursive: true });
|
|
651
|
+
writeFileSync(join(dir, REMEMBERED), JSON.stringify(all, null, 2));
|
|
652
|
+
} catch {
|
|
653
|
+
// A state directory that cannot be written costs a memory, not a stream.
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
602
657
|
/** A channel id nobody chose, for a publisher that did not name one. */
|
|
603
658
|
export function generatedId(): string {
|
|
604
659
|
return `s${randomBytes(3).toString("hex")}`;
|
package/src/server.ts
CHANGED
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
redact,
|
|
27
27
|
} from "./broadcast.ts";
|
|
28
28
|
import { Ingest, normaliseFormat } from "./ingest.ts";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
Channels, cleanId, generatedId, rememberChannels, rememberedChannels,
|
|
31
|
+
type Channel, type RememberedChannel,
|
|
32
|
+
} from "./channels.ts";
|
|
30
33
|
import { RtmpListeners } from "./rtmp-in.ts";
|
|
31
34
|
import { Accounts, clearedCookie, sessionCookie, tokenFrom } from "./accounts.ts";
|
|
32
35
|
import { anonymousHandle, Handles } from "./handles.ts";
|
|
@@ -908,6 +911,30 @@ export class PlayerEngine implements Engine {
|
|
|
908
911
|
* looking at what is on wants "that album from the web", not every track in
|
|
909
912
|
* it. An entry names where to start, so clicking it plays.
|
|
910
913
|
*/
|
|
914
|
+
/**
|
|
915
|
+
* Probe a source and start carrying it as a channel of its own.
|
|
916
|
+
*
|
|
917
|
+
* Shared by the request that puts one on and the boot that puts remembered
|
|
918
|
+
* ones back, so that both agree on what a source is encoded as. Null when
|
|
919
|
+
* that channel id is already on.
|
|
920
|
+
*/
|
|
921
|
+
export async function pullChannel(
|
|
922
|
+
channels: Channels,
|
|
923
|
+
ffprobe: string[],
|
|
924
|
+
id: string,
|
|
925
|
+
name: string,
|
|
926
|
+
source: string,
|
|
927
|
+
): Promise<Channel | null> {
|
|
928
|
+
const codecs = await codecsOf({ ffmpeg: [], ffprobe, play: null }, source);
|
|
929
|
+
const kind = codecs.video === "" ? "audio" : "video";
|
|
930
|
+
const encode = kind === "video"
|
|
931
|
+
? videoArgs(codecs)
|
|
932
|
+
// No picture in it, so none is invented: MP3 is the thing every browser
|
|
933
|
+
// plays and the thing a listener can join halfway through.
|
|
934
|
+
: ["-vn", "-c:a", "libmp3lame", "-b:a", "192k", "-f", "mp3"];
|
|
935
|
+
return channels.pull(id, name, source, encode, kind);
|
|
936
|
+
}
|
|
937
|
+
|
|
911
938
|
export function liveOnes(engine: Engine): { name: string; at: number; tracks: number }[] {
|
|
912
939
|
const tracks = engine.snapshot().tracks ?? [];
|
|
913
940
|
const found = new Map<string, { name: string; at: number; tracks: number }>();
|
|
@@ -1065,6 +1092,8 @@ export interface HandlerOptions {
|
|
|
1065
1092
|
ingest?: Ingest;
|
|
1066
1093
|
/** Several live streams at once, each with its own audience. */
|
|
1067
1094
|
channels?: Channels;
|
|
1095
|
+
/** Write down the channels this server pulls, so a restart puts them back. */
|
|
1096
|
+
rememberChannels?: (list: RememberedChannel[]) => void;
|
|
1068
1097
|
/** Live audio going out to RTMP. */
|
|
1069
1098
|
broadcaster?: Broadcaster;
|
|
1070
1099
|
/** Where a broadcast should send, and what it should look like. */
|
|
@@ -2260,6 +2289,15 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
2260
2289
|
// Asked once: the second call would answer false, having just stopped
|
|
2261
2290
|
// the thing it was asking about.
|
|
2262
2291
|
const stopped = channels.stop(id);
|
|
2292
|
+
// Taken off on purpose is forgotten on purpose: it must not come back
|
|
2293
|
+
// at the next restart.
|
|
2294
|
+
if (stopped && options.rememberChannels) {
|
|
2295
|
+
options.rememberChannels(
|
|
2296
|
+
channels.list()
|
|
2297
|
+
.filter((one) => one.via === "pull" && one.source)
|
|
2298
|
+
.map((one) => ({ id: one.id, name: one.name, source: one.source as string })),
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2263
2301
|
json(response, stopped ? 200 : 404, { ok: stopped });
|
|
2264
2302
|
return;
|
|
2265
2303
|
}
|
|
@@ -2335,20 +2373,20 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
|
|
|
2335
2373
|
return;
|
|
2336
2374
|
}
|
|
2337
2375
|
|
|
2338
|
-
const
|
|
2339
|
-
const codecs = await codecsOf({ ffmpeg: [], ffprobe: probe, play: null }, source);
|
|
2340
|
-
const kind = codecs.video === "" ? "audio" : "video";
|
|
2341
|
-
const encode = kind === "video"
|
|
2342
|
-
? videoArgs(codecs)
|
|
2343
|
-
// No picture in it, so none is invented: MP3 is the thing every
|
|
2344
|
-
// browser plays and the thing a listener can join halfway through.
|
|
2345
|
-
: ["-vn", "-c:a", "libmp3lame", "-b:a", "192k", "-f", "mp3"];
|
|
2346
|
-
|
|
2347
|
-
const channel = channels.pull(wanted, called, source, encode, kind);
|
|
2376
|
+
const channel = await pullChannel(channels, options.ffprobe ?? ["ffprobe"], wanted, called, source);
|
|
2348
2377
|
if (!channel) {
|
|
2349
2378
|
json(response, 409, { error: "that channel is already on" });
|
|
2350
2379
|
return;
|
|
2351
2380
|
}
|
|
2381
|
+
// Written down, so a restart puts it back on the air.
|
|
2382
|
+
if (options.rememberChannels) {
|
|
2383
|
+
options.rememberChannels([
|
|
2384
|
+
...channels.list()
|
|
2385
|
+
.filter((one) => one.via === "pull" && one.source && one.id !== wanted)
|
|
2386
|
+
.map((one) => ({ id: one.id, name: one.name, source: one.source as string })),
|
|
2387
|
+
{ id: wanted, name: called, source },
|
|
2388
|
+
]);
|
|
2389
|
+
}
|
|
2352
2390
|
json(response, 200, { ok: true, channel: channel.info });
|
|
2353
2391
|
return;
|
|
2354
2392
|
}
|
|
@@ -3279,6 +3317,17 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
3279
3317
|
onEnd: (info) => console.log(` "${info.id}" stopped.`),
|
|
3280
3318
|
});
|
|
3281
3319
|
|
|
3320
|
+
// The channels this server was carrying when it was last stopped, put back
|
|
3321
|
+
// on. A server is restarted to pick up a new version, which is often, and
|
|
3322
|
+
// every restart used to take CNN off the air until somebody noticed.
|
|
3323
|
+
const remembering = (list: RememberedChannel[]): void => rememberChannels(stateDir(), options.port, list);
|
|
3324
|
+
for (const one of rememberedChannels(stateDir(), options.port)) {
|
|
3325
|
+
console.log(` Putting "${one.id}" (${one.name}) back on the air.`);
|
|
3326
|
+
void pullChannel(channels, tools.ffprobe, one.id, one.name, one.source).then((channel) => {
|
|
3327
|
+
if (!channel) console.log(` "${one.id}" is already on.`);
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3282
3331
|
const destinations = parseDestinations(options.rtmp);
|
|
3283
3332
|
const broadcaster = new Broadcaster(tools.ffmpeg);
|
|
3284
3333
|
const ingest = options.ingest
|
|
@@ -3479,6 +3528,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
|
|
|
3479
3528
|
media: options.media,
|
|
3480
3529
|
owner,
|
|
3481
3530
|
channels,
|
|
3531
|
+
rememberChannels: remembering,
|
|
3482
3532
|
publishUrls: () => publishUrls,
|
|
3483
3533
|
serverName: options.name || hostname(),
|
|
3484
3534
|
homeSource: root,
|
package/web/dist/install.sh
CHANGED
|
@@ -98,6 +98,22 @@ BASE="${NIXAMP_RELEASE_BASE:-https://github.com/$REPO/releases/download/v$VERSIO
|
|
|
98
98
|
SHARE="$PREFIX/share/nixamp"
|
|
99
99
|
BIN="$PREFIX/bin"
|
|
100
100
|
|
|
101
|
+
# --- a daemon already running here --------------------------------------------
|
|
102
|
+
#
|
|
103
|
+
# Noticed now, restarted at the end. An update used to leave the old version
|
|
104
|
+
# running until somebody remembered to restart it, and restarting it by hand
|
|
105
|
+
# from the wrong directory started a server of your home folder over plain
|
|
106
|
+
# http. `nixamp daemon restart` replays the flags the daemon was started with,
|
|
107
|
+
# so the installer runs that rather than guessing at any.
|
|
108
|
+
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/nixamp"
|
|
109
|
+
DAEMON_PID=""
|
|
110
|
+
if [ -r "$STATE_DIR/daemon.json" ]; then
|
|
111
|
+
DAEMON_PID=$(sed -n 's/^[[:space:]]*"pid":[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$STATE_DIR/daemon.json" | head -n 1)
|
|
112
|
+
if [ -n "$DAEMON_PID" ] && ! kill -0 "$DAEMON_PID" 2>/dev/null; then
|
|
113
|
+
DAEMON_PID=""
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
101
117
|
say "nixamp $VERSION"
|
|
102
118
|
say " platform: $OS-$ARCH"
|
|
103
119
|
say " desktop: $WANT_DESKTOP"
|
|
@@ -219,7 +235,7 @@ else
|
|
|
219
235
|
RUNTIME=""
|
|
220
236
|
|
|
221
237
|
command -v node >/dev/null 2>&1 ||
|
|
222
|
-
say " note: no desktop app was installed, so the CLI needs Node
|
|
238
|
+
say " note: no desktop app was installed, so the CLI needs Node 22.6 or newer, and none is on PATH."
|
|
223
239
|
fi
|
|
224
240
|
|
|
225
241
|
# The shim. Written here rather than shipped, because only the installer knows
|
|
@@ -236,7 +252,7 @@ else
|
|
|
236
252
|
#!/bin/sh
|
|
237
253
|
# nixamp. Written by the installer; \`nixamp uninstall\` removes it.
|
|
238
254
|
command -v node >/dev/null 2>&1 || {
|
|
239
|
-
echo "nixamp: node
|
|
255
|
+
echo "nixamp: node is not on PATH. This install runs on your own Node (22.6 or newer)." >&2
|
|
240
256
|
exit 69
|
|
241
257
|
}
|
|
242
258
|
NIXAMP_HOME="$SHARE" exec node "$CLI_DIR/bin/nixamp.mjs" "\$@"
|
|
@@ -362,6 +378,17 @@ elif [ "$FW_RESULT" = manual ]; then
|
|
|
362
378
|
say " Open it with: $(firewall_command)"
|
|
363
379
|
fi
|
|
364
380
|
|
|
381
|
+
if [ -n "$DAEMON_PID" ]; then
|
|
382
|
+
say ""
|
|
383
|
+
say "A nixamp daemon was running (pid $DAEMON_PID). Restarting it on $VERSION,"
|
|
384
|
+
say "with the flags it was started with..."
|
|
385
|
+
if "$BIN/nixamp" daemon restart; then
|
|
386
|
+
say " Restarted. The channels it was carrying come back on their own."
|
|
387
|
+
else
|
|
388
|
+
say " Could not restart it. Run: nixamp daemon restart"
|
|
389
|
+
fi
|
|
390
|
+
fi
|
|
391
|
+
|
|
365
392
|
case ":$PATH:" in
|
|
366
393
|
*":$BIN:"*)
|
|
367
394
|
say ""
|
package/web/dist/sw.js
CHANGED