@torrent-tv/proxy 2.89.3 → 2.89.5

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 (50) hide show
  1. package/AGENTS.md +338 -336
  2. package/CHANGELOG.md +2319 -2303
  3. package/docs/encode-architecture.md +55 -19
  4. package/docs/playback-start-forecast.md +13 -5
  5. package/docs/storage-architecture.md +6 -0
  6. package/package.json +1 -1
  7. package/routes/stream/get.js +38 -2
  8. package/server.js +748 -745
  9. package/services/encode/EncodeOrchestrator.js +24 -49
  10. package/services/encode/EncodePlan.js +5 -3
  11. package/services/encode/EncodeRun.js +80 -17
  12. package/services/encode/EncodeRuns.js +1339 -1239
  13. package/services/encode/Renditions.js +2 -1
  14. package/services/encode/RunClock.js +87 -0
  15. package/services/encode/RunProgress.js +1 -6
  16. package/services/encode/encoder-readings.js +26 -28
  17. package/services/encode/piece-completeness.js +46 -0
  18. package/services/encode/quality/EncodeCost.js +1043 -1167
  19. package/services/encode/quality/OutputQualityState.js +1 -5
  20. package/services/encode/quality/QualityController.js +213 -390
  21. package/services/encode/quality/link-budget.js +5 -17
  22. package/services/encode/run-inputs.js +13 -2
  23. package/services/server/SegmentServing.js +3 -1
  24. package/services/server/ViewerRequests.js +2 -7
  25. package/services/server/wire-outputs.js +12 -7
  26. package/services/storage/segment-store/SegmentStore.js +1294 -1255
  27. package/services/torrent/supply-margin.js +7 -5
  28. package/test/auto-quality-step.test.js +75 -29
  29. package/test/decode-cost.test.js +1 -1
  30. package/test/encode-cost.test.js +31 -39
  31. package/test/encode-orchestrator.test.js +15 -8
  32. package/test/encode-plan-viewers.test.js +34 -27
  33. package/test/encode-run.test.js +34 -6
  34. package/test/encoder-readings.test.js +48 -26
  35. package/test/helpers/encode-run.js +113 -9
  36. package/test/link-budget.test.js +4 -5
  37. package/test/manager-surface.test.js +1 -2
  38. package/test/orchestrator-wired.test.js +5 -4
  39. package/test/output-speed.test.js +0 -1
  40. package/test/piece-completeness.test.js +42 -0
  41. package/test/quality-offer.test.js +1 -3
  42. package/test/quality-variants.test.js +1 -1
  43. package/test/run-progress.test.js +2 -3
  44. package/test/same-height-switch.test.js +1 -2
  45. package/test/segment-closed-on-both-branches.test.js +114 -96
  46. package/test/segment-store.test.js +15 -15
  47. package/test/stream-route.test.js +28 -0
  48. package/test/supply-margin.test.js +13 -0
  49. package/test/two-viewers-one-picture.test.js +1 -1
  50. package/test/viewer-outputs.test.js +1 -1
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file Whether a closed piece holds the whole of its stretch of film.
3
+ *
4
+ * Decided by the encoding from the cut table and the piece's own media; the
5
+ * segment store only keeps what it is given.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { cutOf, judgePiece } from "../services/encode/piece-completeness.js";
11
+ import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
12
+
13
+ /** Media ending where stated, in the shape a format's reading has. */
14
+ const endingAt = (seconds) => ({ end: seconds });
15
+ const format = { producedThroughSeconds: (ranges) => ranges?.end ?? null };
16
+
17
+ /** Cuts every ten seconds, ten pieces. */
18
+ const timeline = { segmentCount: 10, publishedStartOf: (index) => index * 10 };
19
+
20
+ test("a piece must reach its next cut less the delta the muxer is given", () => {
21
+ assert.equal(cutOf(timeline, 3), 40 - SEGMENT_CUT_TIME_DELTA_SECONDS);
22
+ });
23
+
24
+ test("the final piece has no next cut", () => {
25
+ assert.equal(cutOf(timeline, 9), undefined);
26
+ assert.deepEqual(judgePiece(format, endingAt(95), cutOf(timeline, 9)), { whole: true, throughSeconds: 95 });
27
+ });
28
+
29
+ test("an audio piece ending within the delta of its cut is whole", () => {
30
+ // Field 2026-10-01: AAC frames end 4.7389 s into a piece cut at 4.755 s.
31
+ const cut = 4.755 - SEGMENT_CUT_TIME_DELTA_SECONDS;
32
+ assert.equal(judgePiece(format, endingAt(4.738913832199547), cut).whole, true);
33
+ });
34
+
35
+ test("a piece closed short of its cut is not whole", () => {
36
+ // A run killed mid-piece: 84 ms of film where ten seconds were cut.
37
+ assert.deepEqual(judgePiece(format, endingAt(456.084), 466), { whole: false, throughSeconds: 456.084 });
38
+ });
39
+
40
+ test("a piece with no playable media is not whole, whatever its cut", () => {
41
+ assert.deepEqual(judgePiece(format, null, undefined), { whole: false, throughSeconds: null });
42
+ });
@@ -58,10 +58,8 @@ function offerOver(
58
58
  runningEncoders: () => 0,
59
59
  encodersRunningNow: () => 0,
60
60
  torrentCostSecFor: () => 0,
61
- boundBy: async () => "cpu",
62
61
  runsFor: (session) => [...(session.runs ?? [])],
63
- stateFor: (session) => runStateOf(session.runs),
64
- progressFor: (session) => session.progress ?? null
62
+ stateFor: (session) => runStateOf(session.runs)
65
63
  });
