@torrent-tv/proxy 2.89.6 → 2.89.8

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 (92) hide show
  1. package/AGENTS.md +32 -12
  2. package/CHANGELOG.md +2340 -2330
  3. package/assets/calibration/audio/NOTICE.md +19 -19
  4. package/assets/calibration/audio/manifest.json +98 -98
  5. package/biome.json +412 -412
  6. package/docs/browser-proxy-contract.md +155 -155
  7. package/docs/container-architecture.md +273 -273
  8. package/docs/encode-run-state.md +1 -1
  9. package/docs/playback-start-forecast.md +113 -98
  10. package/package.json +51 -50
  11. package/routes/api/sources/files/get.js +106 -106
  12. package/scripts/build-audio-samples.mjs +31 -31
  13. package/services/encode/EncodeRun.js +67 -15
  14. package/services/encode/EncodeRuns.js +19 -7
  15. package/services/encode/OutputOpening.js +784 -784
  16. package/services/encode/audio-calibration.js +67 -67
  17. package/services/encode/audio-work-rate.js +15 -15
  18. package/services/encode/encode-exit.js +2 -0
  19. package/services/encode/hwaccel.js +1894 -1894
  20. package/services/encode/output/index.js +24 -24
  21. package/services/encode/piece-completeness.js +60 -2
  22. package/services/encode/run-command.js +757 -755
  23. package/services/encode/segment-formats/index.js +1 -0
  24. package/services/encode/segment-formats/mp4-boxes.js +8 -1
  25. package/services/media/ContainerOrchestrator.js +202 -202
  26. package/services/media/KeyframeTables.js +356 -356
  27. package/services/media/SubtitleCues.js +592 -592
  28. package/services/media/audio-inventory.js +299 -299
  29. package/services/media/container/AviContainer.js +268 -268
  30. package/services/media/container/Container.js +632 -632
  31. package/services/media/container/ContainerFactory.js +38 -38
  32. package/services/media/container/MatroskaContainer.js +1031 -1031
  33. package/services/media/container/Mp4Container.js +1354 -1354
  34. package/services/media/container/SubtitleFileContainer.js +339 -339
  35. package/services/media/container/ebml-stream.js +174 -174
  36. package/services/media/container/matroska-clusters.js +1165 -1165
  37. package/services/media/container/unavailable.js +77 -77
  38. package/services/media/tracks/ContainerTrack.js +84 -84
  39. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  40. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  41. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  42. package/services/torrent/Contents.js +382 -382
  43. package/services/torrent/episode-naming.js +182 -182
  44. package/services/torrent/torrent-pool.js +2688 -2688
  45. package/services/torrent/worker/client.js +836 -836
  46. package/services/torrent/worker/held-bytes.js +231 -231
  47. package/services/torrent/worker/worker.js +1137 -1137
  48. package/services/transport/data-channel-handler.js +1373 -1373
  49. package/services/viewer/PriorityMap.js +421 -421
  50. package/test/audio-calibration.test.js +23 -23
  51. package/test/auto-quality-step.test.js +762 -762
  52. package/test/coalesce.test.js +39 -39
  53. package/test/coverage-follows-the-disk.test.js +5 -0
  54. package/test/decode-cost.test.js +475 -475
  55. package/test/encode-run.test.js +58 -0
  56. package/test/episode-naming.test.js +131 -131
  57. package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
  58. package/test/fixtures/audio-frame-boundary.json +1 -1
  59. package/test/fixtures/refused-audio-20261002.json +10 -0
  60. package/test/fmp4-presentation-clock.test.js +4 -2
  61. package/test/format-keyed-outputs.test.js +78 -78
  62. package/test/health-metrics.test.js +11 -13
  63. package/test/held-read-ownership.test.js +77 -77
  64. package/test/helpers/encode-run.js +14 -6
  65. package/test/helpers/held-file.js +77 -77
  66. package/test/helpers/manager.js +62 -62
  67. package/test/helpers/matroska-file.js +296 -296
  68. package/test/keyframe-table-budget.test.js +123 -123
  69. package/test/matroska-cluster-discovery.test.js +323 -323
  70. package/test/one-output-one-session.test.js +273 -273
  71. package/test/output-speed.test.js +6 -1
  72. package/test/piece-completeness.test.js +37 -1
  73. package/test/quality-variants.test.js +1274 -1270
  74. package/test/rate-control-in-the-output.test.js +4 -3
  75. package/test/reads-not-yet-arrived.test.js +218 -218
  76. package/test/seek-frees-the-viewers-requests.test.js +1 -1
  77. package/test/segment-closed-on-both-branches.test.js +114 -114
  78. package/test/segment-judged-by-its-own-header.test.js +222 -222
  79. package/test/segment-serve-wiring.test.js +481 -481
  80. package/test/serving-existing-output.test.js +111 -111
  81. package/test/source-files-route.test.js +113 -113
  82. package/test/subtitle-cue-walk.test.js +385 -385
  83. package/test/subtitle-metadata.test.js +15 -15
  84. package/test/subtitle-plan-waits.test.js +125 -125
  85. package/test/subtitle-track-numbering.test.js +391 -391
  86. package/test/two-viewers-one-picture.test.js +508 -508
  87. package/test/viewer-requests-admission.test.js +92 -92
  88. package/test/viewer-starts-on-map.test.js +27 -27
  89. package/test/viewer-states.test.js +5 -2
  90. package/test/viewing-generations.test.js +390 -390
  91. package/test/whole-file-held-read.test.js +30 -30
  92. package/utils/coalesce.js +41 -41
