nixamp 0.7.37 → 0.7.38

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.
@@ -40,6 +40,13 @@ export declare const GIVE_UP = 5;
40
40
  * channel that is quiet for half a minute is not being quiet, it is dead.
41
41
  */
42
42
  export declare const STALL = 30000;
43
+ /**
44
+ * How much of the recent stream a newcomer is handed. About six seconds of
45
+ * 720p television, and a couple of seconds of 192k MP3: enough to play
46
+ * through a hiccup, not enough to put a viewer noticeably behind the room.
47
+ */
48
+ export declare const BACKLOG_VIDEO: number;
49
+ export declare const BACKLOG_AUDIO: number;
43
50
  /** A name that can sit in a URL and be read back in a list. */
44
51
  export declare function cleanId(value: unknown, fallback?: string): string;
45
52
  export interface ChannelOptions {
@@ -70,6 +77,19 @@ export declare class Channel {
70
77
  private watchdog;
71
78
  private stall;
72
79
  private stderr;
80
+ /**
81
+ * The last few seconds, for whoever joins next.
82
+ *
83
+ * A listener handed only what comes after they arrive starts exactly on the
84
+ * live edge, with nothing buffered ahead: every hiccup in the source or the
85
+ * network is a stall, and CNN in a browser was play, wait, play, wait, for
86
+ * ever. A few seconds of recent fragments, written before the live bytes,
87
+ * is the cushion every other live player has. For a picture the backlog
88
+ * starts at a fragment boundary, because a fragment is the unit a decoder
89
+ * can begin at; for MP3 any point will do, a frame announces itself.
90
+ */
91
+ private recent;
92
+ private recentBytes;
73
93
  constructor(info: ChannelInfo, options: ChannelOptions, onGone: (id: string) => void);
74
94
  start(format: string): void;
75
95
  /**
@@ -128,6 +148,8 @@ export declare class Channel {
128
148
  * make sense.
129
149
  */
130
150
  private emit;
151
+ /** Keep this for the next arrival, and let the oldest go once it is too much. */
152
+ private remember;
131
153
  /** Feed the source. */
132
154
  write(chunk: Buffer): boolean;
133
155
  pump(body: Readable): Promise<void>;
package/dist/channels.js CHANGED
@@ -18,7 +18,7 @@ import { spawn } from "node:child_process";
18
18
  import { randomBytes } from "node:crypto";
19
19
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
- import { Fragments } from "./fragments.js";
21
+ import { Fragments, isOpening } from "./fragments.js";
22
22
  /** How long to wait before dialling a dropped source again. */
23
23
  export const REDIAL = 2000;
24
24
  /** How many times in a row a source may fail without ever sending anything. */
@@ -33,6 +33,17 @@ export const GIVE_UP = 5;
33
33
  export const STALL = 30_000;
34
34
  /** How much of what ffmpeg said to keep, for the last line when it dies. */
35
35
  const TAIL = 2000;
36
+ /**
37
+ * How much of the recent stream a newcomer is handed. About six seconds of
38
+ * 720p television, and a couple of seconds of 192k MP3: enough to play
39
+ * through a hiccup, not enough to put a viewer noticeably behind the room.
40
+ */
41
+ export const BACKLOG_VIDEO = 4 * 1024 * 1024;
42
+ export const BACKLOG_AUDIO = 64 * 1024;
43
+ /** The four-letter name in a box header, or "" for something too short. */
44
+ function boxType(box) {
45
+ return box.length >= 8 ? box.toString("latin1", 4, 8) : "";
46
+ }
36
47
  /**
37
48
  * Read everything a child says on stderr, keeping only the end of it.
38
49
  *
@@ -87,6 +98,19 @@ export class Channel {
87
98
  watchdog = null;
88
99
  stall = STALL;
89
100
  stderr = "";
101
+ /**
102
+ * The last few seconds, for whoever joins next.
103
+ *
104
+ * A listener handed only what comes after they arrive starts exactly on the
105
+ * live edge, with nothing buffered ahead: every hiccup in the source or the
106
+ * network is a stall, and CNN in a browser was play, wait, play, wait, for
107
+ * ever. A few seconds of recent fragments, written before the live bytes,
108
+ * is the cushion every other live player has. For a picture the backlog
109
+ * starts at a fragment boundary, because a fragment is the unit a decoder
110
+ * can begin at; for MP3 any point will do, a frame announces itself.
111
+ */
112
+ recent = [];
113
+ recentBytes = 0;
90
114
  constructor(info, options, onGone) {
91
115
  this.info = info;
92
116
  this.options = options;
@@ -234,6 +258,8 @@ export class Channel {
234
258
  startOver() {
235
259
  if (this.info.kind === "video")
236
260
  this.fragments = new Fragments();
261
+ this.recent = [];
262
+ this.recentBytes = 0;
237
263
  this.hangUp();
238
264
  }
239
265
  /** Expect output within STALL, or treat the source as gone and dial again. */
@@ -304,11 +330,32 @@ export class Channel {
304
330
  */
305
331
  emit(chunk) {
306
332
  if (!this.fragments) {
333
+ this.remember(chunk, BACKLOG_AUDIO, false);
307
334
  this.send(chunk);
308
335
  return;
309
336
  }
310
- for (const box of this.fragments.push(chunk))
337
+ for (const box of this.fragments.push(chunk)) {
338
+ if (!isOpening(boxType(box)))
339
+ this.remember(box, BACKLOG_VIDEO, true);
311
340
  this.send(box);
341
+ }
342
+ }
343
+ /** Keep this for the next arrival, and let the oldest go once it is too much. */
344
+ remember(piece, cap, aligned) {
345
+ this.recent.push(piece);
346
+ this.recentBytes += piece.byteLength;
347
+ while (this.recent.length > 0 && this.recentBytes > cap) {
348
+ const gone = this.recent.shift();
349
+ this.recentBytes -= gone.byteLength;
350
+ }
351
+ // A picture's backlog must begin at a `moof`: an `mdat` on its own is
352
+ // samples nobody has been told the layout of.
353
+ if (aligned) {
354
+ while (this.recent.length > 0 && boxType(this.recent[0]) !== "moof") {
355
+ const gone = this.recent.shift();
356
+ this.recentBytes -= gone.byteLength;
357
+ }
358
+ }
312
359
  }
313
360
  /** Feed the source. */
314
361
  write(chunk) {
@@ -357,6 +404,18 @@ export class Channel {
357
404
  // Gone before it began; the detach below still tidies up.
358
405
  }
359
406
  }
407
+ // Then the last few seconds, so there is something to play while the
408
+ // live bytes catch up, rather than a picture that stalls on every hiccup.
409
+ if (!this.fragments || this.fragments.ready) {
410
+ for (const piece of this.recent) {
411
+ try {
412
+ listener.write(piece);
413
+ }
414
+ catch {
415
+ break;
416
+ }
417
+ }
418
+ }
360
419
  this.listeners.add(listener);
361
420
  this.info.listeners = this.listeners.size;
362
421
  return () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.7.37",
3
+ "version": "0.7.38",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/channels.ts CHANGED
@@ -19,7 +19,7 @@ import { randomBytes } from "node:crypto";
19
19
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import type { Readable } from "node:stream";
22
- import { Fragments } from "./fragments.ts";
22
+ import { Fragments, isOpening } from "./fragments.ts";
23
23
 
24
24
  /** Somewhere for a channel's audio to go. A response, in practice. */
25
25
  export interface Listener {
@@ -66,6 +66,18 @@ export const GIVE_UP = 5;
66
66
  export const STALL = 30_000;
67
67
  /** How much of what ffmpeg said to keep, for the last line when it dies. */
68
68
  const TAIL = 2000;
69
+ /**
70
+ * How much of the recent stream a newcomer is handed. About six seconds of
71
+ * 720p television, and a couple of seconds of 192k MP3: enough to play
72
+ * through a hiccup, not enough to put a viewer noticeably behind the room.
73
+ */
74
+ export const BACKLOG_VIDEO = 4 * 1024 * 1024;
75
+ export const BACKLOG_AUDIO = 64 * 1024;
76
+
77
+ /** The four-letter name in a box header, or "" for something too short. */
78
+ function boxType(box: Buffer): string {
79
+ return box.length >= 8 ? box.toString("latin1", 4, 8) : "";
80
+ }
69
81
 
70
82
  /**
71
83
  * Read everything a child says on stderr, keeping only the end of it.
@@ -126,6 +138,19 @@ export class Channel {
126
138
  private watchdog: ReturnType<typeof setTimeout> | null = null;
127
139
  private stall = STALL;
128
140
  private stderr = "";
141
+ /**
142
+ * The last few seconds, for whoever joins next.
143
+ *
144
+ * A listener handed only what comes after they arrive starts exactly on the
145
+ * live edge, with nothing buffered ahead: every hiccup in the source or the
146
+ * network is a stall, and CNN in a browser was play, wait, play, wait, for
147
+ * ever. A few seconds of recent fragments, written before the live bytes,
148
+ * is the cushion every other live player has. For a picture the backlog
149
+ * starts at a fragment boundary, because a fragment is the unit a decoder
150
+ * can begin at; for MP3 any point will do, a frame announces itself.
151
+ */
152
+ private recent: Buffer[] = [];
153
+ private recentBytes = 0;
129
154
 
130
155
  constructor(
131
156
  readonly info: ChannelInfo,
@@ -282,6 +307,8 @@ export class Channel {
282
307
  */
283
308
  private startOver(): void {
284
309
  if (this.info.kind === "video") this.fragments = new Fragments();
310
+ this.recent = [];
311
+ this.recentBytes = 0;
285
312
  this.hangUp();
286
313
  }
287
314
 
@@ -350,10 +377,32 @@ export class Channel {
350
377
  */
351
378
  private emit(chunk: Buffer): void {
352
379
  if (!this.fragments) {
380
+ this.remember(chunk, BACKLOG_AUDIO, false);
353
381
  this.send(chunk);
354
382
  return;
355
383
  }
356
- for (const box of this.fragments.push(chunk)) this.send(box);
384
+ for (const box of this.fragments.push(chunk)) {
385
+ if (!isOpening(boxType(box))) this.remember(box, BACKLOG_VIDEO, true);
386
+ this.send(box);
387
+ }
388
+ }
389
+
390
+ /** Keep this for the next arrival, and let the oldest go once it is too much. */
391
+ private remember(piece: Buffer, cap: number, aligned: boolean): void {
392
+ this.recent.push(piece);
393
+ this.recentBytes += piece.byteLength;
394
+ while (this.recent.length > 0 && this.recentBytes > cap) {
395
+ const gone = this.recent.shift() as Buffer;
396
+ this.recentBytes -= gone.byteLength;
397
+ }
398
+ // A picture's backlog must begin at a `moof`: an `mdat` on its own is
399
+ // samples nobody has been told the layout of.
400
+ if (aligned) {
401
+ while (this.recent.length > 0 && boxType(this.recent[0] as Buffer) !== "moof") {
402
+ const gone = this.recent.shift() as Buffer;
403
+ this.recentBytes -= gone.byteLength;
404
+ }
405
+ }
357
406
  }
358
407
 
359
408
  /** Feed the source. */
@@ -404,6 +453,17 @@ export class Channel {
404
453
  // Gone before it began; the detach below still tidies up.
405
454
  }
406
455
  }
456
+ // Then the last few seconds, so there is something to play while the
457
+ // live bytes catch up, rather than a picture that stalls on every hiccup.
458
+ if (!this.fragments || this.fragments.ready) {
459
+ for (const piece of this.recent) {
460
+ try {
461
+ listener.write(piece);
462
+ } catch {
463
+ break;
464
+ }
465
+ }
466
+ }
407
467
  this.listeners.add(listener);
408
468
  this.info.listeners = this.listeners.size;
409
469
  return () => {
package/web/dist/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* nixamp service worker — generated, do not edit */
2
- const CACHE = "nixamp-1789031044115";
2
+ const CACHE = "nixamp-1789031509853";
3
3
  const PRECACHE = [
4
4
  "/",
5
5
  "/apple-touch-icon.png",