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.
Files changed (106) hide show
  1. package/README.md +139 -0
  2. package/dist/audio.d.ts +70 -1
  3. package/dist/audio.js +84 -5
  4. package/dist/channels.d.ts +107 -3
  5. package/dist/channels.js +151 -3
  6. package/dist/compression/analyze.d.ts +105 -0
  7. package/dist/compression/analyze.js +213 -0
  8. package/dist/compression/blocks.d.ts +35 -0
  9. package/dist/compression/blocks.js +73 -0
  10. package/dist/compression/cli.d.ts +1 -0
  11. package/dist/compression/cli.js +347 -0
  12. package/dist/compression/codec.d.ts +66 -0
  13. package/dist/compression/codec.js +178 -0
  14. package/dist/compression/envelope.d.ts +77 -0
  15. package/dist/compression/envelope.js +191 -0
  16. package/dist/compression/jobs.d.ts +59 -0
  17. package/dist/compression/jobs.js +120 -0
  18. package/dist/compression/metrics.d.ts +62 -0
  19. package/dist/compression/metrics.js +64 -0
  20. package/dist/compression/policy.d.ts +76 -0
  21. package/dist/compression/policy.js +148 -0
  22. package/dist/compression/receiver.d.ts +51 -0
  23. package/dist/compression/receiver.js +92 -0
  24. package/dist/compression/relay.d.ts +134 -0
  25. package/dist/compression/relay.js +430 -0
  26. package/dist/compression/routes.d.ts +25 -0
  27. package/dist/compression/routes.js +266 -0
  28. package/dist/compression/service.d.ts +162 -0
  29. package/dist/compression/service.js +488 -0
  30. package/dist/compression/static.d.ts +65 -0
  31. package/dist/compression/static.js +248 -0
  32. package/dist/compression/store.d.ts +36 -0
  33. package/dist/compression/store.js +119 -0
  34. package/dist/compression/ts-transform.d.ts +44 -0
  35. package/dist/compression/ts-transform.js +148 -0
  36. package/dist/hls.d.ts +36 -3
  37. package/dist/hls.js +85 -11
  38. package/dist/layouts.d.ts +10 -0
  39. package/dist/layouts.js +71 -1
  40. package/dist/live-api.d.ts +16 -2
  41. package/dist/live-api.js +152 -19
  42. package/dist/live-events.d.ts +57 -2
  43. package/dist/live-events.js +243 -11
  44. package/dist/main.js +47 -0
  45. package/dist/mcp.d.ts +40 -0
  46. package/dist/mcp.js +255 -0
  47. package/dist/oauth-api.d.ts +50 -0
  48. package/dist/oauth-api.js +381 -0
  49. package/dist/oauth-server.d.ts +174 -0
  50. package/dist/oauth-server.js +559 -0
  51. package/dist/party.d.ts +32 -0
  52. package/dist/party.js +196 -0
  53. package/dist/playlist.d.ts +11 -0
  54. package/dist/playlist.js +27 -6
  55. package/dist/server.d.ts +37 -0
  56. package/dist/server.js +277 -45
  57. package/dist/sources.d.ts +16 -8
  58. package/dist/sources.js +102 -0
  59. package/dist/tickets.d.ts +67 -0
  60. package/dist/tickets.js +156 -0
  61. package/dist/tokens.d.ts +27 -2
  62. package/dist/tokens.js +42 -1
  63. package/dist/watch-party.d.ts +127 -0
  64. package/dist/watch-party.js +301 -0
  65. package/package.json +1 -1
  66. package/src/audio.ts +119 -6
  67. package/src/channels.ts +177 -7
  68. package/src/compression/analyze.ts +270 -0
  69. package/src/compression/blocks.ts +93 -0
  70. package/src/compression/cli.ts +340 -0
  71. package/src/compression/codec.ts +202 -0
  72. package/src/compression/envelope.ts +237 -0
  73. package/src/compression/jobs.ts +157 -0
  74. package/src/compression/metrics.ts +109 -0
  75. package/src/compression/policy.ts +171 -0
  76. package/src/compression/receiver.ts +107 -0
  77. package/src/compression/relay.ts +462 -0
  78. package/src/compression/routes.ts +294 -0
  79. package/src/compression/service.ts +525 -0
  80. package/src/compression/static.ts +264 -0
  81. package/src/compression/store.ts +126 -0
  82. package/src/compression/ts-transform.ts +148 -0
  83. package/src/hls.ts +100 -10
  84. package/src/layouts.ts +78 -1
  85. package/src/live-api.ts +172 -17
  86. package/src/live-events.ts +269 -11
  87. package/src/main.ts +47 -0
  88. package/src/mcp.ts +282 -0
  89. package/src/oauth-api.ts +481 -0
  90. package/src/oauth-server.ts +653 -0
  91. package/src/party.ts +216 -0
  92. package/src/playlist.ts +28 -5
  93. package/src/server.ts +296 -44
  94. package/src/sources.ts +100 -0
  95. package/src/tickets.ts +195 -0
  96. package/src/tokens.ts +50 -3
  97. package/src/watch-party.ts +390 -0
  98. package/web/dist/assets/{hls-3VKVEQE3-CrILISPJ.js → hls-3VKVEQE3-eV54kXE3.js} +1 -1
  99. package/web/dist/assets/index-CurZFzlH.css +1 -0
  100. package/web/dist/assets/index-DDzutJ75.js +1 -0
  101. package/web/dist/assets/{mpegts-CWeN63eG.js → mpegts-Buc3Odv6.js} +1 -1
  102. package/web/dist/assets/{mpegts-LO6RVLD6-DaMgRvlO.js → mpegts-LO6RVLD6-DcDKPB4P.js} +1 -1
  103. package/web/dist/index.html +59 -6
  104. package/web/dist/sw.js +6 -6
  105. package/web/dist/assets/index-CQ_m5HqS.css +0 -1
  106. package/web/dist/assets/index-CTxPM5KS.js +0 -1
