@torrent-tv/proxy 2.91.0 → 2.92.1

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 (145) hide show
  1. package/bin/cli.js +3 -2
  2. package/package.json +18 -2
  3. package/routes/api/playback-plan/post.js +21 -4
  4. package/routes/api/sources/files/get.js +20 -28
  5. package/routes/api/sources/post.js +4 -2
  6. package/routes/api/sources/stats/get.js +4 -11
  7. package/routes/api/sources/viewer/post.js +16 -0
  8. package/routes/api/sources/warm/post.js +6 -215
  9. package/routes/api/subtitles/get.js +22 -125
  10. package/routes/api/subtitles/post.js +26 -0
  11. package/routes/api/transcode-sessions/post.js +24 -2
  12. package/routes/media/get.js +80 -0
  13. package/routes/stream/get.js +26 -94
  14. package/routes/transcode/audio-warm/get.js +9 -21
  15. package/routes/transcode/variant-warm/get.js +8 -14
  16. package/server.js +410 -46
  17. package/services/encode/AdmittedInput.js +136 -0
  18. package/services/encode/CushionReport.js +1 -76
  19. package/services/encode/EncodeInputs.js +142 -0
  20. package/services/encode/EncodeOrchestrator.js +4 -1
  21. package/services/encode/EncodeRun.js +13 -4
  22. package/services/encode/EncodeRuns.js +132 -52
  23. package/services/encode/InputFailures.js +37 -0
  24. package/services/encode/MatroskaInput.js +158 -0
  25. package/services/encode/OutputOpening.js +16 -64
  26. package/services/encode/Renditions.js +1 -17
  27. package/services/encode/admitted-command.js +60 -0
  28. package/services/encode/annex-b-input.js +37 -0
  29. package/services/encode/bit-payload.js +29 -0
  30. package/services/encode/output/OutputCatalog.js +1 -1
  31. package/services/encode/output/Timeline.js +11 -0
  32. package/services/encode/pcm-normalization.js +30 -0
  33. package/services/encode/piece-completeness.js +54 -1
  34. package/services/encode/quality/HostLoad.js +19 -73
  35. package/services/encode/quality/QualityOffer.js +1 -1
  36. package/services/encode/run-command.js +1 -589
  37. package/services/encode/segment-formats/mp4-boxes.js +3 -1
  38. package/services/encode/segment-formats/presentation-segment.js +114 -0
  39. package/services/media/ContainerOrchestrator.js +118 -48
  40. package/services/media/KeyframeTables.js +16 -115
  41. package/services/media/MediaReadRequests.js +120 -0
  42. package/services/media/ProbeRequests.js +135 -0
  43. package/services/media/SegmentInputs.js +48 -0
  44. package/services/media/SourceFile.js +6 -0
  45. package/services/media/SubtitleOrchestrator.js +190 -4
  46. package/services/media/await-plan.js +47 -0
  47. package/services/media/container/AsfContainer.js +217 -0
  48. package/services/media/container/AviContainer.js +343 -34
  49. package/services/media/container/CadencedPictureOrder.js +45 -0
  50. package/services/media/container/Container.js +7 -3
  51. package/services/media/container/ContainerFactory.js +10 -3
  52. package/services/media/container/FfprobeContainer.js +222 -0
  53. package/services/media/container/MatroskaContainer.js +72 -46
  54. package/services/media/container/Mp4Container.js +472 -139
  55. package/services/media/container/MpegPsContainer.js +281 -0
  56. package/services/media/container/MpegTsContainer.js +277 -0
  57. package/services/media/container/PacketIndex.js +270 -0
  58. package/services/media/container/PacketRecords.js +237 -0
  59. package/services/media/container/RetainedBytes.js +39 -0
  60. package/services/media/container/RetainedReads.js +51 -0
  61. package/services/media/container/ac3-frame.js +46 -0
  62. package/services/media/container/asf-packets.js +153 -0
  63. package/services/media/container/avc-picture-order.js +50 -0
  64. package/services/media/container/avi-open-dml.js +47 -0
  65. package/services/media/container/dts-frame.js +51 -0
  66. package/services/media/container/ebml-stream.js +12 -1
  67. package/services/media/container/ffprobe-record.js +45 -0
  68. package/services/media/container/flv-packet-ranges.js +27 -0
  69. package/services/media/container/following-packet-time.js +37 -0
  70. package/services/media/container/h264-configuration.js +183 -0
  71. package/services/media/container/h264-elementary-index.js +245 -0
  72. package/services/media/container/hevc-configuration.js +165 -0
  73. package/services/media/container/hevc-elementary-index.js +132 -0
  74. package/services/media/container/hevc-picture-order.js +66 -0
  75. package/services/media/container/latm-frame.js +99 -0
  76. package/services/media/container/matroska-packets.js +207 -0
  77. package/services/media/container/memory-unavailable.js +8 -0
  78. package/services/media/container/mp3-packet-dependencies.js +41 -0
  79. package/services/media/container/mp4-fragments.js +148 -0
  80. package/services/media/container/mpeg-audio-frame.js +19 -0
  81. package/services/media/container/mpeg-audio-reservoir.js +27 -0
  82. package/services/media/container/mpeg-elementary-index.js +196 -0
  83. package/services/media/container/mpeg-sequence-reader.js +69 -0
  84. package/services/media/container/mpeg-ts-packets.js +93 -0
  85. package/services/media/container/mpeg-video-configuration.js +50 -0
  86. package/services/media/container/mpeg4-picture-timing.js +108 -0
  87. package/services/media/container/pes-header.js +34 -0
  88. package/services/media/playback-declarations.js +57 -0
  89. package/services/media/playback-planner.js +72 -230
  90. package/services/media/probe-packets.js +66 -0
  91. package/services/media/read-playback-declarations.js +18 -0
  92. package/services/media/tracks/AudioTrack.js +9 -0
  93. package/services/media/tracks/TextSubtitleTrack.js +4 -1
  94. package/services/server/OutputLifecycle.js +2 -0
  95. package/services/server/SegmentServing.js +40 -33
  96. package/services/server/ViewerRequests.js +22 -41
  97. package/services/server/controllers/PlaybackController.js +4 -0
  98. package/services/server/controllers/SubtitleController.js +8 -29
  99. package/services/server/transcode-session-files.js +58 -99
  100. package/services/server/wire-outputs.js +58 -15
  101. package/services/storage/IndexMemory.js +85 -0
  102. package/services/storage/MachineBudget.js +27 -3
  103. package/services/storage/allowance.js +8 -1
  104. package/services/storage/piece-store/piece-disk-store.js +18 -3
  105. package/services/storage/piece-store/piece-lru.js +8 -4
  106. package/services/storage/piece-store/shared-piece-store.js +26 -11
  107. package/services/storage/segment-store/SegmentStore.js +49 -18
  108. package/services/storage/wire.js +7 -0
  109. package/services/torrent/Contents.js +52 -20
  110. package/services/torrent/demand/DemandRegister.js +13 -4
  111. package/services/torrent/demand/Window.js +13 -2
  112. package/services/torrent/demand-progress.js +25 -0
  113. package/services/torrent/download/DeadlineScheduler.js +167 -0
  114. package/services/torrent/download/FutureDownload.js +166 -0
  115. package/services/torrent/download/SwarmSelection.js +30 -59
  116. package/services/torrent/download/registry.js +45 -5
  117. package/services/torrent/download/withdraw-claim.js +2 -0
  118. package/services/torrent/torrent-pool.js +39 -600
  119. package/services/torrent/worker/channel.js +8 -1
  120. package/services/torrent/worker/client.js +74 -72
  121. package/services/torrent/worker/files-in-use.js +9 -0
  122. package/services/torrent/worker/handle-state.js +12 -0
  123. package/services/torrent/worker/held-bytes.js +82 -99
  124. package/services/torrent/worker/held-file-bytes.js +17 -0
  125. package/services/torrent/worker/piece-reader.js +67 -683
  126. package/services/torrent/worker/pool-adapter.js +37 -123
  127. package/services/torrent/worker/protocol.js +6 -17
  128. package/services/torrent/worker/whole-file-stats.js +2 -0
  129. package/services/torrent/worker/worker.js +86 -156
  130. package/services/transport/data-channel-handler.js +56 -13
  131. package/services/viewer/DownloadMaps.js +168 -0
  132. package/services/viewer/NativeSourceMap.js +33 -0
  133. package/services/viewer/PriorityMap.js +49 -40
  134. package/services/viewer/PriorityOrchestrator.js +42 -13
  135. package/services/viewer/SourcePreparation.js +278 -0
  136. package/services/viewer/Viewer.js +11 -0
  137. package/services/viewer/Viewers.js +109 -2
  138. package/services/viewer/playback-readiness.js +50 -63
  139. package/services/viewer/segment-demands.js +37 -0
  140. package/services/viewer/urgent-output-ready.js +26 -0
  141. package/services/encode/run-inputs.js +0 -53
  142. package/services/media/keyframe-probe.js +0 -157
  143. package/services/media/media-info-probe.js +0 -93
  144. package/services/torrent/worker/background-fill.js +0 -213
  145. package/services/torrent/worker/resume-warm.js +0 -107
