nixamp 0.17.0 → 0.18.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/dist/channels.d.ts +54 -1
- package/dist/channels.js +160 -23
- package/dist/compression/benchmark.d.ts +97 -0
- package/dist/compression/benchmark.js +241 -0
- package/dist/compression/cli.js +59 -1
- package/dist/compression/receiver.d.ts +25 -5
- package/dist/compression/receiver.js +54 -5
- package/dist/compression/routes.js +7 -2
- package/dist/compression/service.d.ts +18 -6
- package/dist/compression/service.js +110 -19
- package/dist/compression/source.d.ts +17 -0
- package/dist/compression/source.js +131 -0
- package/dist/links.js +14 -3
- package/dist/live-api.js +1 -0
- package/dist/live-events.js +11 -3
- package/dist/owner.js +10 -0
- package/dist/server.js +118 -10
- package/dist/share.js +6 -0
- package/package.json +1 -1
- package/src/channels.ts +180 -22
- package/src/compression/benchmark.ts +297 -0
- package/src/compression/cli.ts +56 -1
- package/src/compression/receiver.ts +68 -7
- package/src/compression/routes.ts +7 -2
- package/src/compression/service.ts +121 -24
- package/src/compression/source.ts +130 -0
- package/src/links.ts +12 -3
- package/src/live-api.ts +1 -0
- package/src/live-events.ts +14 -3
- package/src/owner.ts +8 -0
- package/src/server.ts +113 -11
- package/src/share.ts +5 -0
- package/web/dist/assets/{hls-3VKVEQE3-eV54kXE3.js → hls-3VKVEQE3-B2kl0-z1.js} +1 -1
- package/web/dist/assets/{index-CurZFzlH.css → index-D1QxpRmE.css} +1 -1
- package/web/dist/assets/index-DF6O2leR.js +1 -0
- package/web/dist/assets/{mpegts-Buc3Odv6.js → mpegts-4pBNK_Yr.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-DcDKPB4P.js → mpegts-LO6RVLD6-B4RLM-_e.js} +1 -1
- package/web/dist/index.html +15 -10
- package/web/dist/sw.js +6 -6
- package/web/dist/assets/index-DDzutJ75.js +0 -1
package/dist/channels.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export interface ChannelInfo {
|
|
|
64
64
|
* a member may have on at once.
|
|
65
65
|
*/
|
|
66
66
|
startedBy?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Set when nixamp reads the source itself and hands ffmpeg the bytes down
|
|
69
|
+
* a pipe, so the original bytes pass through this process and can be
|
|
70
|
+
* relayed exactly as they came. Only a transport stream from a file or a
|
|
71
|
+
* plain URL is read this way, and only when a policy asks for it.
|
|
72
|
+
*/
|
|
73
|
+
teed?: boolean;
|
|
67
74
|
}
|
|
68
75
|
/** Where a pulled source is picked up from, and whether it can be at all. */
|
|
69
76
|
export interface PullResume {
|
|
@@ -72,6 +79,22 @@ export interface PullResume {
|
|
|
72
79
|
/** Seconds into a film to start from. */
|
|
73
80
|
position: number;
|
|
74
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* A source read by us rather than by ffmpeg: what to tell ffmpeg it is,
|
|
84
|
+
* and how to open it. Opened once per dial; the signal is pulled when that
|
|
85
|
+
* dial is over.
|
|
86
|
+
*/
|
|
87
|
+
export interface PullThrough {
|
|
88
|
+
/** ffmpeg's name for the container, e.g. `mpegts`. */
|
|
89
|
+
format: string;
|
|
90
|
+
open(signal: AbortSignal): Promise<AsyncIterable<Uint8Array>>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Asked at every dial whether this source should be read through us. Null
|
|
94
|
+
* means ffmpeg dials it as it always has. `from` is where a film is being
|
|
95
|
+
* picked up from; a source read through us cannot be joined mid-way.
|
|
96
|
+
*/
|
|
97
|
+
export type ThroughProvider = (info: ChannelInfo, from: number, input: string[], audio: string) => PullThrough | null;
|
|
75
98
|
/**
|
|
76
99
|
* How far back of the saved place a film is picked up from, in seconds. The
|
|
77
100
|
* place is written down every so often and a restart lands between two
|
|
@@ -144,6 +167,8 @@ export interface ChannelOptions {
|
|
|
144
167
|
maxListenerQueueBytes?: number;
|
|
145
168
|
/** How long a rate is measured over before the backlog is sized off it. Tests shorten it. */
|
|
146
169
|
rateWindowMs?: number;
|
|
170
|
+
/** Whether a pulled source is read here and piped to ffmpeg. See `Channels.setThrough`. */
|
|
171
|
+
through?: ThroughProvider;
|
|
147
172
|
}
|
|
148
173
|
/**
|
|
149
174
|
* One live source, and its audience.
|
|
@@ -192,6 +217,10 @@ export declare class Channel {
|
|
|
192
217
|
*/
|
|
193
218
|
ephemeral: boolean;
|
|
194
219
|
private idle;
|
|
220
|
+
/** Whoever wants the source's own bytes, when the source is read through us. */
|
|
221
|
+
private readonly sourceTaps;
|
|
222
|
+
/** Pulls the plug on the current read-through, when there is one. */
|
|
223
|
+
private throughAbort;
|
|
195
224
|
constructor(info: ChannelInfo, options: ChannelOptions, onGone: (id: string) => void);
|
|
196
225
|
start(format: string): void;
|
|
197
226
|
/**
|
|
@@ -230,6 +259,8 @@ export declare class Channel {
|
|
|
230
259
|
* new beginning as it is written, rather than a stale one first.
|
|
231
260
|
*/
|
|
232
261
|
private startOver;
|
|
262
|
+
/** Everybody tapping the source is told it ended. */
|
|
263
|
+
private endTaps;
|
|
233
264
|
/** Expect output within STALL, or treat the source as gone and dial again. */
|
|
234
265
|
private rearm;
|
|
235
266
|
/** End everybody listening; the stream they were on is over. */
|
|
@@ -298,6 +329,21 @@ export declare class Channel {
|
|
|
298
329
|
* own ffmpeg is dialled again, and a newcomer gets the new beginning.
|
|
299
330
|
*/
|
|
300
331
|
rollover(): void;
|
|
332
|
+
/**
|
|
333
|
+
* Hear the source's own bytes, as they go down the pipe to ffmpeg. Only
|
|
334
|
+
* a channel read through us has any; for the rest this attaches nothing
|
|
335
|
+
* and returns null. Ended, like a listener, when the source starts over.
|
|
336
|
+
*/
|
|
337
|
+
tapSource(tap: Listener): (() => void) | null;
|
|
338
|
+
private tap;
|
|
339
|
+
/**
|
|
340
|
+
* Read the source and write it to this ffmpeg's stdin, at the rate ffmpeg
|
|
341
|
+
* takes it. Every chunk is handed to the taps first, so a tap sees exactly
|
|
342
|
+
* the bytes ffmpeg does. When the source ends, stdin is ended, ffmpeg
|
|
343
|
+
* finishes, and its close handler dials again -- the same path a source
|
|
344
|
+
* that ffmpeg read itself takes when it drops.
|
|
345
|
+
*/
|
|
346
|
+
private feedThrough;
|
|
301
347
|
listen(listener: Listener): () => void;
|
|
302
348
|
/** Stay up with nobody watching: no longer on demand. */
|
|
303
349
|
keep(): void;
|
|
@@ -337,7 +383,7 @@ export declare class Channels {
|
|
|
337
383
|
* about it is the same too, which is the point -- a re-stream stops being a
|
|
338
384
|
* special case and becomes one more thing that is on.
|
|
339
385
|
*/
|
|
340
|
-
pull(id: string, name: string, source: string, encode: string[], kind: "audio" | "video", paced?: boolean, stall?: number, input?: string[], audio?: string, resume?: PullResume): Channel | null;
|
|
386
|
+
pull(id: string, name: string, source: string, encode: string[], kind: "audio" | "video", paced?: boolean, stall?: number, input?: string[], audio?: string, resume?: PullResume, codecs?: ChannelInfo["codecs"]): Channel | null;
|
|
341
387
|
/**
|
|
342
388
|
* Dial a pulled channel's source again, now. False for a channel that is
|
|
343
389
|
* not there or is not ours to dial: a publisher's stream restarts at the
|
|
@@ -382,6 +428,13 @@ export declare class Channels {
|
|
|
382
428
|
relayIn(id: string, name: string, kind: "audio" | "video", source: string): Channel | null;
|
|
383
429
|
/** What a new listener would be written first, for a relay's preface. */
|
|
384
430
|
opening(id: string): Buffer[];
|
|
431
|
+
/** Hear a channel's source bytes, when it is read through us. Null otherwise. */
|
|
432
|
+
tapSource(id: string, tap: Listener): (() => void) | null;
|
|
433
|
+
/**
|
|
434
|
+
* Who decides whether a pulled source is read here and piped to ffmpeg.
|
|
435
|
+
* Set once by whoever owns the policies; asked at every dial.
|
|
436
|
+
*/
|
|
437
|
+
setThrough(provider: ThroughProvider | null): void;
|
|
385
438
|
/** The kind of a channel, for a relay to say what it is carrying. */
|
|
386
439
|
kindOf(id: string): "audio" | "video" | undefined;
|
|
387
440
|
stop(id: string): boolean;
|
package/dist/channels.js
CHANGED
|
@@ -163,6 +163,10 @@ export class Channel {
|
|
|
163
163
|
*/
|
|
164
164
|
ephemeral = false;
|
|
165
165
|
idle = null;
|
|
166
|
+
/** Whoever wants the source's own bytes, when the source is read through us. */
|
|
167
|
+
sourceTaps = new Set();
|
|
168
|
+
/** Pulls the plug on the current read-through, when there is one. */
|
|
169
|
+
throughAbort = null;
|
|
166
170
|
constructor(info, options, onGone) {
|
|
167
171
|
this.info = info;
|
|
168
172
|
this.options = options;
|
|
@@ -236,6 +240,15 @@ export class Channel {
|
|
|
236
240
|
// and a film that has barely started is started.
|
|
237
241
|
const from = resume.live ? 0 : Math.max(0, Math.floor((this.info.position ?? 0) - REWIND));
|
|
238
242
|
const seek = from > 0 ? ["-ss", String(from)] : [];
|
|
243
|
+
// Read the source here rather than in ffmpeg, when a policy wants the
|
|
244
|
+
// original bytes and the source is the kind that can be. ffmpeg then
|
|
245
|
+
// reads a pipe, and every byte that goes down it is also handed to
|
|
246
|
+
// whoever has tapped the source. Asked again at every dial, so a
|
|
247
|
+
// policy set after the channel started applies at its next restart.
|
|
248
|
+
this.throughAbort?.abort();
|
|
249
|
+
this.throughAbort = null;
|
|
250
|
+
const through = this.options.through?.(this.info, from, input, audio) ?? null;
|
|
251
|
+
this.info.teed = through !== null;
|
|
239
252
|
const child = spawn(command, [
|
|
240
253
|
...prefix,
|
|
241
254
|
"-hide_banner",
|
|
@@ -245,33 +258,52 @@ export class Channel {
|
|
|
245
258
|
// to. Not stderr, which is for what went wrong.
|
|
246
259
|
"-progress", "pipe:3",
|
|
247
260
|
"-stats_period", "1",
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
261
|
+
...(through
|
|
262
|
+
? [
|
|
263
|
+
// Stated, as for a publisher: ffmpeg mis-probes a pipe. Paced
|
|
264
|
+
// the same way, since a pipe is read as fast as it is written.
|
|
265
|
+
// The source's own input tuning still applies -- a transport
|
|
266
|
+
// stream read from a pipe needs the same probe depth and
|
|
267
|
+
// generated timestamps it would off a socket -- but request
|
|
268
|
+
// headers, which only a source read through us leaves out,
|
|
269
|
+
// are no use to a pipe and are not here (a source that needs
|
|
270
|
+
// them is not read through us in the first place).
|
|
271
|
+
...input,
|
|
272
|
+
"-f", through.format,
|
|
273
|
+
...(paced ? ["-re"] : []),
|
|
274
|
+
"-i", "pipe:0",
|
|
275
|
+
]
|
|
276
|
+
: [
|
|
277
|
+
// A dropped source is normal over hours, and a channel that dies
|
|
278
|
+
// the first time a CDN hiccups is not a channel anybody can rely
|
|
279
|
+
// on. ffmpeg redials on its own before we have to.
|
|
280
|
+
// A connection that stops answering is an error after this long,
|
|
281
|
+
// and an error is a thing the reconnect knows what to do with.
|
|
282
|
+
// Without it a silent socket is waited on for ever. In
|
|
283
|
+
// microseconds, as ffmpeg wants it.
|
|
284
|
+
...remoteArgs,
|
|
285
|
+
// Real time, always. A file read as fast as the disk allows is an
|
|
286
|
+
// hour of film in ninety seconds and a room that cannot be in it
|
|
287
|
+
// together; a live source is already paced and loses nothing.
|
|
288
|
+
...(paced ? ["-re"] : []),
|
|
289
|
+
// What the source's site expects on the request: a user agent, a
|
|
290
|
+
// referer, a cookie. A link resolved by yt-dlp comes with these,
|
|
291
|
+
// and a CDN that got them from yt-dlp and not from us answers 403.
|
|
292
|
+
...input,
|
|
293
|
+
...seek,
|
|
294
|
+
"-i", source,
|
|
295
|
+
// The sound, when the site keeps it apart from the picture: a
|
|
296
|
+
// second input, dialled the same way, that the encode maps in.
|
|
297
|
+
...(audio ? [...remoteArgs, ...(paced ? ["-re"] : []), ...input, ...seek, "-i", audio] : []),
|
|
298
|
+
]),
|
|
269
299
|
...encode,
|
|
270
300
|
"pipe:1",
|
|
271
|
-
], { stdio: ["ignore", "pipe", "pipe", "pipe"] });
|
|
301
|
+
], { stdio: [through ? "pipe" : "ignore", "pipe", "pipe", "pipe"] });
|
|
272
302
|
let sent = false;
|
|
273
303
|
this.child = child;
|
|
274
304
|
this.rearm(child);
|
|
305
|
+
if (through)
|
|
306
|
+
void this.feedThrough(child, through);
|
|
275
307
|
// ffmpeg's progress: key=value lines, out_time_us being how much it
|
|
276
308
|
// has written, from where it was told to start. Read whole lines,
|
|
277
309
|
// since a chunk can end mid-number. Drained whatever it says, for
|
|
@@ -363,6 +395,22 @@ export class Channel {
|
|
|
363
395
|
this.rateBytes = 0;
|
|
364
396
|
this.rate = 0;
|
|
365
397
|
this.hangUp();
|
|
398
|
+
// The source's own bytes start over too: a new dial is a new stream
|
|
399
|
+
// from its beginning, and whoever was tapping it must not be handed the
|
|
400
|
+
// new beginning after the old middle.
|
|
401
|
+
this.endTaps();
|
|
402
|
+
}
|
|
403
|
+
/** Everybody tapping the source is told it ended. */
|
|
404
|
+
endTaps() {
|
|
405
|
+
for (const tap of this.sourceTaps) {
|
|
406
|
+
try {
|
|
407
|
+
tap.end();
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Gone already.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
this.sourceTaps.clear();
|
|
366
414
|
}
|
|
367
415
|
/** Expect output within STALL, or treat the source as gone and dial again. */
|
|
368
416
|
rearm(child) {
|
|
@@ -572,6 +620,75 @@ export class Channel {
|
|
|
572
620
|
this.info.redials = (this.info.redials ?? 0) + 1;
|
|
573
621
|
this.startOver();
|
|
574
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* Hear the source's own bytes, as they go down the pipe to ffmpeg. Only
|
|
625
|
+
* a channel read through us has any; for the rest this attaches nothing
|
|
626
|
+
* and returns null. Ended, like a listener, when the source starts over.
|
|
627
|
+
*/
|
|
628
|
+
tapSource(tap) {
|
|
629
|
+
if (!this.info.teed || this.closing)
|
|
630
|
+
return null;
|
|
631
|
+
this.sourceTaps.add(tap);
|
|
632
|
+
return () => {
|
|
633
|
+
this.sourceTaps.delete(tap);
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
tap(chunk) {
|
|
637
|
+
for (const tap of this.sourceTaps) {
|
|
638
|
+
try {
|
|
639
|
+
tap.write(chunk);
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
this.sourceTaps.delete(tap);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Read the source and write it to this ffmpeg's stdin, at the rate ffmpeg
|
|
648
|
+
* takes it. Every chunk is handed to the taps first, so a tap sees exactly
|
|
649
|
+
* the bytes ffmpeg does. When the source ends, stdin is ended, ffmpeg
|
|
650
|
+
* finishes, and its close handler dials again -- the same path a source
|
|
651
|
+
* that ffmpeg read itself takes when it drops.
|
|
652
|
+
*/
|
|
653
|
+
async feedThrough(child, through) {
|
|
654
|
+
const controller = new AbortController();
|
|
655
|
+
this.throughAbort = controller;
|
|
656
|
+
const stdin = child.stdin;
|
|
657
|
+
if (!stdin)
|
|
658
|
+
return;
|
|
659
|
+
stdin.on("error", () => undefined);
|
|
660
|
+
try {
|
|
661
|
+
const body = await through.open(controller.signal);
|
|
662
|
+
for await (const raw of body) {
|
|
663
|
+
if (this.child !== child || this.closing || controller.signal.aborted)
|
|
664
|
+
break;
|
|
665
|
+
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
666
|
+
this.tap(chunk);
|
|
667
|
+
if (!stdin.write(chunk)) {
|
|
668
|
+
// Wait for ffmpeg to take it, or for the pipe to go: a pipe that
|
|
669
|
+
// closed never drains, and waiting on it would hold the read open.
|
|
670
|
+
await new Promise((done) => {
|
|
671
|
+
stdin.once("drain", done);
|
|
672
|
+
stdin.once("close", done);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (this.child === child && !controller.signal.aborted)
|
|
679
|
+
this.info.error = error.message;
|
|
680
|
+
}
|
|
681
|
+
finally {
|
|
682
|
+
try {
|
|
683
|
+
stdin.end();
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
// Already gone.
|
|
687
|
+
}
|
|
688
|
+
if (this.throughAbort === controller)
|
|
689
|
+
this.throughAbort = null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
575
692
|
listen(listener) {
|
|
576
693
|
// What the stream is, before any of what it is currently saying. Without
|
|
577
694
|
// this a listener who arrives after the first second gets fragments that
|
|
@@ -645,6 +762,9 @@ export class Channel {
|
|
|
645
762
|
this.info.error = said;
|
|
646
763
|
const child = this.child;
|
|
647
764
|
this.child = null;
|
|
765
|
+
this.throughAbort?.abort();
|
|
766
|
+
this.throughAbort = null;
|
|
767
|
+
this.endTaps();
|
|
648
768
|
try {
|
|
649
769
|
child?.stdin?.end();
|
|
650
770
|
}
|
|
@@ -731,7 +851,7 @@ export class Channels {
|
|
|
731
851
|
* about it is the same too, which is the point -- a re-stream stops being a
|
|
732
852
|
* special case and becomes one more thing that is on.
|
|
733
853
|
*/
|
|
734
|
-
pull(id, name, source, encode, kind, paced = true, stall = STALL, input = [], audio = "", resume = { live: true, position: 0 }) {
|
|
854
|
+
pull(id, name, source, encode, kind, paced = true, stall = STALL, input = [], audio = "", resume = { live: true, position: 0 }, codecs) {
|
|
735
855
|
if (this.open.has(id))
|
|
736
856
|
return null;
|
|
737
857
|
const channel = new Channel({
|
|
@@ -744,6 +864,9 @@ export class Channels {
|
|
|
744
864
|
listeners: 0,
|
|
745
865
|
kind,
|
|
746
866
|
source,
|
|
867
|
+
// Known before the first dial, so whether to read the source here
|
|
868
|
+
// can be decided from what it holds.
|
|
869
|
+
...(codecs ? { codecs } : {}),
|
|
747
870
|
}, this.options, (gone) => this.open.delete(gone));
|
|
748
871
|
this.open.set(id, channel);
|
|
749
872
|
channel.pull(source, encode, paced, stall, input, audio, resume);
|
|
@@ -841,6 +964,20 @@ export class Channels {
|
|
|
841
964
|
opening(id) {
|
|
842
965
|
return this.open.get(id)?.opening() ?? [];
|
|
843
966
|
}
|
|
967
|
+
/** Hear a channel's source bytes, when it is read through us. Null otherwise. */
|
|
968
|
+
tapSource(id, tap) {
|
|
969
|
+
return this.open.get(id)?.tapSource(tap) ?? null;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Who decides whether a pulled source is read here and piped to ffmpeg.
|
|
973
|
+
* Set once by whoever owns the policies; asked at every dial.
|
|
974
|
+
*/
|
|
975
|
+
setThrough(provider) {
|
|
976
|
+
if (provider)
|
|
977
|
+
this.options.through = provider;
|
|
978
|
+
else
|
|
979
|
+
delete this.options.through;
|
|
980
|
+
}
|
|
844
981
|
/** The kind of a channel, for a relay to say what it is carrying. */
|
|
845
982
|
kindOf(id) {
|
|
846
983
|
return this.open.get(id)?.info.kind;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type BenchRow } from "./analyze.ts";
|
|
2
|
+
/** The report schema version, bumped when the shape below changes. */
|
|
3
|
+
export declare const REPORT_SCHEMA = 1;
|
|
4
|
+
export interface Sample {
|
|
5
|
+
name: string;
|
|
6
|
+
/** synthetic bytes we generated, or a real file the runner supplied. */
|
|
7
|
+
kind: "synthetic" | "real";
|
|
8
|
+
bytes: Buffer;
|
|
9
|
+
/** A word on what it is and why it is in the corpus. */
|
|
10
|
+
notes: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SampleResult {
|
|
13
|
+
sample: string;
|
|
14
|
+
kind: Sample["kind"];
|
|
15
|
+
inputBytes: number;
|
|
16
|
+
sha256: string;
|
|
17
|
+
container: string;
|
|
18
|
+
rows: BenchRow[];
|
|
19
|
+
/** The mode the policy would pick for this sample, and why. */
|
|
20
|
+
recommendation: {
|
|
21
|
+
mode: string;
|
|
22
|
+
level: number;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface BenchmarkReport {
|
|
27
|
+
schema: number;
|
|
28
|
+
spec: "openstream";
|
|
29
|
+
specVersion: string;
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
implementation: {
|
|
32
|
+
name: string;
|
|
33
|
+
version: string;
|
|
34
|
+
};
|
|
35
|
+
environment: {
|
|
36
|
+
runtime: string;
|
|
37
|
+
zstd: string;
|
|
38
|
+
zlib: string;
|
|
39
|
+
os: string;
|
|
40
|
+
arch: string;
|
|
41
|
+
cpu: string;
|
|
42
|
+
cores: number;
|
|
43
|
+
memoryGiB: number;
|
|
44
|
+
};
|
|
45
|
+
envelope: {
|
|
46
|
+
magic: string;
|
|
47
|
+
streamHeaderBytes: number;
|
|
48
|
+
frameHeaderBytes: number;
|
|
49
|
+
};
|
|
50
|
+
policy: {
|
|
51
|
+
minSavingsPercent: number;
|
|
52
|
+
minSavingsBytes: number;
|
|
53
|
+
maxBlockBytes: number;
|
|
54
|
+
zstdLevels: number[];
|
|
55
|
+
};
|
|
56
|
+
samples: SampleResult[];
|
|
57
|
+
/** Per mode, summed across the corpus: the honest aggregate. */
|
|
58
|
+
summary: {
|
|
59
|
+
corpusBytes: number;
|
|
60
|
+
byMode: {
|
|
61
|
+
mode: string;
|
|
62
|
+
level: number;
|
|
63
|
+
wireBytes: number;
|
|
64
|
+
savingsPercent: number;
|
|
65
|
+
roundTrip: boolean;
|
|
66
|
+
encodeMs: number;
|
|
67
|
+
decodeMs: number;
|
|
68
|
+
}[];
|
|
69
|
+
};
|
|
70
|
+
caveats: string[];
|
|
71
|
+
}
|
|
72
|
+
/** The default corpus: bytes only, no ffmpeg, deterministic. */
|
|
73
|
+
export declare function syntheticCorpus(): Sample[];
|
|
74
|
+
/** Real files a runner points us at, each read whole (capped) as a sample. */
|
|
75
|
+
export declare function fileCorpus(dir: string, capBytes?: number): Sample[];
|
|
76
|
+
export interface RunOptions {
|
|
77
|
+
implementationVersion: string;
|
|
78
|
+
zstdLevels?: number[];
|
|
79
|
+
blockBytes?: number;
|
|
80
|
+
signal?: AbortSignal;
|
|
81
|
+
}
|
|
82
|
+
/** Run the corpus and build the report. */
|
|
83
|
+
export declare function runBenchmark(corpus: Sample[], options: RunOptions): Promise<BenchmarkReport>;
|
|
84
|
+
/** The report as Markdown, for a human and for the reports page. */
|
|
85
|
+
export declare function reportMarkdown(report: BenchmarkReport): string;
|
|
86
|
+
/**
|
|
87
|
+
* Whether every applicable codec restored exactly: the gate a release must
|
|
88
|
+
* pass. A row with a note is a mode that did not apply to that sample, not a
|
|
89
|
+
* corrupted round trip, so it does not fail the gate.
|
|
90
|
+
*/
|
|
91
|
+
export declare function reportPasses(report: BenchmarkReport): boolean;
|
|
92
|
+
/** The rows that are a real exactness failure: ran, and did not restore. */
|
|
93
|
+
export declare function exactnessFailures(report: BenchmarkReport): {
|
|
94
|
+
sample: string;
|
|
95
|
+
mode: string;
|
|
96
|
+
level: number;
|
|
97
|
+
}[];
|