@torrent-tv/proxy 2.87.11 → 2.87.13
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/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/transcode-sessions/post.js +13 -0
- package/server.js +725 -725
- package/services/encode/EncodeAdmission.js +45 -1
- package/services/encode/OutputOpening.js +4 -1
- package/services/encode/quality/EncodeCost.js +31 -0
- package/services/encode/quality/output-format.js +58 -9
- package/services/media/audio-inventory.js +245 -245
- package/services/server/ViewerRequests.js +25 -1
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/wire-outputs.js +3 -0
- package/services/storage/piece-store/shared-piece-store.js +50 -30
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/pool-adapter.js +365 -365
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/viewer/Viewer.js +8 -1
- package/test/audio-inventory.test.js +212 -212
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/piece-store-eviction.test.js +37 -2
|
@@ -69,6 +69,7 @@ import { correctForAvailability } from "./available-share.js";
|
|
|
69
69
|
* @property {() => Set<string>} [watchedAddresses] - Every output a present
|
|
70
70
|
* viewer is watching that still has something left to make.
|
|
71
71
|
* @property {(address: string) => OutputLoad | null} loadOf
|
|
72
|
+
* @property {(spec: object, file: object) => OutputLoad | null} [loadForCandidate]
|
|
72
73
|
* @property {() => ({ share: number, known: boolean } | null)} availability
|
|
73
74
|
*/
|
|
74
75
|
|
|
@@ -189,7 +190,50 @@ export class EncodeAdmission {
|
|
|
189
190
|
if (this.#heldWithoutRun().has(address)) {
|
|
190
191
|
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
191
192
|
}
|
|
192
|
-
|
|
193
|
+
return this.#assessCandidate(address, this.#host.loadOf(address));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Whether a not-yet-opened format can take a place on the whole machine.
|
|
198
|
+
* The output choice calls this for each candidate rung; the final claim uses
|
|
199
|
+
* `admitsWatching` with the same arithmetic after registering the output.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} spec
|
|
202
|
+
* @param {object} file
|
|
203
|
+
* @returns {{ admitted: boolean, reason: string, speedX: number | null }}
|
|
204
|
+
*/
|
|
205
|
+
previewCandidate(spec, file) {
|
|
206
|
+
const address = typeof spec?.toKey === "function" ? spec.toKey() : "";
|
|
207
|
+
if (!address) {
|
|
208
|
+
return { admitted: false, reason: "the output has no address", speedX: null };
|
|
209
|
+
}
|
|
210
|
+
if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
|
|
211
|
+
return { admitted: true, reason: "an encoder already runs there", speedX: null };
|
|
212
|
+
}
|
|
213
|
+
if (this.#heldWithoutRun().has(address)) {
|
|
214
|
+
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
215
|
+
}
|
|
216
|
+
const load = this.#host.loadForCandidate?.(spec, file) ?? null;
|
|
217
|
+
return this.#assessCandidate(address, load);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Projected production rate for outputs currently holding machine capacity.
|
|
222
|
+
* A live encoder reading takes precedence; this measured-cost projection
|
|
223
|
+
* supplies the playback trajectory before its first run sample arrives.
|
|
224
|
+
*
|
|
225
|
+
* @returns {number | null}
|
|
226
|
+
*/
|
|
227
|
+
projectedSpeedX() {
|
|
228
|
+
const occupied = this.#occupied(null);
|
|
229
|
+
if (occupied.unpriced.length > 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const speedX = this.#speedAt(occupied.costSec);
|
|
233
|
+
return Number.isFinite(speedX) && speedX > 0 ? speedX : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#assessCandidate(address, load) {
|
|
193
237
|
if (!load || load.costSec === null) {
|
|
194
238
|
return { admitted: false, reason: "the output has no measured encoding cost", speedX: null };
|
|
195
239
|
}
|
|
@@ -45,7 +45,7 @@ export class OutputOpening {
|
|
|
45
45
|
#host;
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* @param {object} host - `logger`, `probeMediaInfo`, `probeKeyframeTimes`, `enabled`, `segmentFormat`, `renditions`, `sourceFiles`, `getCachedMediaInfo`, `localBaseUrl`, `tonemapSupported`, `videoEncoder`, `hostLoad`, `keyframeTables`, `timelines`, `segmentDurationSec`, `softwarePresetBenchmark`, `decodeCostModel`, `encodeCost`, `outputs`, `segmentOutputFiles`, `hostTimings`, `returns`, `encodeRuns`
|
|
48
|
+
* @param {object} host - `logger`, `probeMediaInfo`, `probeKeyframeTimes`, `enabled`, `segmentFormat`, `renditions`, `sourceFiles`, `getCachedMediaInfo`, `localBaseUrl`, `tonemapSupported`, `videoEncoder`, `hostLoad`, `keyframeTables`, `timelines`, `segmentDurationSec`, `softwarePresetBenchmark`, `decodeCostModel`, `encodeCost`, `admission`, `outputs`, `segmentOutputFiles`, `hostTimings`, `returns`, `encodeRuns`
|
|
49
49
|
* @param {SegmentOutputFiles} host.segmentOutputFiles - The storage
|
|
50
50
|
* operations needed while selecting or opening an output.
|
|
51
51
|
*/
|
|
@@ -573,6 +573,9 @@ export class OutputOpening {
|
|
|
573
573
|
requiredSpeed: this.#host.hostLoad.requiredSpeedFor(sourceKey, fileIndex)
|
|
574
574
|
},
|
|
575
575
|
chooseBudget: (params) => this.#host.encodeCost.chooseEncodeBudget(params),
|
|
576
|
+
machineAdmission: this.#host.admission
|
|
577
|
+
? (candidate) => this.#host.admission.previewCandidate(candidate, file)
|
|
578
|
+
: null,
|
|
576
579
|
tonemap: applyTonemap,
|
|
577
580
|
capKbps,
|
|
578
581
|
limitsFor: (frame) => this.#host.limitsFor(frame),
|
|
@@ -329,6 +329,37 @@ export class EncodeCost {
|
|
|
329
329
|
if (!session) {
|
|
330
330
|
return null;
|
|
331
331
|
}
|
|
332
|
+
return this.#loadOfSession(session);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* What a not-yet-opened output would cost, priced by the same readings and
|
|
337
|
+
* rules as an output already in the catalogue. This lets the machine-wide
|
|
338
|
+
* admission answer before the format is committed, so AUTO can select a
|
|
339
|
+
* lower rung that the whole host can sustain.
|
|
340
|
+
*
|
|
341
|
+
* @param {object} spec
|
|
342
|
+
* @param {object} file
|
|
343
|
+
* @returns {{ costSec: number | null, fileKey: string, fileCostSec: number } | null}
|
|
344
|
+
*/
|
|
345
|
+
loadForCandidate(spec, file) {
|
|
346
|
+
if (!spec || !file) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const encode = spec.video?.encode ?? null;
|
|
350
|
+
const candidate = {
|
|
351
|
+
spec,
|
|
352
|
+
file,
|
|
353
|
+
output: {
|
|
354
|
+
encodeWidth: Number(encode?.width) || 0,
|
|
355
|
+
encodeHeight: Number(encode?.height) || 0,
|
|
356
|
+
outputFps: Number(encode?.fps) || TRANSCODE_FPS
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
return this.#loadOfSession(candidate);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#loadOfSession(session) {
|
|
332
363
|
const fileKey = session.file?.key ?? "";
|
|
333
364
|
const fileCost = Number(this.#torrentCostSecFor(session));
|
|
334
365
|
const fileCostSec = Number.isFinite(fileCost) && fileCost > 0 ? fileCost : 0;
|
|
@@ -51,6 +51,8 @@ import { linkAnswerFigures, linkCouldCarry, loadOf, videoLoadOfSpec } from "./li
|
|
|
51
51
|
* @param {unknown} params.benchmark - The startup measurement of the software encoder, or null.
|
|
52
52
|
* @param {{ decodeModel: object | null, observedDecodeCostSec: number | null, requiredSpeed: number | null }} params.cost
|
|
53
53
|
* @param {(params: object) => object | null} params.chooseBudget - The realtime budget's ladder choice.
|
|
54
|
+
* @param {(spec: OutputSpec) => { admitted: boolean, reason: string, speedX: number | null }} [params.machineAdmission] -
|
|
55
|
+
* The proxy's machine-wide capacity decision for a candidate output.
|
|
54
56
|
* @param {boolean} params.tonemap
|
|
55
57
|
* @param {number | null} [params.capKbps] - A nominal limit asked for outright;
|
|
56
58
|
* above the size's own it is refused (`softwareRateControlFor`).
|
|
@@ -86,7 +88,8 @@ export function decideOutputFormat({
|
|
|
86
88
|
limitsFor = (frame) => [nominalKbpsFor(frame)],
|
|
87
89
|
audioLoad = null,
|
|
88
90
|
specWith,
|
|
89
|
-
serving
|
|
91
|
+
serving,
|
|
92
|
+
machineAdmission = null
|
|
90
93
|
}) {
|
|
91
94
|
const judge = (spec) => linkCouldCarry(
|
|
92
95
|
serving.linkMbps,
|
|
@@ -170,6 +173,22 @@ export function decideOutputFormat({
|
|
|
170
173
|
: measured && w > 0
|
|
171
174
|
? pickSoftwarePreset(benchmark, w * h * fps, priced, { width: w, height: h })
|
|
172
175
|
: null);
|
|
176
|
+
const budgetFor = (spec) => {
|
|
177
|
+
const encode = spec.video?.encode;
|
|
178
|
+
if (!budget || !encode) {
|
|
179
|
+
return budget;
|
|
180
|
+
}
|
|
181
|
+
const rungIndex = budget.ladder?.findIndex((rung) =>
|
|
182
|
+
rung.width === encode.width && rung.height === encode.height
|
|
183
|
+
) ?? budget.rungIndex;
|
|
184
|
+
return {
|
|
185
|
+
...budget,
|
|
186
|
+
width: encode.width,
|
|
187
|
+
height: encode.height,
|
|
188
|
+
preset: encode.preset,
|
|
189
|
+
rungIndex: rungIndex >= 0 ? rungIndex : budget.rungIndex
|
|
190
|
+
};
|
|
191
|
+
};
|
|
173
192
|
// Every format at one size this viewer could be given, highest limit first.
|
|
174
193
|
// A hardware encoder is given no limit, so it has exactly one.
|
|
175
194
|
const formatsAt = (w, h) => {
|
|
@@ -220,12 +239,28 @@ export function decideOutputFormat({
|
|
|
220
239
|
}
|
|
221
240
|
}
|
|
222
241
|
|
|
223
|
-
let anySizeHeld =
|
|
224
|
-
|
|
242
|
+
let anySizeHeld = false;
|
|
243
|
+
let machineRefusal = null;
|
|
244
|
+
const admittedByMachine = (spec) => {
|
|
245
|
+
if (!machineCan(spec.video?.encode?.width ?? 0, spec.video?.encode?.height ?? 0)) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const answer = typeof machineAdmission === "function" ? machineAdmission(spec) : null;
|
|
249
|
+
if (answer?.admitted === false) {
|
|
250
|
+
machineRefusal ??= answer;
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
};
|
|
255
|
+
if (machineCan(width, height)) {
|
|
225
256
|
for (const spec of wantedFormats) {
|
|
257
|
+
if (!admittedByMachine(spec)) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
anySizeHeld = true;
|
|
226
261
|
const answer = judge(spec);
|
|
227
262
|
if (answer.admitted) {
|
|
228
|
-
return given(spec, answer, { budget, wantedKey });
|
|
263
|
+
return given(spec, answer, { budget: budgetFor(spec), wantedKey });
|
|
229
264
|
}
|
|
230
265
|
}
|
|
231
266
|
}
|
|
@@ -234,11 +269,14 @@ export function decideOutputFormat({
|
|
|
234
269
|
if (!machineCan(rung.width, rung.height)) {
|
|
235
270
|
continue;
|
|
236
271
|
}
|
|
237
|
-
anySizeHeld = true;
|
|
238
272
|
for (const spec of formatsAt(rung.width, rung.height)) {
|
|
273
|
+
if (!admittedByMachine(spec)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
anySizeHeld = true;
|
|
239
277
|
const answer = judge(spec);
|
|
240
278
|
if (answer.admitted) {
|
|
241
|
-
return given(spec, answer, { wantedKey });
|
|
279
|
+
return given(spec, answer, { budget: budgetFor(spec), wantedKey });
|
|
242
280
|
}
|
|
243
281
|
}
|
|
244
282
|
}
|
|
@@ -247,10 +285,21 @@ export function decideOutputFormat({
|
|
|
247
285
|
// Not the viewer's link: this machine. Said apart, because the answer to
|
|
248
286
|
// it is another proxy, found before anything plays.
|
|
249
287
|
return {
|
|
250
|
-
...refused(
|
|
288
|
+
...refused(
|
|
289
|
+
wantedSpec,
|
|
290
|
+
null,
|
|
291
|
+
machineRefusal?.reason ?? `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
|
|
292
|
+
{ budget, wantedKey }
|
|
293
|
+
),
|
|
251
294
|
unavailable: {
|
|
252
|
-
reason: `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
|
|
253
|
-
figures: {
|
|
295
|
+
reason: machineRefusal?.reason ?? `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
|
|
296
|
+
figures: {
|
|
297
|
+
width,
|
|
298
|
+
height,
|
|
299
|
+
fps,
|
|
300
|
+
encoder: encoder.name,
|
|
301
|
+
...(Number.isFinite(machineRefusal?.speedX) ? { speedX: Number(machineRefusal.speedX.toFixed(3)) } : {})
|
|
302
|
+
},
|
|
254
303
|
bound: "machine"
|
|
255
304
|
}
|
|
256
305
|
};
|