nixamp 0.16.0 → 0.17.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 +139 -0
- package/dist/audio.d.ts +70 -1
- package/dist/audio.js +84 -5
- package/dist/channels.d.ts +107 -3
- package/dist/channels.js +151 -3
- package/dist/compression/analyze.d.ts +105 -0
- package/dist/compression/analyze.js +213 -0
- package/dist/compression/blocks.d.ts +35 -0
- package/dist/compression/blocks.js +73 -0
- package/dist/compression/cli.d.ts +1 -0
- package/dist/compression/cli.js +347 -0
- package/dist/compression/codec.d.ts +66 -0
- package/dist/compression/codec.js +178 -0
- package/dist/compression/envelope.d.ts +77 -0
- package/dist/compression/envelope.js +191 -0
- package/dist/compression/jobs.d.ts +59 -0
- package/dist/compression/jobs.js +120 -0
- package/dist/compression/metrics.d.ts +62 -0
- package/dist/compression/metrics.js +64 -0
- package/dist/compression/policy.d.ts +76 -0
- package/dist/compression/policy.js +148 -0
- package/dist/compression/receiver.d.ts +51 -0
- package/dist/compression/receiver.js +92 -0
- package/dist/compression/relay.d.ts +134 -0
- package/dist/compression/relay.js +430 -0
- package/dist/compression/routes.d.ts +25 -0
- package/dist/compression/routes.js +266 -0
- package/dist/compression/service.d.ts +162 -0
- package/dist/compression/service.js +488 -0
- package/dist/compression/static.d.ts +65 -0
- package/dist/compression/static.js +248 -0
- package/dist/compression/store.d.ts +36 -0
- package/dist/compression/store.js +119 -0
- package/dist/compression/ts-transform.d.ts +44 -0
- package/dist/compression/ts-transform.js +148 -0
- package/dist/hls.d.ts +36 -3
- package/dist/hls.js +85 -11
- package/dist/layouts.d.ts +10 -0
- package/dist/layouts.js +71 -1
- package/dist/live-api.d.ts +16 -2
- package/dist/live-api.js +152 -19
- package/dist/live-events.d.ts +57 -2
- package/dist/live-events.js +243 -11
- package/dist/main.js +47 -0
- package/dist/mcp.d.ts +40 -0
- package/dist/mcp.js +255 -0
- package/dist/oauth-api.d.ts +50 -0
- package/dist/oauth-api.js +381 -0
- package/dist/oauth-server.d.ts +174 -0
- package/dist/oauth-server.js +559 -0
- package/dist/party.d.ts +32 -0
- package/dist/party.js +196 -0
- package/dist/playlist.d.ts +11 -0
- package/dist/playlist.js +27 -6
- package/dist/server.d.ts +37 -0
- package/dist/server.js +277 -45
- package/dist/sources.d.ts +16 -8
- package/dist/sources.js +102 -0
- package/dist/tickets.d.ts +67 -0
- package/dist/tickets.js +156 -0
- package/dist/tokens.d.ts +27 -2
- package/dist/tokens.js +42 -1
- package/dist/watch-party.d.ts +127 -0
- package/dist/watch-party.js +301 -0
- package/package.json +1 -1
- package/src/audio.ts +119 -6
- package/src/channels.ts +177 -7
- package/src/compression/analyze.ts +270 -0
- package/src/compression/blocks.ts +93 -0
- package/src/compression/cli.ts +340 -0
- package/src/compression/codec.ts +202 -0
- package/src/compression/envelope.ts +237 -0
- package/src/compression/jobs.ts +157 -0
- package/src/compression/metrics.ts +109 -0
- package/src/compression/policy.ts +171 -0
- package/src/compression/receiver.ts +107 -0
- package/src/compression/relay.ts +462 -0
- package/src/compression/routes.ts +294 -0
- package/src/compression/service.ts +525 -0
- package/src/compression/static.ts +264 -0
- package/src/compression/store.ts +126 -0
- package/src/compression/ts-transform.ts +148 -0
- package/src/hls.ts +100 -10
- package/src/layouts.ts +78 -1
- package/src/live-api.ts +172 -17
- package/src/live-events.ts +269 -11
- package/src/main.ts +47 -0
- package/src/mcp.ts +282 -0
- package/src/oauth-api.ts +481 -0
- package/src/oauth-server.ts +653 -0
- package/src/party.ts +216 -0
- package/src/playlist.ts +28 -5
- package/src/server.ts +296 -44
- package/src/sources.ts +100 -0
- package/src/tickets.ts +195 -0
- package/src/tokens.ts +50 -3
- package/src/watch-party.ts +390 -0
- package/web/dist/assets/{hls-3VKVEQE3-CrILISPJ.js → hls-3VKVEQE3-eV54kXE3.js} +1 -1
- package/web/dist/assets/index-CurZFzlH.css +1 -0
- package/web/dist/assets/index-DDzutJ75.js +1 -0
- package/web/dist/assets/{mpegts-CWeN63eG.js → mpegts-Buc3Odv6.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-DaMgRvlO.js → mpegts-LO6RVLD6-DcDKPB4P.js} +1 -1
- package/web/dist/index.html +59 -6
- package/web/dist/sw.js +6 -6
- package/web/dist/assets/index-CQ_m5HqS.css +0 -1
- package/web/dist/assets/index-CTxPM5KS.js +0 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP face of the compression service.
|
|
3
|
+
*
|
|
4
|
+
* Under a channel: its policy (read, and change with the version you
|
|
5
|
+
* saw), an analysis of it (a job), and its relay -- out to another nixamp
|
|
6
|
+
* that asks in the envelope's media type, or in from one. Under
|
|
7
|
+
* /api/compression: the server-wide switch and the jobs. All of it is
|
|
8
|
+
* translation; the rules live in the service. The server's own helpers
|
|
9
|
+
* for answering (JSON, counting a stream) are handed in, so this file
|
|
10
|
+
* imports nothing from the server and the server one thing from it.
|
|
11
|
+
*/
|
|
12
|
+
import { createReadStream } from "node:fs";
|
|
13
|
+
import { MEDIA_TYPE, parseModes } from "./envelope.js";
|
|
14
|
+
import { CODECS_HEADER, KIND_HEADER } from "./receiver.js";
|
|
15
|
+
/** Who a diagnostic job belongs to. Every control holder shares them. */
|
|
16
|
+
const CONTROL = "control";
|
|
17
|
+
/** The header a conditional change names the version in, or the body's own field. */
|
|
18
|
+
function expectedVersion(request, body) {
|
|
19
|
+
const header = request.headers["if-match"];
|
|
20
|
+
const raw = typeof header === "string" ? header.replace(/^W\//, "").replace(/"/g, "") : body["version"];
|
|
21
|
+
const version = Number(raw);
|
|
22
|
+
return raw === undefined || raw === "" || !Number.isInteger(version) ? undefined : version;
|
|
23
|
+
}
|
|
24
|
+
async function parsed(request, ctx) {
|
|
25
|
+
try {
|
|
26
|
+
const text = await ctx.readBody(request);
|
|
27
|
+
const body = text === "" ? {} : JSON.parse(text);
|
|
28
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Routes under /api/channels/:id/. True when the request was answered.
|
|
36
|
+
* `action` and `file` are the path segments after the id.
|
|
37
|
+
*/
|
|
38
|
+
export async function handleChannelCompression(request, response, ctx, id, action, file) {
|
|
39
|
+
const { service, json } = ctx;
|
|
40
|
+
if (action === "compression") {
|
|
41
|
+
if (file === undefined && request.method === "GET") {
|
|
42
|
+
json(response, 200, service.status(id));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (file === undefined && request.method === "PATCH") {
|
|
46
|
+
if (!(await ctx.controls())) {
|
|
47
|
+
json(response, 403, { error: "the controls are needed to change a policy" });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const body = await parsed(request, ctx);
|
|
51
|
+
if (body === null) {
|
|
52
|
+
json(response, 400, { error: "send a JSON object" });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const { version: _version, ...change } = body;
|
|
56
|
+
const result = service.set(id, change, expectedVersion(request, body));
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
json(response, result.status, { error: result.errors.join("; "), errors: result.errors, status: service.status(id) });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
json(response, 200, service.status(id));
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (file === "analyses" && request.method === "POST") {
|
|
65
|
+
if (!(await ctx.controls())) {
|
|
66
|
+
json(response, 403, { error: "the controls are needed to start an analysis" });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const body = await parsed(request, ctx);
|
|
70
|
+
const seconds = Number(body?.["seconds"] ?? 30);
|
|
71
|
+
const started = service.analyzeChannel(id, Number.isFinite(seconds) ? seconds : 30, CONTROL);
|
|
72
|
+
if ("error" in started) {
|
|
73
|
+
json(response, started.status, { error: started.error });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
json(response, started.existing ? 200 : 202, { job: started.job, existing: started.existing });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
json(response, 405, { error: "GET or PATCH the policy; POST to analyses" });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (action === "relay" && file === undefined) {
|
|
83
|
+
if (request.method === "GET") {
|
|
84
|
+
const accept = String(request.headers["accept"] ?? "");
|
|
85
|
+
if (!accept.includes(MEDIA_TYPE)) {
|
|
86
|
+
json(response, 406, {
|
|
87
|
+
error: `this is a relay for another nixamp, sent as ${MEDIA_TYPE}; a player wants the ordinary channel URL`,
|
|
88
|
+
playback: `/api/channels/${id}`,
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (request.headers["range"] !== undefined) {
|
|
93
|
+
json(response, 416, { error: "a relay is a live stream and cannot be asked for a range" });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const offered = parseModes(String(request.headers[CODECS_HEADER] ?? ""));
|
|
97
|
+
const listener = {
|
|
98
|
+
write: (chunk) => response.write(chunk),
|
|
99
|
+
end: () => {
|
|
100
|
+
response.end();
|
|
101
|
+
},
|
|
102
|
+
pending: () => response.writableLength,
|
|
103
|
+
};
|
|
104
|
+
const answer = service.relay(id, listener, offered);
|
|
105
|
+
if (!answer.ok) {
|
|
106
|
+
json(response, answer.status, { error: answer.error, code: answer.code, playback: `/api/channels/${id}` });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
ctx.watch(request, response, id);
|
|
110
|
+
response.writeHead(200, {
|
|
111
|
+
...ctx.cors,
|
|
112
|
+
"content-type": `${MEDIA_TYPE}; version=1`,
|
|
113
|
+
"cache-control": "no-store",
|
|
114
|
+
// Never squeezed again by anything in the way: it is already framed.
|
|
115
|
+
"content-encoding": "identity",
|
|
116
|
+
[CODECS_HEADER]: answer.codecs.join(","),
|
|
117
|
+
[KIND_HEADER]: answer.kind,
|
|
118
|
+
"x-nixamp-generation": String(answer.generation),
|
|
119
|
+
});
|
|
120
|
+
const leave = () => answer.session.leave();
|
|
121
|
+
request.on("close", leave);
|
|
122
|
+
response.on("close", leave);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (request.method === "POST") {
|
|
126
|
+
if (!(await ctx.controls())) {
|
|
127
|
+
json(response, 403, { error: "the controls are needed to bring a relay in" });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
const body = await parsed(request, ctx);
|
|
131
|
+
const from = typeof body?.["from"] === "string" ? body["from"] : "";
|
|
132
|
+
if (from === "") {
|
|
133
|
+
json(response, 400, { error: "say where from: { from: \"https://host:port/api/channels/<id>/relay\" }" });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const key = typeof body?.["key"] === "string" ? body["key"] : null;
|
|
137
|
+
const name = typeof body?.["name"] === "string" ? body["name"] : id;
|
|
138
|
+
const started = service.pull(id, from, key, name);
|
|
139
|
+
if (!started.ok) {
|
|
140
|
+
json(response, started.status, { error: started.error });
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
json(response, 202, { ok: true, channel: id, from });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (request.method === "DELETE") {
|
|
147
|
+
if (!(await ctx.controls())) {
|
|
148
|
+
json(response, 403, { error: "the controls are needed to stop a relay" });
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
const stopped = service.stopPull(id);
|
|
152
|
+
json(response, stopped ? 200 : 404, { ok: stopped });
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
json(response, 405, { error: "GET to receive, POST to bring one in, DELETE to stop it" });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/** Routes under /api/compression. True when the request was answered. */
|
|
161
|
+
export async function handleCompressionApi(request, response, ctx, path) {
|
|
162
|
+
const { service, json } = ctx;
|
|
163
|
+
if (path === "/api/compression") {
|
|
164
|
+
if (request.method === "GET") {
|
|
165
|
+
json(response, 200, service.overview());
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (request.method === "PATCH") {
|
|
169
|
+
if (!(await ctx.controls())) {
|
|
170
|
+
json(response, 403, { error: "the controls are needed to change the server's compression" });
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
const body = await parsed(request, ctx);
|
|
174
|
+
if (body === null) {
|
|
175
|
+
json(response, 400, { error: "send a JSON object" });
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
const change = {};
|
|
179
|
+
if ("enabled" in body) {
|
|
180
|
+
if (typeof body["enabled"] !== "boolean") {
|
|
181
|
+
json(response, 400, { error: "enabled must be true or false" });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
change.enabled = body["enabled"];
|
|
185
|
+
}
|
|
186
|
+
if ("hlsPackaging" in body) {
|
|
187
|
+
if (body["hlsPackaging"] !== "mpegts" && body["hlsPackaging"] !== "fmp4") {
|
|
188
|
+
json(response, 400, { error: "hlsPackaging must be mpegts or fmp4" });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
change.hlsPackaging = body["hlsPackaging"];
|
|
192
|
+
}
|
|
193
|
+
json(response, 200, { global: service.setGlobal(change) });
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
json(response, 405, { error: "GET or PATCH" });
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
const job = /^\/api\/compression\/analyses\/([a-z0-9]+)$/.exec(path);
|
|
200
|
+
if (job) {
|
|
201
|
+
if (!(await ctx.controls())) {
|
|
202
|
+
json(response, 403, { error: "the controls are needed to see an analysis" });
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
const jobId = job[1];
|
|
206
|
+
if (request.method === "GET") {
|
|
207
|
+
const found = service.jobs.get(jobId, CONTROL);
|
|
208
|
+
if (!found) {
|
|
209
|
+
json(response, 404, { error: "no such analysis" });
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
json(response, 200, { job: found });
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (request.method === "DELETE") {
|
|
216
|
+
const cancelled = service.jobs.cancel(jobId, CONTROL);
|
|
217
|
+
json(response, cancelled ? 200 : 404, { ok: cancelled });
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
json(response, 405, { error: "GET or DELETE" });
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The static representation of a library file, at /api/media/N/relay.
|
|
227
|
+
* The caller has already resolved N to a path and checked that media may
|
|
228
|
+
* be streamed at all.
|
|
229
|
+
*/
|
|
230
|
+
export function handleStaticRelay(request, response, ctx, file, title) {
|
|
231
|
+
const { service, json } = ctx;
|
|
232
|
+
const accept = String(request.headers["accept"] ?? "");
|
|
233
|
+
if (!accept.includes(MEDIA_TYPE)) {
|
|
234
|
+
json(response, 406, { error: `this is the file's relay representation, sent as ${MEDIA_TYPE}; a player wants the ordinary media URL` });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (request.headers["range"] !== undefined) {
|
|
238
|
+
json(response, 416, { error: "the relay representation cannot be asked for a range; the ordinary media URL can" });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const state = service.representation(file);
|
|
242
|
+
if (state.state === "off") {
|
|
243
|
+
json(response, 409, { error: "compression is off for static files on this server", code: "COMPRESSION_OFF" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (state.state === "failed") {
|
|
247
|
+
json(response, 503, { error: state.reason });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (state.state === "building") {
|
|
251
|
+
response.writeHead(202, { ...ctx.cors, "content-type": "application/json; charset=utf-8", "retry-after": "5" });
|
|
252
|
+
response.end(JSON.stringify({ building: true }));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
ctx.watch(request, response, title);
|
|
256
|
+
response.writeHead(200, {
|
|
257
|
+
...ctx.cors,
|
|
258
|
+
"content-type": `${MEDIA_TYPE}; version=1`,
|
|
259
|
+
"content-length": String(state.entry.bytes),
|
|
260
|
+
"content-encoding": "identity",
|
|
261
|
+
"cache-control": "no-store",
|
|
262
|
+
"x-nixamp-sha256": state.entry.sha256,
|
|
263
|
+
"x-nixamp-original-length": String(state.entry.size),
|
|
264
|
+
});
|
|
265
|
+
createReadStream(state.path).pipe(response);
|
|
266
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { type Channels } from "../channels.ts";
|
|
2
|
+
import { type Analysis } from "./analyze.ts";
|
|
3
|
+
import { Pool } from "./codec.ts";
|
|
4
|
+
import { type Mode } from "./envelope.ts";
|
|
5
|
+
import { AnalysisJobs, type Job } from "./jobs.ts";
|
|
6
|
+
import { type ChannelMetricsSnapshot } from "./metrics.ts";
|
|
7
|
+
import { type ChannelPolicy, type LosslessPolicy } from "./policy.ts";
|
|
8
|
+
import { type RelayListener, type RelaySession } from "./relay.ts";
|
|
9
|
+
import { type GlobalSettings, PolicyStore } from "./store.ts";
|
|
10
|
+
import { StaticCache } from "./static.ts";
|
|
11
|
+
/** The pseudo-channel whose policy governs static file representations. */
|
|
12
|
+
export declare const STATIC_CHANNEL = "static";
|
|
13
|
+
export interface AnalyzeFileOptions {
|
|
14
|
+
policy?: LosslessPolicy;
|
|
15
|
+
ffprobe?: string[];
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A file's first bytes, up to the sample limit, analysed at the source
|
|
20
|
+
* boundary: these are the bytes as they sit on disk, before any ffmpeg.
|
|
21
|
+
* Shared by the service and the CLI, which runs it with no server at all.
|
|
22
|
+
*/
|
|
23
|
+
export declare function analyzeFile(path: string, options?: AnalyzeFileOptions): Promise<Analysis>;
|
|
24
|
+
export interface CompressionServiceOptions {
|
|
25
|
+
channels: Channels;
|
|
26
|
+
/** Where settings live; null for a server that remembers nothing. */
|
|
27
|
+
stateDir: string | null;
|
|
28
|
+
port: number;
|
|
29
|
+
pool?: Pool;
|
|
30
|
+
ffprobe?: string[];
|
|
31
|
+
/** Where static representations go; null to keep none. */
|
|
32
|
+
cacheDir?: string | null;
|
|
33
|
+
maxCacheBytes?: number;
|
|
34
|
+
onEvent?: (message: string) => void;
|
|
35
|
+
}
|
|
36
|
+
export interface EffectivePolicy {
|
|
37
|
+
/** The policy as it applies right now, after the global switch. */
|
|
38
|
+
losslessCompression: LosslessPolicy;
|
|
39
|
+
hlsPackaging: ChannelPolicy["hlsPackaging"];
|
|
40
|
+
qualityProfile: ChannelPolicy["qualityProfile"];
|
|
41
|
+
/** Why it differs from what is configured, when it does. */
|
|
42
|
+
reason: string | null;
|
|
43
|
+
}
|
|
44
|
+
export interface ChannelStatus {
|
|
45
|
+
channel: string;
|
|
46
|
+
live: boolean;
|
|
47
|
+
configured: ChannelPolicy;
|
|
48
|
+
effective: EffectivePolicy;
|
|
49
|
+
global: GlobalSettings;
|
|
50
|
+
metrics: ChannelMetricsSnapshot | null;
|
|
51
|
+
relay: {
|
|
52
|
+
sessions: number;
|
|
53
|
+
generation: number;
|
|
54
|
+
} | null;
|
|
55
|
+
/** An incoming relay this channel is fed by, when it is. */
|
|
56
|
+
incoming: {
|
|
57
|
+
from: string;
|
|
58
|
+
generation: number;
|
|
59
|
+
reconnects: number;
|
|
60
|
+
error: string | null;
|
|
61
|
+
} | null;
|
|
62
|
+
}
|
|
63
|
+
export type RelayAnswer = {
|
|
64
|
+
ok: true;
|
|
65
|
+
session: RelaySession;
|
|
66
|
+
codecs: Mode[];
|
|
67
|
+
generation: number;
|
|
68
|
+
kind: "audio" | "video" | "";
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
status: 404 | 406 | 409 | 503;
|
|
72
|
+
code: string;
|
|
73
|
+
error: string;
|
|
74
|
+
};
|
|
75
|
+
export declare class CompressionService {
|
|
76
|
+
private readonly options;
|
|
77
|
+
readonly store: PolicyStore;
|
|
78
|
+
readonly jobs: AnalysisJobs;
|
|
79
|
+
readonly pool: Pool;
|
|
80
|
+
readonly statics: StaticCache | null;
|
|
81
|
+
private readonly running;
|
|
82
|
+
private readonly metrics;
|
|
83
|
+
private readonly incoming;
|
|
84
|
+
private generation;
|
|
85
|
+
constructor(options: CompressionServiceOptions);
|
|
86
|
+
private get channels();
|
|
87
|
+
/** What applies to a channel right now, and why that is not what is configured, if it is not. */
|
|
88
|
+
effective(id: string): EffectivePolicy;
|
|
89
|
+
/** For the HLS packager: how to wrap this channel. */
|
|
90
|
+
packagingOf(id: string): ChannelPolicy["hlsPackaging"];
|
|
91
|
+
status(id: string): ChannelStatus;
|
|
92
|
+
/** The whole server at a glance. */
|
|
93
|
+
overview(): {
|
|
94
|
+
global: GlobalSettings;
|
|
95
|
+
pool: Pool["stats"];
|
|
96
|
+
jobs: AnalysisJobs["counts"];
|
|
97
|
+
channels: ChannelStatus[];
|
|
98
|
+
cache: {
|
|
99
|
+
bytes: number;
|
|
100
|
+
entries: number;
|
|
101
|
+
} | null;
|
|
102
|
+
};
|
|
103
|
+
/** Change a channel's policy. A live relay under a different variant is ended; receivers reconnect. */
|
|
104
|
+
set(id: string, change: unknown, expectVersion?: number): ReturnType<PolicyStore["set"]>;
|
|
105
|
+
setGlobal(change: Partial<GlobalSettings>): GlobalSettings;
|
|
106
|
+
/** The kill switch, and its opposite. */
|
|
107
|
+
private applyPolicy;
|
|
108
|
+
/**
|
|
109
|
+
* A receiver asking for a channel. Everything that can go wrong is
|
|
110
|
+
* answered before a byte of stream: no such channel, compression off,
|
|
111
|
+
* a boundary this server cannot provide, a receiver that cannot decode
|
|
112
|
+
* anything worth sending.
|
|
113
|
+
*/
|
|
114
|
+
relay(id: string, listener: RelayListener, offered: Set<Mode>): RelayAnswer;
|
|
115
|
+
private startEncoder;
|
|
116
|
+
/** Start listening to another nixamp's channel as one of ours. */
|
|
117
|
+
pull(id: string, from: string, key: string | null, name: string): {
|
|
118
|
+
ok: true;
|
|
119
|
+
} | {
|
|
120
|
+
ok: false;
|
|
121
|
+
status: 409 | 400;
|
|
122
|
+
error: string;
|
|
123
|
+
};
|
|
124
|
+
/** Stop an incoming relay, and the channel it feeds. */
|
|
125
|
+
stopPull(id: string): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Analyse a live channel: up to `seconds` of it, or the byte limit,
|
|
128
|
+
* whichever comes first, as a job. The listener is one more on the
|
|
129
|
+
* channel; the channel is not touched.
|
|
130
|
+
*/
|
|
131
|
+
analyzeChannel(id: string, seconds: number, owner: string): {
|
|
132
|
+
job: Job;
|
|
133
|
+
existing: boolean;
|
|
134
|
+
} | {
|
|
135
|
+
error: string;
|
|
136
|
+
status: 404 | 429;
|
|
137
|
+
};
|
|
138
|
+
private sampleChannel;
|
|
139
|
+
/** Analyse a file on this machine: its first bytes, up to the sample limit. */
|
|
140
|
+
analyzeFile(path: string, signal?: AbortSignal): Promise<Analysis>;
|
|
141
|
+
/**
|
|
142
|
+
* The static representation of a file. Ready when it is on disk and
|
|
143
|
+
* still describes the file; building when this call started it (or
|
|
144
|
+
* found it being built); failed when the last build said why; off when
|
|
145
|
+
* this server keeps no cache or the static policy is off. Never a wait:
|
|
146
|
+
* a film takes minutes to read, and a request is not held for that.
|
|
147
|
+
*/
|
|
148
|
+
representation(file: string): {
|
|
149
|
+
state: "off";
|
|
150
|
+
} | {
|
|
151
|
+
state: "building";
|
|
152
|
+
} | {
|
|
153
|
+
state: "failed";
|
|
154
|
+
reason: string;
|
|
155
|
+
} | {
|
|
156
|
+
state: "ready";
|
|
157
|
+
path: string;
|
|
158
|
+
entry: NonNullable<ReturnType<StaticCache["lookup"]>>["entry"];
|
|
159
|
+
};
|
|
160
|
+
private readonly failed;
|
|
161
|
+
stopAll(): void;
|
|
162
|
+
}
|