66
64
  let computed = 0;
67
65
  const real = cost.sustainableHeights.bind(cost);
@@ -131,7 +131,7 @@ function fakeSession({
131
131
  useSyntheticPlaylist: true,
132
132
  playlistText: "#EXTM3U\n",
133
133
  segmentCount: 100,
134
- progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
134
+ progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
135
135
  };
136
136
  }
137
137
 
@@ -16,7 +16,7 @@ test("a run rebases ffmpeg's relative clock onto its own source position", () =>
16
16
  let now = 10;
17
17
  const progress = new RunProgress({ startSeconds: 600, totalSeconds: 1000, now: () => now });
18
18
  now = 20;
19
- progress.note({ processedSeconds: 12.5, speed: "2.0x" });
19
+ progress.note({ processedSeconds: 12.5 });
20
20
 
21
21
  assert.deepEqual(progress.snapshot(), {
22
22
  processedSeconds: 612.5,
@@ -24,7 +24,6 @@ test("a run rebases ffmpeg's relative clock onto its own source position", () =>
24
24
  totalSeconds: 1000,
25
25
  percent: 3.125,
26
26
  remainingSeconds: 387.5,
27
- speed: "2.0x",
28
27
  updatedAt: 20
29
28
  });
30
29
  });
@@ -33,7 +32,7 @@ test("two runs of one output retain separate clocks", () => {
33
32
  const made = orchestrator();
34
33
  const first = { from: 0, to: 49, isAlive: true, progress: new RunProgress({ startSeconds: 0 }) };
35
34
  const second = { from: 50, to: 99, isAlive: true, progress: new RunProgress({ startSeconds: 200 }) };
36
- first.progress.note({ processedSeconds: 40, speed: "1.0x" });
35
+ first.progress.note({ processedSeconds: 40 });
37
36
  second.progress.note({ processedSeconds: 8, speed: "2.0x" });
38
37
  made.adopt("picture", first);
39
38
  made.adopt("picture", second);
@@ -65,7 +65,6 @@ import { SourceFile } from "../services/media/SourceFile.js";
65
65
  import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
66
66
  import { outputSpec } from "./helpers/output-spec.js";
67
67
  import { maxrateKbpsFor, nominalKbpsFor, softwareRateControlFor } from "../services/encode/args.js";
68
- import { LINK_SAFETY } from "../services/encode/quality/link-budget.js";
69
68
 
70
69
  const BASE_ID = "1111111122223333";
71
70
  const SEGMENT_SECONDS = 4;
@@ -86,7 +85,7 @@ const AUDIO_MBPS = 0.128;
86
85
  * @returns {number}
87
86
  */
88
87
  function linkJustFor(limit) {
89
- return (maxrateKbpsFor(limit) / 1000 + AUDIO_MBPS + 0.01) / LINK_SAFETY;
88
+ return maxrateKbpsFor(limit) / 1000 + AUDIO_MBPS + 0.01;
90
89
  }
91
90
 
92
91
  /**
@@ -1,96 +1,114 @@
1
- /**
2
- * @file A piece is closed by one of two writers, and both must say so.
3
- *
4
- * The `segment` muxer writes under a working name and reports the closure on a
5
- * channel of its own, which reaches `SegmentStore.publish`. The `hls` muxer
6
- * writes through a temporary name of its own (`+temp_file`) and reports
7
- * nothing: the file simply appears under the name it is served as. That branch
8
- * is taken by every re-encoded output cut on the even grid — an ordinary
9
- * quality step — and for it a wait could only end on its deadline, so a segment
10
- * lying finished on disk was answered 503 after the whole hold.
11
- */
12
-
13
- import test from "node:test";
14
- import assert from "node:assert/strict";
15
- import { mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
- import os from "node:os";
17
- import path from "node:path";
18
- import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
19
- import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
20
-
21
- const KEY = "torrent:abc:fmt=fmp4:grid=uniform@0:video-only:v=libx264/854x480";
22
-
23
- /** A deadline is the backstop of these waits, never what they measure. */
24
- const BACKSTOP_MS = 10_000;
25
-
26
- /**
27
- * @returns {{ store: SegmentStore, root: string }}
28
- */
29
- function storeInATempRoot() {
30
- const root = mkdtempSync(path.join(os.tmpdir(), "segment-closed-"));
31
- const store = new SegmentStore({ root, logger: { info: () => {}, warn: () => {} } });
32
- store.useFormat(KEY, fmp4Format);
33
- return { store, root };
34
- }
35
-
36
- test("a piece that simply appears ends the wait that began before it", async (t) => {
37
- const { store, root } = storeInATempRoot();
38
- t.after(() => {
39
- store.dropAll("the test is over");
40
- rmSync(root, { recursive: true, force: true });
41
- });
42
-
43
- const dir = store.directoryFor(KEY);
44
- // The wait begins on a segment nobody has written yet — the cold case, and
45
- // the only one the deadline used to decide.
46
- const waited = store.waitFor(KEY, 7, BACKSTOP_MS);
47
-
48
- // What the `hls` muxer does: a temporary name of its own, renamed into place
49
- // when the piece is whole. Nothing is told; the directory simply moves.
50
- const temp = path.join(dir, "segment-00007.mp4.tmp");
51
- writeFileSync(temp, Buffer.alloc(64));
52
- renameSync(temp, path.join(dir, fmp4Format.segmentFileName(7)));
53
-
54
- assert.equal(await waited, true, "the piece was on disk and the wait did not end");
55
- assert.ok(store.pathOf(KEY, 7), "and the store must be able to hand it over");
56
- });
57
-
58
- test("clearing up after a run is not its pieces arriving", async (t) => {
59
- const { store, root } = storeInATempRoot();
60
- t.after(() => {
61
- store.dropAll("the test is over");
62
- rmSync(root, { recursive: true, force: true });
63
- });
64
-
65
- const dir = store.directoryFor(KEY);
66
- const unfinished = path.join(dir, fmp4Format.makingFileNameTemplate("3").replace("%05d", "00009"));
67
- writeFileSync(unfinished, Buffer.alloc(64));
68
-
69
- let ended = false;
70
- // 200 ms is a grace before asserting that something did NOT happen, which can
71
- // only ever pass too easily — never a measurement.
72
- const waited = store.waitFor(KEY, 9, 200).then((value) => {
73
- ended = value;
74
- return value;
75
- });
76
- rmSync(unfinished, { force: true });
77
-
78
- assert.equal(await waited, false, "a directory that moved because a file went holds nothing new");
79
- assert.equal(ended, false);
80
- });
81
-
82
- test("the writer that does report a closure is unaffected", async (t) => {
83
- const { store, root } = storeInATempRoot();
84
- t.after(() => {
85
- store.dropAll("the test is over");
86
- rmSync(root, { recursive: true, force: true });
87
- });
88
-
89
- const dir = store.directoryFor(KEY);
90
- const making = fmp4Format.makingFileNameTemplate("4").replace("%05d", "00004");
91
- writeFileSync(path.join(dir, making), Buffer.alloc(64));
92
-
93
- const waited = store.waitFor(KEY, 4, BACKSTOP_MS);
94
- assert.equal(store.publish(KEY, making, fmp4Format), fmp4Format.segmentFileName(4));
95
- assert.equal(await waited, true);
96
- });
1
+ /**
2
+ * @file A piece is closed by one of two writers, and both must say so.
3
+ *
4
+ * The `segment` muxer writes under a working name and reports the closure on a
5
+ * channel of its own, which reaches `SegmentStore.publish`. The `hls` muxer
6
+ * writes through a temporary name of its own (`+temp_file`) and reports
7
+ * nothing: the file simply appears under the name it is served as. That branch
8
+ * is taken by every re-encoded output cut on the even grid — an ordinary
9
+ * quality step — and for it a wait could only end on its deadline, so a segment
10
+ * lying finished on disk was answered 503 after the whole hold.
11
+ */
12
+
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
+ import os from "node:os";
17
+ import path from "node:path";
18
+ import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
19
+ import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
20
+
21
+ const KEY = "torrent:abc:fmt=fmp4:grid=uniform@0:video-only:v=libx264/854x480";
22
+
23
+ /** A deadline is the backstop of these waits, never what they measure. */
24
+ const BACKSTOP_MS = 10_000;
25
+
26
+ /**
27
+ * @returns {{ store: SegmentStore, root: string }}
28
+ */
29
+ function storeInATempRoot() {
30
+ const root = mkdtempSync(path.join(os.tmpdir(), "segment-closed-"));
31
+ const store = new SegmentStore({ root, logger: { info: () => {}, warn: () => {} } });
32
+ store.useFormat(KEY, fmp4Format);
33
+ return { store, root };
34
+ }
35
+
36
+ test("a piece that simply appears ends the wait that began before it", async (t) => {
37
+ const { store, root } = storeInATempRoot();
38
+ t.after(() => {
39
+ store.dropAll("the test is over");
40
+ rmSync(root, { recursive: true, force: true });
41
+ });
42
+
43
+ const dir = store.directoryFor(KEY);
44
+ // The wait begins on a segment nobody has written yet — the cold case, and
45
+ // the only one the deadline used to decide.
46
+ const waited = store.waitFor(KEY, 7, BACKSTOP_MS);
47
+
48
+ // What the `hls` muxer does: a temporary name of its own, renamed into place
49
+ // when the piece is whole. Nothing is told; the directory simply moves.
50
+ const temp = path.join(dir, "segment-00007.mp4.tmp");
51
+ writeFileSync(temp, Buffer.alloc(64));
52
+ renameSync(temp, path.join(dir, fmp4Format.segmentFileName(7)));
53
+
54
+ assert.equal(await waited, true, "the piece was on disk and the wait did not end");
55
+ assert.ok(store.pathOf(KEY, 7), "and the store must be able to hand it over");
56
+ });
57
+
58
+ test("clearing up after a run is not its pieces arriving", async (t) => {
59
+ const { store, root } = storeInATempRoot();
60
+ t.after(() => {
61
+ store.dropAll("the test is over");
62
+ rmSync(root, { recursive: true, force: true });
63
+ });
64
+
65
+ const dir = store.directoryFor(KEY);
66
+ const unfinished = path.join(dir, fmp4Format.makingFileNameTemplate("3").replace("%05d", "00009"));
67
+ writeFileSync(unfinished, Buffer.alloc(64));
68
+
69
+ let ended = false;
70
+ // 200 ms is a grace before asserting that something did NOT happen, which can
71
+ // only ever pass too easily — never a measurement.
72
+ const waited = store.waitFor(KEY, 9, 200).then((value) => {
73
+ ended = value;
74
+ return value;
75
+ });
76
+ rmSync(unfinished, { force: true });
77
+
78
+ assert.equal(await waited, false, "a directory that moved because a file went holds nothing new");
79
+ assert.equal(ended, false);
80
+ });
81
+
82
+ test("the writer that does report a closure is unaffected", async (t) => {
83
+ const { store, root } = storeInATempRoot();
84
+ t.after(() => {
85
+ store.dropAll("the test is over");
86
+ rmSync(root, { recursive: true, force: true });
87
+ });
88
+
89
+ const dir = store.directoryFor(KEY);
90
+ const making = fmp4Format.makingFileNameTemplate("4").replace("%05d", "00004");
91
+ writeFileSync(path.join(dir, making), Buffer.alloc(64));
92
+
93
+ const waited = store.waitFor(KEY, 4, BACKSTOP_MS);
94
+ assert.equal(store.publish(KEY, making, fmp4Format), fmp4Format.segmentFileName(4));
95
+ assert.equal(await waited, true);
96
+ });
97
+
98
+ test("a directory removed from outside and made again is watched again", async (t) => {
99
+ const { store, root } = storeInATempRoot();
100
+ t.after(() => {
101
+ store.dropAll("the test is over");
102
+ rmSync(root, { recursive: true, force: true });
103
+ });
104
+
105
+ // Removed by something other than the store, so its watch was not stopped
106
+ // first. The watch held on that path watches a directory that is gone.
107
+ rmSync(store.directoryFor(KEY), { recursive: true, force: true });
108
+ const dir = store.directoryFor(KEY);
109
+ const waited = store.waitFor(KEY, 3, BACKSTOP_MS);
110
+
111
+ writeFileSync(path.join(dir, fmp4Format.segmentFileName(3)), Buffer.alloc(64));
112
+
113
+ assert.equal(await waited, true, "the piece arrived in the new directory and the wait did not end");
114
+ });
@@ -10,7 +10,6 @@ import os from "node:os";
10
10
  import path from "node:path";
11
11
  import { directoryNameFor, SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
12
12
  import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
13
- import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
14
13
 
15
14
  /**
16
15
  * @returns {{ store: SegmentStore, root: string, lines: string[] }}
@@ -36,32 +35,33 @@ function writeSegment(dir, index, bytes = 16) {
36
35
 
37
36
  const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
38
37
 
39
- test("a complete audio cut within the configured muxer delta remains usable", (t) => {
38
+ test("a published piece keeps the media intervals the encoding read from it", (t) => {
39
+ // The store does not judge a piece; whether it is whole is the encoding's
40
+ // decision (`encode/piece-completeness.js`), taken before it is published.
40
41
  const { store, root } = storeInATempRoot();
41
42
  t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
42
- const format = { ...fmp4Format, readMediaRanges: () => [{ start: 0, end: 4.738913832199547 }],
43
- producedThroughSeconds: (ranges) => ranges.at(-1).end };
43
+ const format = { ...fmp4Format, readMediaRanges: () => assert.fail("the store does not read a piece it was handed") };
44
44
  const dir = store.directoryFor(KEY);
45
45
  store.useFormat(KEY, format);
46
- const endSeconds = 4.755 - SEGMENT_CUT_TIME_DELTA_SECONDS;
47
- writeFileSync(path.join(dir, "making-0-00000.mp4"), Buffer.alloc(16));
48
- assert.equal(store.publish(KEY, "making-0-00000.mp4", format, { endSeconds }), "segment-00000.mp4");
49
- assert.ok(store.mediaRangesOf(KEY, 0, { endSeconds }));
46
+ writeFileSync(path.join(dir, "making-0-00000.mp4"), Buffer.from("closed"));
47
+ assert.deepEqual(store.closedBytesOf(KEY, "making-0-00000.mp4"), Buffer.from("closed"));
48
+ const mediaRanges = { tracks: [] };
49
+ assert.equal(store.publish(KEY, "making-0-00000.mp4", format, { mediaRanges }), "segment-00000.mp4");
50
+ assert.equal(store.mediaRangesOf(KEY, 0), mediaRanges);
50
51
  });
51
52
 
52
- test("a closed but truncated non-final piece is not published or reused", (t) => {
53
- const { store, root } = storeInATempRoot();
53
+ test("a stored piece is taken off the disk at its owner's word", (t) => {
54
+ const { store, root, lines } = storeInATempRoot();
54
55
  t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
55
- const format = { ...fmp4Format, readMediaRanges: () => [{ start: 456, end: 456.084 }],
56
- producedThroughSeconds: (ranges) => ranges.at(-1).end };
56
+ const format = { ...fmp4Format, readMediaRanges: () => ({ tracks: [] }) };
57
57
  const dir = store.directoryFor(KEY);
58
58
  store.useFormat(KEY, format);
59
- writeFileSync(path.join(dir, "making-0-00071.mp4"), Buffer.alloc(16));
60
- assert.equal(store.publish(KEY, "making-0-00071.mp4", format, { endSeconds: 466 }), null);
61
59
  writeSegment(dir, 71);
62
60
  assert.ok(store.mediaRangesOf(KEY, 71));
63
- assert.equal(store.mediaRangesOf(KEY, 71, { endSeconds: 466 }), undefined);
61
+ store.remove(KEY, 71, "short of its cut");
64
62
  assert.equal(store.pathOf(KEY, 71), null);
63
+ assert.equal(store.mediaRangesOf(KEY, 71), undefined);
64
+ assert.ok(lines.some((line) => line.includes("segment 71") && line.includes("short of its cut")));
65
65
  });
66
66
 
67
67
  test("two viewers of one output are given the same directory", (t) => {
@@ -207,3 +207,31 @@ test("a file this proxy does not have whole still goes to the torrent", async ()
207
207
  await handleStreamGet(req, reply, deps);
208
208
  assert.equal(state.prioritized.length, 1, "the ordinary path was not taken");
209
209
  });
210
+
211
+ test("every wait of an encoder's input read is marked for the run its URL names", async () => {
212
+ // The run's processing speed is read over its own working time; the time its
213
+ // input waits for the swarm is what this marks (RunClock).
214
+ const { req, reply, deps } = harness({ method: "GET", range: "bytes=0-5" });
215
+ const { files: [file] } = await deps.torrentPool.getTorrent();
216
+ const pieces = [Buffer.from("abc"), Buffer.from("def")];
217
+ file.createFragmentReader = () => ({
218
+ cancel() {},
219
+ async *[Symbol.asyncIterator]() {
220
+ for (const bytes of pieces) {
221
+ yield { bytes, release() {} };
222
+ }
223
+ }
224
+ });
225
+ reply.raw.write = (_bytes, done) => done();
226
+ reply.raw.writableEnded = false;
227
+ reply.raw.destroyed = false;
228
+ req.query.session = "output-1";
229
+ req.query.run = "7";
230
+ const marks = [];
231
+ deps.noteInputWaiting = (runToken, waiting) => marks.push([runToken, waiting]);
232
+
233
+ await handleStreamGet(req, reply, deps);
234
+
235
+ assert.deepEqual(marks, [[7, true], [7, false], [7, true], [7, false], [7, true], [7, false]],
236
+ "a wait before each fragment and one before the end, each closed");
237
+ });
@@ -175,3 +175,16 @@ test("a term with nothing observed contributes nothing", () => {
175
175
  assert.equal(minimumBufferFrom({}), null, "without a segment duration nothing can be said");
176
176
  assert.equal(minimumBufferFrom({ segmentSeconds: 0 }), null);
177
177
  });
178
+
179
+ test("a supply that delivered for a sliver of the time asks for what it measured, uncut", () => {
180
+ // Two stalls of 99.9 s with 0.1 s of delivery between them: the share lost is
181
+ // 0.999, and the speed asked for is a thousand times realtime. A cap at a
182
+ // chosen 0.99 used to report a hundred.
183
+ const answer = requiredSpeedFrom([
184
+ { waitedMs: 99_900, at: 99_900 },
185
+ { waitedMs: 99_900, at: 199_900 }
186
+ ]);
187
+ assert.ok(answer);
188
+ assert.ok(Math.abs(answer.lostShare - 0.999) < 1e-9, `got ${answer.lostShare}`);
189
+ assert.ok(Math.abs(answer.requiredSpeed - 1000) < 1e-6, `got ${answer.requiredSpeed}`);
190
+ });
@@ -132,7 +132,7 @@ function fakeSession({
132
132
  useSyntheticPlaylist: true,
133
133
  playlistText: "#EXTM3U\n",
134
134
  segmentCount: 100,
135
- progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
135
+ progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
136
136
  };
137
137
  }
138
138
 
@@ -98,7 +98,7 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
98
98
  useSyntheticPlaylist: true,
99
99
  playlistText: "#EXTM3U\n",
100
100
  segmentCount: 100,
101
- progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
101
+ progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
102
102
  };
103
103
  }
104
104