@@ -0,0 +1,430 @@
1
+ /**
2
+ * One compressor per channel, however many receivers.
3
+ *
4
+ * The encoder is a listener on the channel like any other. What it hears
5
+ * goes into blocks, each block is squeezed once on the pool, and the result
6
+ * is written to every relay session -- the payload shared, the frame header
7
+ * stamped per session because each session numbers its own frames. A
8
+ * session that joins late is first sent the channel's opening bytes (the
9
+ * header and the recent backlog, exactly what an ordinary listener gets)
10
+ * compressed for it alone, and then falls in with the shared blocks from
11
+ * the moment it joined. Nothing is sent twice and nothing is skipped: the
12
+ * encoder flushes its half-built block before it records where "the
13
+ * moment it joined" is.
14
+ *
15
+ * Bypass, overflow and slow listeners are all decided here and all bounded.
16
+ * A block that does not compress is sent stored; `auto` stops trying after
17
+ * a run of those and tries again later. A compressor that falls behind by
18
+ * more than the channel's queue limit ends its relays rather than either
19
+ * buffering for ever or stalling the channel. A session whose socket is
20
+ * not draining is cut off, on its own, and the channel never knows.
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ import { Blocker } from "./blocks.js";
24
+ import { decode, encode, PoolError } from "./codec.js";
25
+ import { decodeFrameHeader, decodeStreamHeader, encodeEndFrame, encodeFrameHeader, encodeStreamHeader, FRAME_DATA, FRAME_END, FRAME_HEADER_BYTES, endFrameTotal, RelayError, sha256, STREAM_HEADER_BYTES, } from "./envelope.js";
26
+ import { eligible } from "./policy.js";
27
+ import { TS_PACKET, tsLayout } from "./ts-transform.js";
28
+ /** How many blocks in a row may fail to pay before `auto` stops trying, for a while. */
29
+ export const GIVE_UP_AFTER = 8;
30
+ export class RelayEncoder {
31
+ options;
32
+ attached = false;
33
+ blocker;
34
+ chain = Promise.resolve();
35
+ pushed = 0;
36
+ sessions = new Set();
37
+ ineligibleRun = 0;
38
+ bypassUntil = null;
39
+ closed = false;
40
+ now;
41
+ streamHeader;
42
+ constructor(options) {
43
+ this.options = options;
44
+ this.now = options.now ?? (() => performance.now());
45
+ const { policy } = options;
46
+ this.blocker = new Blocker({
47
+ maxBlockBytes: policy.maxBlockBytes,
48
+ maxHoldMs: policy.maxHoldMs,
49
+ align: policy.tsAware ? TS_PACKET : 0,
50
+ onBlock: (block) => this.onBlock(block),
51
+ });
52
+ this.streamHeader = encodeStreamHeader({
53
+ version: 1,
54
+ boundary: options.boundary,
55
+ generation: options.generation,
56
+ maxFrameBytes: policy.maxBlockBytes,
57
+ });
58
+ options.metrics.reset(options.generation);
59
+ options.metrics.fallbackReason = null;
60
+ }
61
+ get generation() {
62
+ return this.options.generation;
63
+ }
64
+ get sessionCount() {
65
+ return this.sessions.size;
66
+ }
67
+ /**
68
+ * Call once `Channel.listen` has returned. What it wrote before then was
69
+ * the opening bytes, which every session gets for itself as its preface;
70
+ * taking them here too would send them twice to the first session.
71
+ */
72
+ attach() {
73
+ this.attached = true;
74
+ }
75
+ /** The channel's listener face. */
76
+ write(chunk) {
77
+ if (!this.attached || this.closed)
78
+ return true;
79
+ this.options.metrics.inputBytes += chunk.length;
80
+ this.blocker.push(chunk);
81
+ return true;
82
+ }
83
+ /** The channel ended, or started over: finish every session cleanly. */
84
+ end() {
85
+ if (this.closed)
86
+ return;
87
+ this.closed = true;
88
+ this.blocker.end();
89
+ this.chain = this.chain.then(() => {
90
+ for (const session of [...this.sessions])
91
+ session.finish();
92
+ this.options.onAbort("ended");
93
+ });
94
+ }
95
+ /**
96
+ * A new receiver. `preface` is what the channel would write first to an
97
+ * ordinary listener, snapshotted by the caller in the same tick as this
98
+ * call, so that the shared blocks from here on follow it exactly.
99
+ */
100
+ join(listener, preface) {
101
+ // Whatever is half-built is pre-join and is in the preface's backlog
102
+ // already: out with it before the join point is marked.
103
+ this.blocker.flush();
104
+ const session = new RelaySession(listener, this, this.pushed, this.options.policy.maxListenerQueueBytes, (gone, reason) => {
105
+ this.sessions.delete(gone);
106
+ this.options.metrics.listeners = this.sessions.size;
107
+ if (reason !== null) {
108
+ this.options.metrics.droppedListeners += 1;
109
+ this.options.metrics.fallbackReason = reason;
110
+ }
111
+ if (this.sessions.size === 0 && !this.closed)
112
+ this.options.onIdle();
113
+ });
114
+ this.sessions.add(session);
115
+ this.options.metrics.listeners = this.sessions.size;
116
+ // The join point is marked now; the writing starts a tick later, so
117
+ // the caller can put response headers in front of the first byte.
118
+ // No shared block can arrive in between: every block goes through the
119
+ // pool first, and the pool answers on a later tick than this one.
120
+ queueMicrotask(() => void session.start(preface));
121
+ return session;
122
+ }
123
+ onBlock(block) {
124
+ const start = this.pushed;
125
+ this.pushed += block.length;
126
+ const metrics = this.options.metrics;
127
+ metrics.queueBytes += block.length;
128
+ if (metrics.queueBytes > this.options.policy.maxChannelQueueBytes) {
129
+ this.abort("processing budget exceeded");
130
+ return;
131
+ }
132
+ const entered = this.now();
133
+ this.chain = this.chain.then(async () => {
134
+ if (this.closed && this.sessions.size === 0)
135
+ return;
136
+ const { mode, payload } = await this.encodeBytes(block);
137
+ metrics.queueBytes -= block.length;
138
+ metrics.latency(this.now() - entered);
139
+ metrics.blocks += 1;
140
+ metrics.representationBytes += payload.length;
141
+ if (mode === "stored")
142
+ metrics.storedBlocks += 1;
143
+ else
144
+ metrics.compressedBlocks += 1;
145
+ metrics.activeMode = mode;
146
+ const encoded = { start, mode, original: block, digest: sha256(block), payload };
147
+ for (const session of [...this.sessions])
148
+ session.deliver(encoded);
149
+ }).catch(() => {
150
+ // A codec that threw something other than a pool refusal is a codec
151
+ // that cannot be trusted with the next block either. Stop the relays;
152
+ // the channel itself is untouched, and a receiver reconnects.
153
+ metrics.codecFailures += 1;
154
+ this.abort("processing budget exceeded");
155
+ });
156
+ }
157
+ /**
158
+ * The bytes in the mode the policy picks: compressed when that pays,
159
+ * stored when it does not or cannot be done in time.
160
+ */
161
+ async encodeBytes(bytes) {
162
+ const policy = this.options.policy;
163
+ const metrics = this.options.metrics;
164
+ const stored = { mode: "stored", payload: bytes };
165
+ if (policy.mode === "off")
166
+ return stored;
167
+ if (policy.mode === "auto" && this.bypassUntil !== null) {
168
+ if (this.now() < this.bypassUntil)
169
+ return stored;
170
+ this.bypassUntil = null;
171
+ this.ineligibleRun = 0;
172
+ metrics.bypassed = false;
173
+ metrics.bypassUntil = null;
174
+ }
175
+ const candidates = ["zstd"];
176
+ if (policy.tsAware && tsLayout(bytes)?.packetSize === TS_PACKET)
177
+ candidates.push("ts-zstd");
178
+ let results;
179
+ try {
180
+ results = await Promise.all(candidates.map((mode) => this.options.pool.run(() => encode(mode, bytes, policy.zstdLevel))));
181
+ }
182
+ catch (error) {
183
+ metrics.codecFailures += 1;
184
+ metrics.fallbackReason = "processing budget exceeded";
185
+ if (!(error instanceof PoolError) && !(error instanceof RelayError))
186
+ throw error;
187
+ return stored;
188
+ }
189
+ let best = 0;
190
+ for (let i = 1; i < results.length; i += 1)
191
+ if (results[i].length < results[best].length)
192
+ best = i;
193
+ const payload = results[best];
194
+ if (eligible(bytes.length, payload.length, policy)) {
195
+ this.ineligibleRun = 0;
196
+ return { mode: candidates[best], payload };
197
+ }
198
+ this.ineligibleRun += 1;
199
+ if (policy.mode === "auto" && this.ineligibleRun >= GIVE_UP_AFTER) {
200
+ this.bypassUntil = this.now() + policy.resampleAfterMs;
201
+ metrics.bypassed = true;
202
+ metrics.bypassUntil = Date.now() + policy.resampleAfterMs;
203
+ metrics.fallbackReason = "already efficiently compressed";
204
+ }
205
+ return stored;
206
+ }
207
+ abort(reason) {
208
+ if (this.closed)
209
+ return;
210
+ this.closed = true;
211
+ this.options.metrics.fallbackReason = reason;
212
+ for (const session of [...this.sessions])
213
+ session.close(reason);
214
+ this.options.onAbort(reason);
215
+ }
216
+ }
217
+ /** One receiver's view of the relay: its own frame numbers, its own end marker. */
218
+ export class RelaySession {
219
+ listener;
220
+ encoder;
221
+ from;
222
+ maxQueueBytes;
223
+ onClose;
224
+ seq = 0;
225
+ total = 0;
226
+ digest = createHash("sha256");
227
+ queue = [];
228
+ queuedBytes = 0;
229
+ ready = false;
230
+ closed = false;
231
+ constructor(listener, encoder, from, maxQueueBytes, onClose) {
232
+ this.listener = listener;
233
+ this.encoder = encoder;
234
+ this.from = from;
235
+ this.maxQueueBytes = maxQueueBytes;
236
+ this.onClose = onClose;
237
+ }
238
+ async start(preface) {
239
+ if (!this.put(this.encoder.streamHeader))
240
+ return;
241
+ for (const piece of preface) {
242
+ for (let at = 0; at < piece.length && !this.closed; at += this.maxBlockBytes) {
243
+ const block = piece.subarray(at, Math.min(piece.length, at + this.maxBlockBytes));
244
+ const { mode, payload } = await this.encoder.encodeBytes(block);
245
+ this.send(mode, block, sha256(block), payload);
246
+ }
247
+ }
248
+ if (this.closed)
249
+ return;
250
+ this.ready = true;
251
+ const queued = this.queue;
252
+ this.queue = [];
253
+ this.queuedBytes = 0;
254
+ for (const block of queued) {
255
+ if (this.closed)
256
+ return;
257
+ this.send(block.mode, block.original, block.digest, block.payload);
258
+ }
259
+ }
260
+ get maxBlockBytes() {
261
+ return this.encoder.streamHeader.readUInt32BE(12);
262
+ }
263
+ deliver(block) {
264
+ if (this.closed || block.start < this.from)
265
+ return;
266
+ if (!this.ready) {
267
+ this.queue.push(block);
268
+ this.queuedBytes += block.original.length;
269
+ if (this.queuedBytes > this.maxQueueBytes)
270
+ this.close("slow listener");
271
+ return;
272
+ }
273
+ this.send(block.mode, block.original, block.digest, block.payload);
274
+ }
275
+ send(mode, original, digest, payload) {
276
+ if (this.closed)
277
+ return;
278
+ const head = encodeFrameHeader({
279
+ type: FRAME_DATA,
280
+ mode,
281
+ seq: this.seq,
282
+ originalLength: original.length,
283
+ encodedLength: payload.length,
284
+ sha256: digest,
285
+ });
286
+ this.seq += 1;
287
+ this.total += original.length;
288
+ this.digest.update(original);
289
+ if (!this.put(head) || !this.put(payload))
290
+ return;
291
+ const behind = this.listener.pending?.() ?? 0;
292
+ if (behind > this.maxQueueBytes)
293
+ this.close("slow listener");
294
+ }
295
+ /** Clean end: the end frame, then the socket. */
296
+ finish() {
297
+ if (this.closed)
298
+ return;
299
+ this.put(encodeEndFrame(this.seq, this.total, this.digest.digest()));
300
+ this.closed = true;
301
+ try {
302
+ this.listener.end();
303
+ }
304
+ catch {
305
+ // Gone already.
306
+ }
307
+ this.onClose(this, null);
308
+ }
309
+ close(reason) {
310
+ if (this.closed)
311
+ return;
312
+ this.closed = true;
313
+ try {
314
+ this.listener.end();
315
+ }
316
+ catch {
317
+ // Gone already.
318
+ }
319
+ this.onClose(this, reason);
320
+ }
321
+ /** The receiver hung up. Not a fault of anybody's; nothing is counted against it. */
322
+ leave() {
323
+ if (this.closed)
324
+ return;
325
+ this.closed = true;
326
+ this.onClose(this, null);
327
+ }
328
+ put(bytes) {
329
+ try {
330
+ this.listener.write(bytes);
331
+ return true;
332
+ }
333
+ catch {
334
+ this.close("slow listener");
335
+ return false;
336
+ }
337
+ }
338
+ }
339
+ /**
340
+ * The receiving end: bytes in, original bytes out, every rule checked on
341
+ * the way. Frames are validated before their payload is read, payloads are
342
+ * decoded under the size the frame promised, and the result is checked
343
+ * against the length and the digest before it is handed on. A stream that
344
+ * stops without its end frame is reported as cut off by `end()`.
345
+ */
346
+ export class RelayDecoder {
347
+ options;
348
+ header = null;
349
+ frames = 0;
350
+ bytes = 0;
351
+ ended = false;
352
+ buffer = Buffer.alloc(0);
353
+ seq = 0;
354
+ pending = null;
355
+ digest = createHash("sha256");
356
+ busy = Promise.resolve();
357
+ constructor(options) {
358
+ this.options = options;
359
+ }
360
+ /** Feed bytes as they arrive. Serialised, so callers need not await between chunks, though they may. */
361
+ feed(chunk) {
362
+ this.busy = this.busy.then(() => this.consume(chunk));
363
+ return this.busy;
364
+ }
365
+ async consume(chunk) {
366
+ if (this.ended)
367
+ throw new RelayError("AFTER_END", "bytes after the end frame");
368
+ this.buffer = this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
369
+ for (;;) {
370
+ if (this.header === null) {
371
+ if (this.buffer.length < STREAM_HEADER_BYTES)
372
+ return;
373
+ const header = decodeStreamHeader(this.buffer);
374
+ const cap = this.options.maxFrameBytes ?? Infinity;
375
+ if (header.maxFrameBytes > cap)
376
+ throw new RelayError("BAD_LIMIT", `stream asks for ${header.maxFrameBytes}-byte frames; this receiver allows ${cap}`);
377
+ if (this.options.boundary && header.boundary !== this.options.boundary) {
378
+ throw new RelayError("BAD_BOUNDARY", `stream is at the ${header.boundary} boundary, not ${this.options.boundary}`);
379
+ }
380
+ this.header = header;
381
+ this.buffer = this.buffer.subarray(STREAM_HEADER_BYTES);
382
+ continue;
383
+ }
384
+ if (this.pending === null) {
385
+ if (this.buffer.length < FRAME_HEADER_BYTES)
386
+ return;
387
+ this.pending = decodeFrameHeader(this.buffer, {
388
+ maxFrameBytes: this.header.maxFrameBytes,
389
+ modes: this.options.modes,
390
+ expectSeq: this.seq,
391
+ });
392
+ this.buffer = this.buffer.subarray(FRAME_HEADER_BYTES);
393
+ if (this.pending.type === FRAME_END) {
394
+ const total = endFrameTotal(this.pending);
395
+ if (total !== this.bytes)
396
+ throw new RelayError("LENGTH_MISMATCH", `end frame says ${total} bytes, received ${this.bytes}`);
397
+ if (!this.digest.digest().equals(this.pending.sha256))
398
+ throw new RelayError("CHECKSUM_MISMATCH", "the stream digest does not match");
399
+ this.ended = true;
400
+ this.seq += 1;
401
+ if (this.buffer.length > 0)
402
+ throw new RelayError("AFTER_END", "bytes after the end frame");
403
+ return;
404
+ }
405
+ }
406
+ const frame = this.pending;
407
+ if (this.buffer.length < frame.encodedLength)
408
+ return;
409
+ const payload = this.buffer.subarray(0, frame.encodedLength);
410
+ this.buffer = Buffer.from(this.buffer.subarray(frame.encodedLength));
411
+ const original = await decode(frame.mode, payload, frame.originalLength);
412
+ if (original.length !== frame.originalLength)
413
+ throw new RelayError("LENGTH_MISMATCH", `frame ${frame.seq} decoded to ${original.length} bytes, not ${frame.originalLength}`);
414
+ if (!sha256(original).equals(frame.sha256))
415
+ throw new RelayError("CHECKSUM_MISMATCH", `frame ${frame.seq} does not match its digest`);
416
+ this.pending = null;
417
+ this.seq += 1;
418
+ this.frames += 1;
419
+ this.bytes += original.length;
420
+ this.digest.update(original);
421
+ await this.options.onBytes(original);
422
+ }
423
+ }
424
+ /** The connection closed. Fine after the end frame; a cut-off otherwise. */
425
+ async end() {
426
+ await this.busy;
427
+ if (!this.ended)
428
+ throw new RelayError("TRUNCATED", `the stream stopped after ${this.frames} frames without an end marker`);
429
+ }
430
+ }
@@ -0,0 +1,25 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { CompressionService } from "./service.ts";
3
+ export interface RouteContext {
4
+ service: CompressionService;
5
+ json: (response: ServerResponse, code: number, body: unknown) => void;
6
+ readBody: (request: IncomingMessage) => Promise<string>;
7
+ /** Whether the caller holds the controls, as opposed to a listening link. */
8
+ controls: () => Promise<boolean>;
9
+ /** Count a streaming response in the connections view. */
10
+ watch: (request: IncomingMessage, response: ServerResponse, track: string) => void;
11
+ cors: Record<string, string>;
12
+ }
13
+ /**
14
+ * Routes under /api/channels/:id/. True when the request was answered.
15
+ * `action` and `file` are the path segments after the id.
16
+ */
17
+ export declare function handleChannelCompression(request: IncomingMessage, response: ServerResponse, ctx: RouteContext, id: string, action: string | undefined, file: string | undefined): Promise<boolean>;
18
+ /** Routes under /api/compression. True when the request was answered. */
19
+ export declare function handleCompressionApi(request: IncomingMessage, response: ServerResponse, ctx: RouteContext, path: string): Promise<boolean>;
20
+ /**
21
+ * The static representation of a library file, at /api/media/N/relay.
22
+ * The caller has already resolved N to a path and checked that media may
23
+ * be streamed at all.
24
+ */
25
+ export declare function handleStaticRelay(request: IncomingMessage, response: ServerResponse, ctx: RouteContext, file: string, title: string): void;