@torrent-tv/proxy 2.107.33 → 2.107.34

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/bin/cli.js CHANGED
@@ -405,7 +405,6 @@ try {
405
405
  onSubtitleCues: (event) => dataChannelHandler?.publishSubtitleCues(event),
406
406
  // Late-bound for the same reason: what the connections carry beyond the
407
407
  // film is counted by the data-channel handler, built further down.
408
- serviceShare: () => dataChannelHandler?.serviceShare?.() ?? null,
409
408
  onStateChanged: () => void stateReporter.changed(),
410
409
  onChaptersChanged: (event) => dataChannelHandler?.publishChaptersChanged(event)
411
410
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.107.33",
3
+ "version": "2.107.34",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -140,9 +140,6 @@ export async function startProxyServer({
140
140
  // The one software preset every picture is encoded at, for runs compared at
141
141
  // one quality; null leaves the choice to the machine.
142
142
  pinnedPreset = null,
143
- // What viewers' connections carry beyond the film, per byte of film; the
144
- // transport counts it and is built after this server, so it is late-bound.
145
- serviceShare = () => null,
146
143
  // Where the proxy writes its own log. Its DIRECTORY is what matters here:
147
144
  // the browser's half of every session is written beside it, so the two are
148
145
  // on one durable disk and join by name.
@@ -1041,7 +1038,6 @@ export async function startProxyServer({
1041
1038
  observeWait: (sourceKey, event, fields) => waitTrace.source(sourceKey, event, fields),
1042
1039
  pinnedPreset,
1043
1040
  indexMemory,
1044
- serviceShare,
1045
1041
  readMetadataActivity: () => containerOrchestrator.activity(),
1046
1042
  onViewerChanged: () => { void sourcePreparation?.refresh(); onStateChanged(); },
1047
1043
  onEncodersChanged: () => onStateChanged(),
@@ -599,8 +599,7 @@ export class OutputOpening {
599
599
  linkMbps: viewerLinkMbps,
600
600
  keys: [...this.#host.outputs.values()].map((other) => other.outputKey).concat(this.#host.segmentOutputFiles.addresses()),
601
601
  readyAt: (key) => this.#host.segmentOutputFiles.isClosed(key, timeline.indexForTime(Math.max(0, startPositionSeconds))),
602
- observedPeakMbps: (candidate) => this.#host.observedPeakMbps?.(candidate) ?? null,
603
- serviceShare: () => this.#host.serviceShare?.() ?? null
602
+ observedPeakMbps: (candidate) => this.#host.observedPeakMbps?.(candidate) ?? null
604
603
  }
605
604
  });
606
605
  // Which picture rate the link was asked about, and where it came from
@@ -148,8 +148,7 @@ export class Renditions {
148
148
  Number.isFinite(base.file.pictureKbps) ? base.file.pictureKbps / 1000 : null,
149
149
  this.#host.observedPeakMbps?.(spec) ?? null
150
150
  ),
151
- audioLoad,
152
- this.#host.serviceShare?.() ?? null
151
+ audioLoad
153
152
  )
154
153
  )
155
154
  });