@@ -9,11 +9,8 @@
9
9
  * construction, and the call sites are untouched — which is what keeps a change
10
10
  * of this size reviewable.
11
11
  *
12
- * Two accommodations are needed, and both are deliberate:
12
+ * Source handles retain their derived identity across calls:
13
13
  *
14
- * - **`prioritizeByteRange` stays synchronous.** It returns nothing the caller
15
- * inspects, so the command is dispatched and not awaited. Awaiting it would
16
- * mean touching every call site for no observable gain.
17
14
  * - **`getTorrent` needs a `sourceKey`.** Torrent objects cannot cross a
18
15
  * thread, so the worker keys them. Callers that have one pass it; the rest
19
16
  * get one derived from the source itself, so the identity stays stable
@@ -33,13 +30,17 @@ export class WorkerTorrentPool {
33
30
  #client;
34
31
  /** Stand-ins by source key, so repeat calls return the same object. */
35
32
  #torrents = new Map();
33
+ #pending = new Map();
36
34
 
37
35
  /**
38
36
  * @param {{ memoryBytes?: number, stateDir?: string }} [options]
39
37
  * @param {object | null} [client] - The worker interface; tests supply a fake.
40
38
  */
41
39
  constructor(options = {}, client = null) {
42
- this.#client = client ?? new TorrentWorkerClient(options);
40
+ this.#client = client ?? new TorrentWorkerClient({ ...options, onSourceForgotten: event => {
41
+ this.forget(event.sourceKey);
42
+ options.onSourceForgotten?.(event);
43
+ } });
43
44
  }