@@ -1,92 +1,92 @@
1
- /**
2
- * @file Opening an output admits and places its viewer in one synchronous call.
3
- */
4
-
5
- import test from "node:test";
6
- import assert from "node:assert/strict";
7
- import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
8
- import { ViewerRequests } from "../services/server/ViewerRequests.js";
9
-
10
- test("the first opening holds capacity before a concurrent opening is admitted", async () => {
11
- const records = new Map();
12
- const outputCosts = new Map([
13
- ["source-a:0", 0.6],
14
- ["source-b:0", 0.6]
15
- ]);
16
- const viewers = {
17
- get: () => null,
18
- forOutput(output) {
19
- return new Set(records.get(output.outputKey)?.keys() ?? []);
20
- },
21
- of(output, consumerId) {
22
- let viewersOnOutput = records.get(output.outputKey);
23
- if (!viewersOnOutput) {
24
- viewersOnOutput = new Map();
25
- records.set(output.outputKey, viewersOnOutput);
26
- }
27
- let viewer = viewersOnOutput.get(consumerId);
28
- if (!viewer) {
29
- viewer = {
30
- position: null,
31
- linkReading: () => null,
32
- noteVisiblePicture: () => {},
33
- // What the request says about them, kept by the real viewer; what
34
- // this check is about is the place on the machine, not the report.
35
- report: () => {}
36
- };
37
- viewersOnOutput.set(consumerId, viewer);
38
- }
39
- return viewer;
40
- },
41
- watchedAddresses() {
42
- return new Set(
43
- [...records]
44
- .filter(([, members]) => [...members.values()].some((viewer) => viewer.position !== null))
45
- .map(([address]) => address)
46
- );
47
- }
48
- };
49
- const admission = new EncodeAdmission({
50
- liveRunsByAddress: () => new Map(),
51
- preparedAddresses: () => new Set(),
52
- watchedAddresses: () => viewers.watchedAddresses(),
53
- loadOf: (address) => {
54
- const costSec = outputCosts.get(address);
55
- return costSec === undefined ? null : { costSec, fileKey: address, fileCostSec: 0 };
56
- },
57
- availability: () => ({ share: 1, known: true }),
58
- finished: () => false
59
- });
60
- const opening = {
61
- async open({ sourceKey, fileIndex, claim }) {
62
- const output = { id: `${sourceKey}:${fileIndex}`, outputKey: `${sourceKey}:${fileIndex}` };
63
- const answer = claim(output);
64
- if (!answer.admitted) {
65
- const error = new Error(answer.reason);
66
- error.code = "OUTPUT_NO_CAPACITY";
67
- throw error;
68
- }
69
- return { output, existed: false, audio: null, verdict: null };
70
- }
71
- };
72
- const requests = new ViewerRequests({
73
- opening,
74
- viewers,
75
- admitsWatching: (output) => admission.admitsWatching(output.outputKey),
76
- outputTimes: { segmentIndexForTime: (_output, seconds) => Math.floor(seconds / 4) },
77
- planEncodersSoon: () => {},
78
- outputs: { touch: () => {} },
79
- noteServingVerdict: () => {},
80
- waitUntilReady: async () => {}
81
- });
82
-
83
- const first = requests.createOrGetSession({ sourceKey: "source-a", fileIndex: 0, consumerId: "viewer-a" });
84
- const second = requests.createOrGetSession({ sourceKey: "source-b", fileIndex: 0, consumerId: "viewer-b" });
85
- const [firstResult, secondResult] = await Promise.allSettled([first, second]);
86
-
87
- assert.equal(firstResult.status, "fulfilled");
88
- assert.equal(secondResult.status, "rejected");
89
- assert.match(secondResult.reason.message, /would make 0\.83x/);
90
- assert.equal(viewers.forOutput({ outputKey: "source-a:0" }).has("viewer-a"), true);
91
- assert.equal(viewers.forOutput({ outputKey: "source-b:0" }).has("viewer-b"), false);
92
- });
1
+ /**
2
+ * @file Opening an output admits and places its viewer in one synchronous call.
3
+ */
4
+
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
8
+ import { ViewerRequests } from "../services/server/ViewerRequests.js";
9
+
10
+ test("the first opening holds capacity before a concurrent opening is admitted", async () => {
11
+ const records = new Map();
12
+ const outputCosts = new Map([
13
+ ["source-a:0", 0.6],
14
+ ["source-b:0", 0.6]
15
+ ]);
16
+ const viewers = {
17
+ get: () => null,
18
+ forOutput(output) {
19
+ return new Set(records.get(output.outputKey)?.keys() ?? []);
20
+ },
21
+ of(output, consumerId) {
22
+ let viewersOnOutput = records.get(output.outputKey);
23
+ if (!viewersOnOutput) {
24
+ viewersOnOutput = new Map();
25
+ records.set(output.outputKey, viewersOnOutput);
26
+ }
27
+ let viewer = viewersOnOutput.get(consumerId);
28
+ if (!viewer) {
29
+ viewer = {
30
+ position: null,
31
+ linkReading: () => null,
32
+ noteVisiblePicture: () => {},
33
+ // What the request says about them, kept by the real viewer; what
34
+ // this check is about is the place on the machine, not the report.
35
+ report: () => {}
36
+ };
37
+ viewersOnOutput.set(consumerId, viewer);
38
+ }
39
+ return viewer;
40
+ },
41
+ watchedAddresses() {
42
+ return new Set(
43
+ [...records]
44
+ .filter(([, members]) => [...members.values()].some((viewer) => viewer.position !== null))
45
+ .map(([address]) => address)
46
+ );
47
+ }
48
+ };
49
+ const admission = new EncodeAdmission({
50
+ liveRunsByAddress: () => new Map(),
51
+ preparedAddresses: () => new Set(),
52
+ watchedAddresses: () => viewers.watchedAddresses(),
53
+ loadOf: (address) => {
54
+ const costSec = outputCosts.get(address);
55
+ return costSec === undefined ? null : { costSec, fileKey: address, fileCostSec: 0 };
56
+ },
57
+ availability: () => ({ share: 1, known: true }),
58
+ finished: () => false
59
+ });
60
+ const opening = {
61
+ async open({ sourceKey, fileIndex, claim }) {
62
+ const output = { id: `${sourceKey}:${fileIndex}`, outputKey: `${sourceKey}:${fileIndex}` };
63
+ const answer = claim(output);
64
+ if (!answer.admitted) {
65
+ const error = new Error(answer.reason);
66
+ error.code = "OUTPUT_NO_CAPACITY";
67
+ throw error;
68
+ }
69
+ return { output, existed: false, audio: null, verdict: null };
70
+ }
71
+ };
72
+ const requests = new ViewerRequests({
73
+ opening,
74
+ viewers,
75
+ admitsWatching: (output) => admission.admitsWatching(output.outputKey),
76
+ outputTimes: { segmentIndexForTime: (_output, seconds) => Math.floor(seconds / 4) },
77
+ planEncodersSoon: () => {},
78
+ outputs: { touch: () => {} },
79
+ noteServingVerdict: () => {},
80
+ waitUntilReady: async () => {}
81
+ });
82
+
83
+ const first = requests.createOrGetSession({ sourceKey: "source-a", fileIndex: 0, consumerId: "viewer-a" });
84
+ const second = requests.createOrGetSession({ sourceKey: "source-b", fileIndex: 0, consumerId: "viewer-b" });
85
+ const [firstResult, secondResult] = await Promise.allSettled([first, second]);
86
+
87
+ assert.equal(firstResult.status, "fulfilled");
88
+ assert.equal(secondResult.status, "rejected");
89
+ assert.match(secondResult.reason.message, /would make 0\.83x/);
90
+ assert.equal(viewers.forOutput({ outputKey: "source-a:0" }).has("viewer-a"), true);
91
+ assert.equal(viewers.forOutput({ outputKey: "source-b:0" }).has("viewer-b"), false);
92
+ });
@@ -1,27 +1,27 @@
1
- /**
2
- * @file Where viewers stand, read off a merged priority map — the times the
3
- * subtitle walk reads first.
4
- */
5
-
6
- import test from "node:test";
7
- import assert from "node:assert/strict";
8
-
9
- import { emptyMap, mapForViewer, mergeMaps, viewerStartsOn } from "../services/viewer/PriorityMap.js";
10
-
11
- test("each viewer's position is a step up in the merged map", () => {
12
- const map = mergeMaps([
13
- mapForViewer({ atSeconds: 30, durationSeconds: 300, allowanceSeconds: 5 }),
14
- mapForViewer({ atSeconds: 200, durationSeconds: 300, allowanceSeconds: 5 })
15
- ]);
16
- assert.deepEqual(viewerStartsOn(map), [30, 200]);
17
- });
18
-
19
- test("a viewer at the very start, a stopped viewer, and nobody", () => {
20
- assert.deepEqual(viewerStartsOn(mapForViewer({ atSeconds: 0, durationSeconds: 60, allowanceSeconds: 5 })), [0]);
21
- assert.deepEqual(
22
- viewerStartsOn(mapForViewer({ atSeconds: 12, durationSeconds: 60, allowanceSeconds: 5, playing: false })),
23
- [12]
24
- );
25
- assert.deepEqual(viewerStartsOn(emptyMap(60)), []);
26
- assert.deepEqual(viewerStartsOn(null), []);
27
- });
1
+ /**
2
+ * @file Where viewers stand, read off a merged priority map — the times the
3
+ * subtitle walk reads first.
4
+ */
5
+
6
+ import test from "node:test";
7
+ import assert from "node:assert/strict";
8
+
9
+ import { emptyMap, mapForViewer, mergeMaps, viewerStartsOn } from "../services/viewer/PriorityMap.js";
10
+
11
+ test("each viewer's position is a step up in the merged map", () => {
12
+ const map = mergeMaps([
13
+ mapForViewer({ atSeconds: 30, durationSeconds: 300, allowanceSeconds: 5 }),
14
+ mapForViewer({ atSeconds: 200, durationSeconds: 300, allowanceSeconds: 5 })
15
+ ]);
16
+ assert.deepEqual(viewerStartsOn(map), [30, 200]);
17
+ });
18
+
19
+ test("a viewer at the very start, a stopped viewer, and nobody", () => {
20
+ assert.deepEqual(viewerStartsOn(mapForViewer({ atSeconds: 0, durationSeconds: 60, allowanceSeconds: 5 })), [0]);
21
+ assert.deepEqual(
22
+ viewerStartsOn(mapForViewer({ atSeconds: 12, durationSeconds: 60, allowanceSeconds: 5, playing: false })),
23
+ [12]
24
+ );
25
+ assert.deepEqual(viewerStartsOn(emptyMap(60)), []);
26
+ assert.deepEqual(viewerStartsOn(null), []);
27
+ });
@@ -79,8 +79,11 @@ test("a report without a link measurement still states everything about the view
79
79
 
80
80
  assert.equal(viewer.positionSeconds(AT), 12);
81
81
  assert.equal(viewer.waiting, true);
82
- // Nothing was measured, so nothing is claimed about the link.
83
- assert.equal(viewer.netReport, null);
82
+ // The report is kept from the first one the page sends (proxy 2.86.0), and
83
+ // since nothing was measured, nothing is claimed about the link.
84
+ assert.equal(viewer.netReport.linkMbps, null);
85
+ assert.equal(viewer.netReport.linkSampleMbps, null);
86
+ assert.equal(viewer.netReport.positionSeconds, 12);
84
87
  });
85
88
 
86
89
  test("a page one release behind, which cannot say `waiting`, is read from what it does say", () => {