@@ -504,8 +503,8 @@ export class Renditions {
504
503
  * size whose bounds are two neighbouring limits of `limitsFor(frame, file)` — the
505
504
  * source-scaled nominal for the frame on screen, the same nominal used
506
505
  * to open the output (`output-format.js`). Down,
507
- * the highest lower limit their link admits; up, the next limit only, and
508
- * only if their link admits it. A copy has no limit and a hardware encode is
506
+ * the highest lower limit the measured link supports; up, the next limit
507
+ * only, and only if the measured link supports it. A copy has no limit and a hardware encode is
509
508
  * given none, so neither has anywhere to move to here, and the caller's other
510
509
  * lever — another height — is what remains.
511
510
  *
@@ -551,11 +550,10 @@ export class Renditions {
551
550
  }
552
551
  const linkMbps = this.#host.linkMbpsOf(base, consumerId);
553
552
  const audioLoad = this.viewerAudioLoadOf(base, consumerId);
554
- const serviceShare = this.#host.serviceShare?.() ?? null;
555
- const admits = (limit) => linkCouldCarry(linkMbps, loadOf(videoLoadOfLimit(limit), audioLoad, serviceShare)).admitted;
553
+ const fits = (limit) => linkCouldCarry(linkMbps, loadOf(videoLoadOfLimit(limit), audioLoad)).fits;
556
554
  const target = direction === "down"
557
- ? limits.slice(at + 1).find(admits)
558
- : (at > 0 && admits(limits[at - 1]) ? limits[at - 1] : undefined);
555
+ ? limits.slice(at + 1).find(fits)
556
+ : (at > 0 && fits(limits[at - 1]) ? limits[at - 1] : undefined);
559
557
  if (target === undefined) {
560
558
  return refuse(direction === "down"
561
559
  ? `no lower limit of ${encode.height}p is admitted by this viewer's link`
@@ -276,8 +276,8 @@ export class QualityController {
276
276
  * being told (roadmap item 97, step 12). A limit is part of an output, so
277
277
  * this is a move between outputs, prepared for one viewer.
278
278
  * 2. ANOTHER HEIGHT, asked of the player in AUTO — only for a viewer with no
279
- * higher limit to go to on the height they watch, and only if their own
280
- * link admits the next height's whole load.
279
+ * higher limit to go to on the height they watch, and when the measured
280
+ * link supports the next height's whole load.
281
281
  *
282
282
  * Both follow one window: the machine and the viewer's link have held the
283
283
  * output on screen, with room to spare, for four times as long as a step
@@ -382,10 +382,9 @@ export class QualityController {
382
382
  const report = this.#host.linkReportOf(session, consumerId);
383
383
  const load = loadOf(
384
384
  this.#host.qualityOffer.videoLoadFor(base, height, this.#host.videoEncoder?.kind ?? ""),
385
- this.#host.viewerAudioLoadOf(base, consumerId),
386
- this.#host.serviceShare?.() ?? null
385
+ this.#host.viewerAudioLoadOf(base, consumerId)
387
386
  );
388
- return linkCouldCarry(report?.linkMbps ?? null, load).admitted;
387
+ return linkCouldCarry(report?.linkMbps ?? null, load).fits;
389
388
  }
390
389
 
391
390
  /**
@@ -5,7 +5,7 @@
5
5
  * the last report showed — and never as a reference to a viewer. Nothing here
6
6
  * holds anybody, reads anything or can be asked at the wrong moment.
7
7
  *
8
- * WHAT A LINK CARRIES IS A LOAD, NOT A PICTURE (roadmap item 97, step 11). The
8
+ * WHAT A LINK CARRIES IS A LOAD, NOT A PICTURE. The
9
9
  * viewer receives the picture AND the soundtrack they chose — inside the
10
10
  * picture's own stream, or as a separate one — so both are added before the
11
11
  * link is asked. And each part says how much its figure can be trusted:
@@ -14,20 +14,16 @@
14
14
  * most a copied soundtrack's codec allows it to carry;
15
15
  * 2. `estimated` — an average: a copied picture's or soundtrack's rate as the
16
16
  * FILE states it, or the target a re-encoded soundtrack is asked for. An
17
- * average is not a bound, so a load built on it is admitted only by
18
- * estimate and is never confirmed;
17
+ * average is not a bound, so a load built on it is only estimated to fit
18
+ * and is never confirmed;
19
19
  * 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
20
20
  * soundtrack whose rate nothing states.
21
21
  *
22
22
  * A load is as trustworthy as its least trustworthy part.
23
23
  *
24
- * AND WHAT THE CONNECTION CARRIES BESIDES THE FILM (torrent-tv/meta#169). The
25
- * same connection carries message framing, playlists, poll answers, probes and
26
- * pushed cues. Their share is measured by the transport over everything this
27
- * proxy has delivered (`transport/delivery-shares.js`) and added on top of the
28
- * picture and the sound. It is an average proportion and not a bound, so it
29
- * moves the figure without deciding the load's class; unmeasured, nothing is
30
- * added and the figures say so (`serviceShare: null`).
24
+ * Control traffic and measurement transfers cannot be apportioned to a new
25
+ * output from lifetime byte counters; doing so turned short or interrupted
26
+ * playback into fictitious per-video rates.
31
27
  */
32
28
 
33
29
  import { AUDIO_TRANSCODE_KBPS, maxrateKbpsFor, nominalKbpsFor } from "../args.js";
@@ -52,7 +48,7 @@ const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN]
52
48
 
53
49
  /**
54
50
  * @typedef {{ mbps: number | null, peakClass: string }} LoadPart
55
- * @typedef {{ video: LoadPart | null, audio: LoadPart | null, serviceMbps: number | null, serviceShare: number | null, totalMbps: number | null, peakClass: string }} Load
51
+ * @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
56
52
  */
57
53
 
58
54
  /**
@@ -233,52 +229,43 @@ export const SOUNDTRACK_MODE_CAUSE = Object.freeze({
233
229
  *
234
230
  * @param {LoadPart | null} video
235
231
  * @param {LoadPart | null} audio
236
- * @param {number | null} [serviceShare] - What the connection carries beyond
237
- * the film, per byte of film, as the transport measured it; null while
238
- * nothing has been measured.
239
232
  * @returns {Load}
240
233
  */
241
- export function loadOf(video, audio, serviceShare = null) {
234
+ export function loadOf(video, audio) {
242
235
  const parts = [video, audio].filter(Boolean);
243
236
  const peakClass = parts.reduce(
244
237
  (worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
245
238
  PEAK_CLASS.KNOWN
246
239
  );
247
- const share = Number.isFinite(serviceShare) && serviceShare >= 0 ? serviceShare : null;
248
240
  const filmMbps = peakClass === PEAK_CLASS.UNKNOWN
249
241
  ? null
250
242
  : parts.reduce((sum, one) => sum + one.mbps, 0);
251
- const serviceMbps = filmMbps === null || share === null ? null : filmMbps * share;
252
- const totalMbps = filmMbps === null ? null : filmMbps + (serviceMbps ?? 0);
253
- return { video, audio, serviceMbps, serviceShare: share, totalMbps, peakClass };
243
+ return { video, audio, totalMbps: filmMbps, peakClass };
254
244
  }
255
245
 
256
246
  /**
257
247
  * What a link that measured `linkMbps` does with a load.
258
248
  *
259
- * 1. nothing measured the link — every class is admitted, since the link gave
260
- * no ground to refuse, and nothing is confirmed;
261
- * 2. an `unknown` load against a measured link — `no safe bound`, refused:
262
- * there is no number to compare;
263
- * 3. otherwise the load is compared with the usable share of the link: a
264
- * `known` load that passes FITS and is confirmed, an `estimated` one that
265
- * passes is admitted by estimate and is NOT confirmed.
249
+ * 1. nothing measured the link — no comparison can be made;
250
+ * 2. an `unknown` load against a measured link — `no safe bound`;
251
+ * 3. otherwise the load is compared with the measured link. The result helps
252
+ * choose a sustainable format, but every result remains playable.
266
253
  *
267
254
  * @param {number | null} linkMbps
268
255
  * @param {Load} load
269
- * @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
256
+ * @returns {{ verdict: string, confirmed: boolean, fits: boolean, linkMbps: number | null, load: Load }}
270
257
  */
271
258
  export function linkCouldCarry(linkMbps, load) {
272
259
  const measured = Number.isFinite(linkMbps) && linkMbps > 0;
273
- const answer = (verdict, admitted, confirmed) => ({
260
+ const answer = (verdict, confirmed, fits) => ({
274
261
  verdict,
275
- admitted,
276
262
  confirmed,
263
+ fits,
277
264
  linkMbps: measured ? linkMbps : null,
278
265
  load
279
266
  });
280
267
  if (!measured) {
281
- return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
268
+ return answer(LINK_VERDICT.NO_MEASUREMENT, false, true);
282
269
  }
283
270
  if (load.peakClass === PEAK_CLASS.UNKNOWN) {
284
271
  return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
@@ -292,44 +279,20 @@ export function linkCouldCarry(linkMbps, load) {
292
279
  }
293
280
  return load.peakClass === PEAK_CLASS.KNOWN
294
281
  ? answer(LINK_VERDICT.FITS, true, true)
295
- : answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
282
+ : answer(LINK_VERDICT.ESTIMATED_TO_FIT, false, true);
296
283
  }
297
284
 
298
285
  /**
299
- * Why a link refused a load, in words naming what was missing or too much.
300
- *
301
- * `no safe bound` names every part that has no figure — the picture, the
302
- * soundtrack or both — because that, and not the link, is what the refusal is
303
- * about. Any other refusal is about the size of the load and is described by
304
- * the caller, who knows which outputs were weighed.
305
- *
306
- * @param {{ verdict: string, load: Load }} answer
307
- * @param {string} otherwise - The words for a load that is too large.
308
- * @returns {string}
309
- */
310
- export function linkRefusalReason(answer, otherwise) {
311
- if (answer?.verdict !== LINK_VERDICT.NO_SAFE_BOUND) {
312
- return otherwise;
313
- }
314
- const missing = [
315
- answer.load?.video?.peakClass === PEAK_CLASS.UNKNOWN ? "the picture" : "",
316
- answer.load?.audio?.peakClass === PEAK_CLASS.UNKNOWN ? "the soundtrack" : ""
317
- ].filter(Boolean);
318
- return `nothing states how much ${missing.length > 0 ? missing.join(" or ") : "this output"} would send, ` +
319
- "so this viewer's link cannot be asked whether it fits";
320
- }
321
-
322
- /**
323
- * How strongly an answer is admitted, for ordering: a load whose figure is a
286
+ * How strongly a sustainable answer ranks, for selection: a load whose figure is a
324
287
  * bound before one whose figure is an average, and both before one with no
325
- * figure (which is only ever admitted with nothing measured). Zero for a
326
- * refusal.
288
+ * figure. A load above the measured link remains playable but ranks below
289
+ * outputs the measurement supports.
327
290
  *
328
- * @param {{ admitted: boolean, load: Load }} answer
291
+ * @param {{ fits: boolean, load: Load }} answer
329
292
  * @returns {number}
330
293
  */
331
294
  export function admissionRank(answer) {
332
- if (!answer.admitted) {
295
+ if (!answer.fits) {
333
296
  return 0;
334
297
  }
335
298
  return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
@@ -350,8 +313,6 @@ export function linkAnswerFigures(answer) {
350
313
  videoClass: answer.load.video?.peakClass ?? null,
351
314
  audioMbps: answer.load.audio?.mbps ?? null,
352
315
  audioClass: answer.load.audio?.peakClass ?? null,
353
- serviceMbps: answer.load.serviceMbps ?? null,
354
- serviceShare: answer.load.serviceShare ?? null,
355
316
  totalMbps: answer.load.totalMbps,
356
317
  peakClass: answer.load.peakClass
357
318
  };
@@ -18,17 +18,16 @@
18
18
  * for (`capKbps`), and declared at that source and size's nominal level. A
19
19
  * hardware encoder is given no bound. Unknown source rates mean no bound.
20
20
  *
21
- * AND THE VIEWER'S LINK DECIDES WHETHER IT MAY BE GIVEN AT ALL (roadmap item
22
- * 97, step 11). Every format considered is judged by what this viewer's link
23
- * does with its whole load — the picture and the soundtrack THEY chose
24
- * (`link-budget.js`). In order:
21
+ * THE VIEWER'S LINK HELPS CHOOSE A SUSTAINABLE FORMAT, BUT NEVER DECIDES
22
+ * WHETHER THE VIDEO MAY OPEN. Every format is compared with the measured rate
23
+ * against the picture and soundtrack they chose (`link-budget.js`). In order:
25
24
  *
26
25
  * 1. an output already here that suits them (`serving-output.js`);
27
- * 2. the size wanted, at the highest limit their link admits;
28
- * 3. in AUTO only, a smaller rung of their ladder at the highest limit their
29
- * link admits — a smaller size proves nothing by itself;
30
- * 4. otherwise NOTHING: the answer is `unavailable`, with the figures, and no
31
- * output is named. A format known not to fit is never handed over.
26
+ * 2. the size wanted, at the highest limit the measured link supports;
27
+ * 3. in AUTO only, a smaller rung when its measured load fits;
28
+ * 4. when no sustainable format is available, prepare the first
29
+ * machine-supported format in this order and let playback readiness wait
30
+ * for its continuous media.
32
31
  *
33
32
  * Nothing here reads the machine or the disk: what it needs arrives as values
34
33
  * and as functions over them.
@@ -39,7 +38,7 @@ import { buildResolutionLadder, canSustainOutput, pickSoftwarePreset } from "../
39
38
  import { OutputSpec } from "../output/OutputSpec.js";
40
39
  import { throughputAt } from "../throughput.js";
41
40
  import { chooseServingOutput, nextRungAreaBelow, servingCandidates } from "./serving-output.js";
42
- import { linkAnswerFigures, linkCouldCarry, linkRefusalReason, loadOf, videoLoadOfSpec } from "./link-budget.js";
41
+ import { linkAnswerFigures, linkCouldCarry, loadOf, videoLoadOfSpec } from "./link-budget.js";
43
42
 
44
43
  /**
45
44
  * @param {object} params
@@ -67,7 +66,7 @@ import { linkAnswerFigures, linkCouldCarry, linkRefusalReason, loadOf, videoLoad
67
66
  * soundtrack this viewer receives with the picture, from it or beside it.
68
67
  * @param {(encode: object | null) => OutputSpec} params.specWith - The output
69
68
  * with this picture format and every other part as the request made it.
70
- * @param {{ mode: "auto" | "manual", linkMbps: number | null, keys: Iterable<string>, readyAt: (key: string) => boolean, observedPeakMbps?: (spec: OutputSpec) => number | null, serviceShare?: () => number | null }} params.serving
69
+ * @param {{ mode: "auto" | "manual", linkMbps: number | null, keys: Iterable<string>, readyAt: (key: string) => boolean, observedPeakMbps?: (spec: OutputSpec) => number | null }} params.serving
71
70
  * @returns {{
72
71
  * spec: OutputSpec | null,
73
72
  * budget: object | null,
@@ -103,8 +102,7 @@ export function decideOutputFormat({
103
102
  loadOf(
104
103
  videoLoadOfSpec(spec, Number.isFinite(source.pictureKbps) ? source.pictureKbps / 1000 : null,
105
104
  serving.observedPeakMbps?.(spec) ?? null),
106
- audioLoad,
107
- serving.serviceShare?.() ?? null
105
+ audioLoad
108
106
  )
109
107
  );
110
108
  const given = (spec, answer, extra = {}) => ({
@@ -122,7 +120,7 @@ export function decideOutputFormat({
122
120
  wantedKey: spec.toKey(),
123
121
  reusedKey: null,
124
122
  answer,
125
- unavailable: { reason, figures: answer ? linkAnswerFigures(answer) : null, bound: "link" },
123
+ unavailable: { reason, figures: answer ? linkAnswerFigures(answer) : null },
126
124
  ...extra
127
125
  });
128
126
 
@@ -134,9 +132,7 @@ export function decideOutputFormat({
134
132
  return given(spec, null);
135
133
  }
136
134
  const answer = judge(spec);
137
- return answer.admitted
138
- ? given(spec, answer)
139
- : refused(spec, answer, linkRefusalReason(answer, "the source's own picture is more than this viewer's link carries"));
135
+ return given(spec, answer);
140
136
  }
141
137
 
142
138
  const ceiling = computeOutputDimensions(target.width, target.height, source.width, source.height);
@@ -277,6 +273,7 @@ export function decideOutputFormat({
277
273
  }
278
274
 
279
275
  let anySizeHeld = false;
276
+ let fallback = null;
280
277
  if (machineCan(width, height)) {
281
278
  for (const spec of wantedFormats) {
282
279
  if (!admittedByMachine(spec)) {
@@ -284,7 +281,8 @@ export function decideOutputFormat({
284
281
  }
285
282
  anySizeHeld = true;
286
283
  const answer = judge(spec);
287
- if (answer.admitted) {
284
+ fallback ??= { spec, answer };
285
+ if (answer.fits) {
288
286
  return given(spec, answer, { budget: budgetFor(spec), wantedKey });
289
287
  }
290
288
  }
@@ -300,7 +298,8 @@ export function decideOutputFormat({
300
298
  }
301
299
  anySizeHeld = true;
302
300
  const answer = judge(spec);
303
- if (answer.admitted) {
301
+ fallback ??= { spec, answer };
302
+ if (answer.fits) {
304
303
  return given(spec, answer, { budget: budgetFor(spec), wantedKey });
305
304
  }
306
305
  }
@@ -329,16 +328,7 @@ export function decideOutputFormat({
329
328
  }
330
329
  };
331
330
  }
332
- const wantedAnswer = judge(wantedSpec);
333
- return refused(
334
- wantedSpec,
335
- wantedAnswer,
336
- linkRefusalReason(
337
- wantedAnswer,
338
- serving.mode === "manual"
339
- ? "no limit of the size picked is admitted by this viewer's link"
340
- : "no size and limit this viewer's ladder offers is admitted by their link"
341
- ),
342
- { budget, wantedKey }
343
- );
331
+ // Link measurements are advisory. If none of the sustainable formats can be
332
+ // used yet, keep the best machine-supported output and let readiness wait.
333
+ return given(fallback.spec, fallback.answer, { budget: budgetFor(fallback.spec), wantedKey });
344
334
  }
@@ -3,14 +3,14 @@
3
3
  * encode of the format they would otherwise be given.
4
4
  *
5
5
  * Decided with the user 2026-09-16, and made one rule for every path on
6
- * 2026-09-23 (roadmap item 97, step 11). Quality is compared by the number of
7
- * points in the picture. An output qualifies only if the piece at the viewer's
6
+ * 2026-09-23. Quality is compared by the number of points in the picture.
7
+ * An output qualifies only if the piece at the viewer's
8
8
  * position is already made — material somewhere else in the film spares them
9
- * nothing — AND the viewer's own link admits its load (`link-budget.js`), in
10
- * every mode and on every branch. A load whose figure is a bound (`known`) is
9
+ * nothing — and the viewer's own link can carry its load (`link-budget.js`)
10
+ * where possible. A load whose figure is a bound (`known`) is
11
11
  * taken before one whose figure is an average (`estimated`), whatever their
12
- * sizes; an `unknown` load is admitted only while nothing has measured the
13
- * link.
12
+ * sizes. If none fit the measured rate, format selection prepares a supported
13
+ * output and readiness waits for media to arrive.
14
14
  *
15
15
  * AUTOMATIC:
16
16
  * 1. at least the quality wanted — the nearest to it;
@@ -20,9 +20,9 @@
20
20
  * waiting: this machine does not hold it at the speed the file needs — the
21
21
  * largest of those.
22
22
  *
23
- * CHOSEN BY HAND: exactly the size chosen — of those, the one with the highest
24
- * bitrate the link admits. Which encoder made it and at which speed setting do
25
- * not matter to the viewer.
23
+ * CHOSEN BY HAND: exactly the size chosen — of sustainable outputs, use the
24
+ * one with the highest bitrate. Which encoder made it and at which speed
25
+ * setting do not matter to the viewer.
26
26
  *
27
27
  * No reading of disk or machine here: every figure arrives as a plain value.
28
28
  */
@@ -50,7 +50,7 @@ import { admissionRank } from "./link-budget.js";
50
50
  * keep up with what the file needs. False where nothing measured it.
51
51
  * @param {(candidate: Candidate) => boolean} [params.canProduce] - Whether
52
52
  * this machine can keep producing an output that is not finished.
53
- * @param {(candidate: Candidate) => { admitted: boolean, load: object }} params.judge -
53
+ * @param {(candidate: Candidate) => { fits: boolean, load: object }} params.judge -
54
54
  * What this viewer's link does with the candidate's whole load.
55
55
  * @param {Candidate[]} params.candidates
56
56
  * @returns {{ key: string, answer: object } | null} The output to serve, or
@@ -60,7 +60,7 @@ export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, canPr
60
60
  const admitted = (candidates ?? [])
61
61
  .filter((candidate) => candidate.readyHere === true && canProduce(candidate))
62
62
  .map((candidate) => ({ candidate, answer: judge(candidate) }))
63
- .filter((one) => one.answer.admitted);
63
+ .filter((one) => one.answer.fits);
64
64
  const areaOf = (one) => one.candidate.width * one.candidate.height;
65
65
  const rateOf = (one) => one.answer.load.totalMbps ?? 0;
66
66
  const byRank = (left, right) => admissionRank(right.answer) - admissionRank(left.answer);
@@ -185,8 +185,8 @@ export function servingCandidates({ wanted, keys, source, encoderName, readyAt }
185
185
 
186
186
  /**
187
187
  * Whether ONE output may serve this viewer: the same material as the picture
188
- * it would stand in for, the size their mode requires, and a load their own
189
- * link admits.
188
+ * it would stand in for, the size their mode requires, and its link estimate
189
+ * for quality selection.
190
190
  *
191
191
  * THE ONE RULE EVERY PATH ASKS (roadmap item 97, step 11). The picture
192
192
  * answering for its own height, an output found already producing a height,
@@ -201,7 +201,7 @@ export function servingCandidates({ wanted, keys, source, encoderName, readyAt }
201
201
  * output produces; for a copy, the source's.
202
202
  * @param {{ width: number, height: number } | null} params.wanted - The size a
203
203
  * viewer who picked by hand must get exactly; ignored in AUTO.
204
- * @param {(spec: OutputSpec) => { admitted: boolean }} params.judge
204
+ * @param {(spec: OutputSpec) => { fits: boolean }} params.judge
205
205
  * @returns {{ suits: boolean, answer: object | null, reason: string }}
206
206
  */
207
207
  export function outputSuits({ spec, pictureSpec, mode, size, wanted, judge }) {
@@ -212,7 +212,7 @@ export function outputSuits({ spec, pictureSpec, mode, size, wanted, judge }) {
212
212
  return { suits: false, answer: null, reason: "not the size picked" };
213
213
  }
214
214
  const answer = judge(spec);
215
- return answer.admitted
215
+ return answer.fits
216
216
  ? { suits: true, answer, reason: answer.verdict }
217
217
  : { suits: false, answer, reason: answer.verdict };
218
218
  }
@@ -2,10 +2,7 @@
2
2
  * Shared HTTP responses used by more than one route.
3
3
  */
4
4
  /**
5
- * The answer given when no output suits this viewer: nothing this proxy holds
6
- * or could produce is admitted by their link. Machine-readable, because the
7
- * page has to explain it to the viewer and offer what they can do; the
8
- * figures are the ones the decision was made on.
5
+ * The answer given when no output can be opened.
9
6
  *
10
7
  * 409 and not a 5xx: nothing is wrong with the proxy, and repeating the same
11
8
  * request against the same link gives the same answer.
@@ -239,10 +239,6 @@ export function wireOutputs({
239
239
  encodeInputs = null,
240
240
  indexMemory = null,
241
241
  budgetPolicy = null,
242
- // What viewers' connections carry beyond the film, per byte of film, as
243
- // the transport measured it (`transport/delivery-shares.js`); null while
244
- // nothing has been delivered.
245
- serviceShare = () => null,
246
242
  onViewerChanged = null,
247
243
  // Told when an encoder starts or ends: the room this host has for one more
248
244
  // encode changes with it, and the server chooses proxies by that room.
@@ -400,7 +396,6 @@ export function wireOutputs({
400
396
  // re-encoded where it may be, and copied where it may not.
401
397
  get transcodeEnabled() { return parts.enabled; },
402
398
  observedPeakMbps: (spec) => parts.localObservations?.peakMbps(spec) ?? null,
403
- serviceShare,
404
399
  // A viewer has been moved onto an output prepared for them: how long it
405
400
  // took, and what they held.
406
401
  notePreparation: (output, seconds, bufferedSec) =>
@@ -599,7 +594,6 @@ export function wireOutputs({
599
594
  // The largest peak an output of this mode and rate control has been seen
600
595
  // carrying here: the only figure an encoder with no bound of its own has.
601
596
  observedPeakMbps: (spec) => parts.localObservations?.peakMbps(spec) ?? null,
602
- serviceShare,
603
597
  limitsFor: (frame, file) => parts.limitsFor(frame, file),
604
598
  readSourceMedia,
605
599
  get encodeInputs() { return parts.encodeInputs; },
@@ -1268,7 +1262,6 @@ export function wireOutputs({
1268
1262
  // otherwise.
1269
1263
  // The quality budget: what each output is asked to step to, and the bitrate ceiling a viewer's link sets. Everything it reads of the rest of the proxy is listed here.
1270
1264
  parts.quality = new QualityController({
1271
- serviceShare,
1272
1265
  // The longest an output of this mode has been seen taking to be ready for
1273
1266
  // a viewer moving onto it, in milliseconds, or null.
1274
1267
  observedPreparationMs: (output) => {
@@ -47,7 +47,6 @@
47
47
  /** @import { DataChannel } from 'node-datachannel' */
48
48
 
49
49
  import { createDeliveryProbe, PROBE_INTERVAL_MS } from "./delivery-probe.js";
50
- import { DeliveryShares } from "./delivery-shares.js";
51
50
  import { Agent } from "undici";
52
51
 
53
52
  // Route availability and caller cancellation determine media request lifetime.
@@ -885,7 +884,6 @@ export function createDataChannelHandler({
885
884
  }
886
885
  try {
887
886
  channel.sendMessage(message);
888
- deliveryShares.recordSent(Buffer.byteLength(message));
889
887
  sent += 1;
890
888
  } catch {
891
889
  // Closed between the lookup and the send. The subscription belongs to
@@ -903,9 +901,7 @@ export function createDataChannelHandler({
903
901
  /** Channel-scoped requests cannot be cancelled by a different connection. */
904
902
  const activeRequests = new WeakMap();
905
903
 
906
- // How much of what the connections carry is not the film
907
- // (`delivery-shares.js`), counted where both halves are seen.
908
- const deliveryShares = new DeliveryShares();
904
+ // Transport delivery observations used to report link quality.
909
905
  const { watchSendQueue, readDelivery, noteBrowserReport } = makeSendQueueWatcher({
910
906
  log: (message) => log(message),
911
907
  getTransportSnapshot,
@@ -931,8 +927,7 @@ export function createDataChannelHandler({
931
927
  readDelivery,
932
928
  getTransportSnapshot,
933
929
  witness,
934
- usrsctpState,
935
- onSent: (bytes) => deliveryShares.recordSent(bytes)
930
+ usrsctpState
936
931
  });
937
932
 
938
933
  /**
@@ -1307,7 +1302,6 @@ export function createDataChannelHandler({
1307
1302
  if (done) {
1308
1303
  chunks += body.flush();
1309
1304
  sendChunk(channel, requestId, null, true);
1310
- deliveryShares.recordBody(path, totalBytes);
1311
1305
  const elapsedMs = Date.now() - sendStartedAt;
1312
1306
  let bufferedNow = 0;
1313
1307
  try { bufferedNow = typeof channel.bufferedAmount === "function" ? channel.bufferedAmount() : 0; } catch { /* ignore */ }
@@ -1396,7 +1390,6 @@ export function createDataChannelHandler({
1396
1390
  try {
1397
1391
  const frame = encodeFrame(requestIdBytes(requestId), bytes, done);
1398
1392
  channel.sendMessageBinary(frame);
1399
- deliveryShares.recordSent(frame.length);
1400
1393
  } catch {
1401
1394
  // Channel closed between check and send — safe to ignore.
1402
1395
  }
@@ -1503,7 +1496,6 @@ export function createDataChannelHandler({
1503
1496
  try {
1504
1497
  const text = JSON.stringify(message);
1505
1498
  channel.sendMessage(text);
1506
- deliveryShares.recordSent(Buffer.byteLength(text));
1507
1499
  } catch {
1508
1500
  // Channel closed between check and send — safe to ignore.
1509
1501
  }
@@ -1512,8 +1504,6 @@ export function createDataChannelHandler({
1512
1504
  return {
1513
1505
  handleChannel,
1514
1506
  publishSubtitleCues,
1515
- /** What the film costs viewers' links beyond itself, per byte of film, or null. */
1516
- serviceShare: () => deliveryShares.serviceShare(),
1517
1507
  publishChaptersChanged
1518
1508
  };
1519
1509
  }
@@ -427,10 +427,7 @@ export function createDeliveryProbe({
427
427
  readDelivery,
428
428
  getTransportSnapshot,
429
429
  witness,
430
- usrsctpState,
431
- // Every probe handed to a channel, in bytes: what it costs the viewer's link
432
- // is counted with everything else the connection carries.
433
- onSent = () => {}
430
+ usrsctpState
434
431
  }) {
435
432
  /** @type {Map<string, ProbeConnection>} */
436
433
  const connections = new Map();
@@ -461,7 +458,6 @@ export function createDeliveryProbe({
461
458
  for (const channel of connection.channels.keys()) {
462
459
  try {
463
460
  channel.sendMessage(message);
464
- onSent(Buffer.byteLength(message));
465
461
  } catch {
466
462
  // A channel closing between the check and the send is ordinary.
467
463
  }
@@ -1,108 +0,0 @@
1
- /**
2
- * @file How much of what this proxy hands to its viewers' connections is not
3
- * the film.
4
- *
5
- * A viewer's link carries the picture and the soundtrack AND everything else
6
- * this proxy sends down the same connection: the framing of every data-channel
7
- * message, playlists, answers to progress polls, delivery probes, pushed
8
- * subtitle cues, segments sent and then abandoned. A load that counts only the
9
- * picture and the sound asks the link for less than it will be given
10
- * (torrent-tv/meta#169).
11
- *
12
- * What is counted, all of it in bytes handed to a data channel, which is the
13
- * level the browser's own link measurement is taken at — application bytes,
14
- * without SCTP, DTLS, UDP or IP framing — and all of it at the moment it is
15
- * sent, so the three counts always describe the same messages:
16
- *
17
- * 1. `sent` — every message handed to a channel, frame headers included;
18
- * 2. `media` — the bodies of segment and init responses delivered whole;
19
- * 3. `measurement` — the bodies of the link measurement and the delivery sink,
20
- * which are sent when a connection is made or on request and are not part
21
- * of watching a film.
22
- *
23
- * The share is `(sent - media - measurement) / media`: what the film costs
24
- * the link beyond itself, per byte of film. Counted over the life of the
25
- * process and across every connection, because a viewer about to open a film
26
- * has no history of their own and the traffic around a film is the proxy's,
27
- * not the person's.
28
- */
29
-
30
- /** Which kind of response a request path asks for. */
31
- export const DELIVERY_KIND = Object.freeze({
32
- MEDIA: "media",
33
- MEASUREMENT: "measurement",
34
- SERVICE: "service"
35
- });
36
-
37
- const MEDIA_FILE = /\/(?:segment-\d+\.[a-z0-9]+|init\.mp4)$/u;
38
- const MEASUREMENT_ROUTE = /^\/api\/(?:link-probe|delivery-sink)$/u;
39
-
40
- /**
41
- * What a data-channel request path asks for.
42
- *
43
- * @param {string} path - The request path, query included or not.
44
- * @returns {string} One of {@link DELIVERY_KIND}.
45
- */
46
- export function deliveryKindOf(path) {
47
- const bare = String(path ?? "").split("?")[0];
48
- if (MEDIA_FILE.test(bare)) return DELIVERY_KIND.MEDIA;
49
- if (MEASUREMENT_ROUTE.test(bare)) return DELIVERY_KIND.MEASUREMENT;
50
- return DELIVERY_KIND.SERVICE;
51
- }
52
-
53
- export class DeliveryShares {
54
- #sent = 0;
55
- #media = 0;
56
- #measurement = 0;
57
-
58
- /**
59
- * A response body handed over whole.
60
- *
61
- * @param {string} path
62
- * @param {number} bytes
63
- */
64
- recordBody(path, bytes) {
65
- if (!(Number.isFinite(bytes) && bytes > 0)) return;
66
- const kind = deliveryKindOf(path);
67
- if (kind === DELIVERY_KIND.MEDIA) this.#media += bytes;
68
- else if (kind === DELIVERY_KIND.MEASUREMENT) this.#measurement += bytes;
69
- }
70
-
71
- /**
72
- * One message handed to a channel, whatever it carries.
73
- *
74
- * @param {number} bytes
75
- */
76
- recordSent(bytes) {
77
- if (Number.isFinite(bytes) && bytes > 0) this.#sent += bytes;
78
- }
79
-
80
- /**
81
- * What the film costs the link beyond itself, per byte of film, or null
82
- * while no film has been delivered.
83
- *
84
- * Never below zero: a body is counted once its last message has gone, so
85
- * the two counts agree exactly between responses and the body's own
86
- * messages are already in `sent` when it is counted.
87
- *
88
- * @returns {number | null}
89
- */
90
- serviceShare() {
91
- if (this.#media <= 0) return null;
92
- return Math.max(0, (this.#sent - this.#media - this.#measurement) / this.#media);
93
- }
94
-
95
- /**
96
- * The counts behind the share, for a log line.
97
- *
98
- * @returns {{ sentBytes: number, mediaBytes: number, measurementBytes: number, serviceShare: number | null }}
99
- */
100
- figures() {
101
- return {
102
- sentBytes: this.#sent,
103
- mediaBytes: this.#media,
104
- measurementBytes: this.#measurement,
105
- serviceShare: this.serviceShare()
106
- };
107
- }
108
- }