44
45
 
45
46
  /**
@@ -55,9 +56,29 @@ export class WorkerTorrentPool {
55
56
  if (existing) {
56
57
  return existing;
57
58
  }
58
- const torrent = await this.#client.getTorrent({ sourceKey, sourceType, source });
59
- this.#torrents.set(sourceKey, torrent);
60
- return torrent;
59
+ const joined = this.#pending.get(sourceKey);
60
+ if (joined) return joined;
61
+ const pending = this.#client.getTorrent({ sourceKey, sourceType, source }).then(torrent => {
62
+ if (this.#pending.get(sourceKey) !== pending) {
63
+ const error = new Error("Source was forgotten while its metadata was being read.");
64
+ error.code = "SOURCE_FORGOTTEN";
65
+ throw error;
66
+ }
67
+ this.#torrents.set(sourceKey, torrent);
68
+ return torrent;
69
+ }).finally(() => { if (this.#pending.get(sourceKey) === pending) this.#pending.delete(sourceKey); });
70
+ this.#pending.set(sourceKey, pending);
71
+ return pending;
72
+ }
73
+
74
+ forget(sourceKey) {
75
+ this.#torrents.delete(sourceKey);
76
+ this.#pending.delete(sourceKey);
77
+ }
78
+
79
+ /** Return an existing handle without adding, reviving, or waiting for it. */
80
+ knownTorrent(sourceKey) {
81
+ return this.#torrents.get(sourceKey) ?? null;
61
82
  }
62
83
 
63
84
  /**
@@ -119,10 +140,9 @@ export class WorkerTorrentPool {
119
140
  *
120
141
  * @param {object} torrent
121
142
  * @param {number | null} [fileIndex]
122
- * @param {{ resumeAnchorByteStart?: number | null }} [options]
123
143
  * @returns {Promise<object | null>}
124
144
  */
125
- async getFileStats(torrent, fileIndex = null, options = {}) {
145
+ async getFileStats(torrent, fileIndex = null, { forecast = false } = {}) {
126
146
  const sourceKey = torrent?.sourceKey;
127
147
  if (!sourceKey) {
128
148
  return null;
@@ -130,99 +150,10 @@ export class WorkerTorrentPool {
130
150
  return this.#client.getFileStats({
131
151
  sourceKey,
132
152
  fileIndex,
133
- resumeAnchorByteStart: options?.resumeAnchorByteStart ?? null
153
+ forecast
134
154
  });
135
155
  }
136
156
 
137
- /**
138
- * Fetch one whole file using only the room the viewer's own reading leaves.
139
- *
140
- * For a soundtrack or subtitle file shipped beside the picture: small next to
141
- * the film, and having it on disk is what turns a later switch into a local
142
- * read instead of a wait on the swarm.
143
- *
144
- * @param {object} torrent
145
- * @param {number} fileIndex
146
- * @returns {Promise<boolean>} Whether a fill was started by this call.
147
- */
148
- async fillFileInBackground(torrent, fileIndex) {
149
- const sourceKey = torrent?.sourceKey;
150
- if (!sourceKey) {
151
- return false;
152
- }
153
- const answer = await this.#client.fillFile({ sourceKey, fileIndex });
154
- return answer?.started === true;
155
- }
156
-
157
- /**
158
- * Ask the torrent to fetch all remaining files at conditional TAIL urgency.
159
- *
160
- * @param {object} torrent
161
- * @returns {Promise<boolean>}
162
- */
163
- async fillTorrent(torrent) {
164
- const sourceKey = torrent?.sourceKey;
165
- if (!sourceKey) {
166
- return false;
167
- }
168
- const answer = await this.#client.fillTorrent(sourceKey);
169
- return answer?.started === true;
170
- }
171
-
172
-
173
-
174
-
175
- /**
176
- * Start fetching the region a viewer is about to resume at. Named in seconds
177
- * here; the worker turns it into bytes, where the file's duration is readable.
178
- *
179
- * @param {object} torrent
180
- * @param {number} fileIndex
181
- * @param {number} positionSeconds
182
- * @returns {Promise<boolean>}
183
- */
184
- async warmResumePosition(torrent, fileIndex, positionSeconds, durationSeconds) {
185
- const sourceKey = torrent?.sourceKey;
186
- if (!sourceKey) {
187
- return false;
188
- }
189
- const answer = await this.#client.warmResumePosition({ sourceKey, fileIndex, positionSeconds, durationSeconds });
190
- return answer?.started === true;
191
- }
192
-
193
-
194
-
195
-
196
-
197
-
198
- /**
199
- * Reorder piece selection around a read position.
200
- *
201
- * Synchronous by design — see the file header.
202
- *
203
- * @param {object} torrent
204
- * @param {number} fileIndex
205
- * @param {number} byteStart
206
- * @param {number} [windowBytes]
207
- * @param {{ wholeFileRead?: boolean }} [options]
208
- * @returns {void}
209
- */
210
- prioritizeByteRange(torrent, fileIndex, byteStart, windowBytes, options) {
211
- const sourceKey = torrent?.sourceKey;
212
- if (!sourceKey) {
213
- return;
214
- }
215
- void this.#client
216
- .prioritizeByteRange({
217
- sourceKey,
218
- fileIndex,
219
- byteStart,
220
- windowBytes,
221
- wholeFileRead: options?.wholeFileRead === true
222
- })
223
- .catch(() => undefined);
224
- }
225
-
226
157
  /**
227
158
  * Hand the download the priority map for one file.
228
159
  *
@@ -240,21 +171,6 @@ export class WorkerTorrentPool {
240
171
  await this.#client.setPriorityMap({ sourceKey, fileIndex, durationSeconds, zones });
241
172
  }
242
173
 
243
- /**
244
- * Pre-fetch the head and tail the codec probe needs.
245
- *
246
- * Takes an options object, matching `TorrentPool.prefetchFileEdges` — this
247
- * adapter exists to present that same interface. It previously declared
248
- * positional parameters instead, so the planner's options object arrived as
249
- * `headBytes` and only worked because it was passed along far enough to be
250
- * destructured at the far end. Anyone calling it as documented got the
251
- * defaults instead of the sizes they asked for.
252
- *
253
- * @param {object} torrent
254
- * @param {number} fileIndex
255
- * @param {{ headBytes?: number, tailBytes?: number, timeoutMs?: number }} [options]
256
- * @returns {Promise<unknown>}
257
- */
258
174
  /**
259
175
  * One byte range of one file, as bytes, on THIS thread.
260
176
  *
@@ -313,6 +229,11 @@ export class WorkerTorrentPool {
313
229
  return sourceKey ? this.#client.readHeld({ sourceKey, fileIndex, start, end }) : null;
314
230
  }
315
231
 
232
+ async readHeldRangesOf(torrent, fileIndex, ranges, maxBytes) {
233
+ const sourceKey = torrent?.sourceKey;
234
+ return sourceKey ? this.#client.readHeldRanges({ sourceKey, fileIndex, ranges, maxBytes }) : null;
235
+ }
236
+
316
237
  async readRangeOf(torrent, fileIndex, start, end) {
317
238
  const sourceKey = torrent?.sourceKey;
318
239
  if (!sourceKey || !(end >= start) || !(start >= 0)) {
@@ -337,14 +258,6 @@ export class WorkerTorrentPool {
337
258
  return total > 0 ? Buffer.concat(chunks, total) : null;
338
259
  }
339
260
 
340
- async prefetchFileEdges(torrent, fileIndex, options = {}) {
341
- const sourceKey = torrent?.sourceKey;
342
- if (!sourceKey) {
343
- return null;
344
- }
345
- return this.#client.prefetchFileEdges({ sourceKey, fileIndex, options });
346
- }
347
-
348
261
  /**
349
262
  * Which films this proxy holds right now, and how much of each.
350
263
  *
@@ -366,6 +279,7 @@ export class WorkerTorrentPool {
366
279
  */
367
280
  async destroyAll() {
368
281
  this.#torrents.clear();
282
+ this.#pending.clear();
369
283
  await this.#client.destroyAll();
370
284
  }
371
285
  }
@@ -55,8 +55,6 @@ export const Command = {
55
55
  TORRENT_TOTALS: "torrent-totals",
56
56
  /** Which films this proxy holds right now, for content affinity. */
57
57
  HELD_TORRENTS: "held-torrents",
58
- /** Reorder piece selection around a read position (seek prioritisation). */
59
- PRIORITIZE: "prioritize",
60
58
  /**
61
59
  * The priority map for one file: seconds of film against a number.
62
60
  *
@@ -71,21 +69,6 @@ export const Command = {
71
69
  READ_RANGE: "read-range",
72
70
  /** Abandon an in-flight READ_RANGE (viewer gone, seek superseded). */
73
71
  CANCEL_READ: "cancel-read",
74
- /** Pre-fetch the head and tail a codec probe needs. */
75
- PREFETCH_EDGES: "prefetch-edges",
76
- /**
77
- * Fetch one whole file using only the room the viewer's own reading leaves —
78
- * a soundtrack or subtitle file they may switch to later. Returns as soon as
79
- * the work is under way.
80
- */
81
- FILL_FILE: "fill-file",
82
- /** Claim every remaining file as conditional TAIL demand for one torrent. */
83
- FILL_TORRENT: "fill-torrent",
84
- /**
85
- * Start fetching the region a viewer is about to resume at, named in seconds
86
- * and turned into bytes here, where the file's own duration can be read.
87
- */
88
- WARM_POSITION: "warm-position",
89
72
  /**
90
73
  * The byte ranges of one file the torrent holds WHOLE, so the subtitle walk
91
74
  * can decide what it may read without asking the swarm. A list rather than a
@@ -102,6 +85,8 @@ export const Command = {
102
85
  * must pull nothing.
103
86
  */
104
87
  READ_HELD: "read-held",
88
+ /** Acquire all source pieces of an admitted input before copying its ranges. */
89
+ READ_HELD_RANGES: "read-held-ranges",
105
90
 
106
91
  /** Shut the client down, optionally deleting downloaded data. */
107
92
  /**
@@ -158,6 +143,10 @@ export const Event = {
158
143
  * which put the cue reading in the thread that owns the swarm.
159
144
  */
160
145
  PIECES_ARRIVED: "pieces-arrived",
146
+ /** Available source bytes changed after verification or withdrawal. */
147
+ PIECES_CHANGED: "pieces-changed",
148
+ /** A resolved source handle closed; retained media readings must be released. */
149
+ SOURCE_FORGOTTEN: "source-forgotten",
161
150
 
162
151
  /**
163
152
  * A file has been downloaded whole and written out as a file.
@@ -34,6 +34,8 @@ export function wholeFileStats(length) {
34
34
  fileProgress: 1,
35
35
  fileDownloaded: fileLength,
36
36
  fileLength,
37
+ fileOffset: 0,
38
+ fileAvailable: fileLength > 0,
37
39
  residence: fileLength > 0 ? [{ start: 0, end: fileLength, location: "whole-file" }] : [],
38
40
  resumeNeededBytes: null,
39
41
  resumeDownloadedBytes: null,