@torrent-tv/proxy 2.19.0 → 2.21.0
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 +11 -0
- package/package.json +1 -1
- package/server.js +3 -6
- package/services/encoder-readings.js +45 -0
- package/services/hls-session-manager.js +129 -16
- package/services/torrent-worker/client.js +510 -497
- package/services/torrent-worker/pool-adapter.js +204 -195
- package/services/torrent-worker/protocol.js +2 -0
- package/services/torrent-worker/worker.js +16 -0
- package/test/encoder-readings.test.js +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 2.21.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: The two costs added in 2.18.0 and 2.19.0 were never measured in production — both features were inert. The torrent's cost read `torrentPool.client`, a field that belongs to the pool implementation that no longer runs on this thread (the WebTorrent client lives on the worker), so the byte totals were always zero and the guard that needs two megabytes of movement never passed. The copy's cost sat in a branch its only caller had already filtered out, so it never ran. The totals now come from the worker over its own protocol, and the caller admits a copying session.
|
|
4
|
+
- **Fix**: A speed is read as the DIFFERENCE between two readings of an uninterrupted stretch, not from ffmpeg's cumulative figure. The cumulative one counts every second the look-ahead cap keeps the encoder stopped, and a copy spends most of its life stopped — it reaches the cap in about fifteen seconds and then waits a minute, so a copy running at 8x reports 1.6x and falling. Filed as the price of copying, that would have refused rungs on a measurement of a pause. The pair is dropped whenever the encoder is paused, resumed or restarted, so every surviving pair spans real work.
|
|
5
|
+
- **Fix**: The torrent's cost is divided by the core count. `process.cpuUsage()` adds up every thread, while everything it is added to is wall seconds per second of video — undivided on the four-core addon host it overstated the torrent fourfold, which on the field's own rung is the difference between offering it and refusing it. Only DOWNLOADED bytes are counted, since a byte sent back to the swarm is neither hashed nor stored, and the file is priced by its own length rather than by the video stream's bitrate — the torrent moves the container, and two or three audio tracks are 10-25 % of it.
|
|
6
|
+
- **Fix**: The offered list is recomputed when either new figure changes, and the FIRST offer — the one a viewer sees on opening a file — is priced with the torrent's cost too. Keyed only on the decode version, the cache could never change for a copied picture, which is precisely the case these costs exist for.
|
|
7
|
+
|
|
8
|
+
## 2.20.0
|
|
9
|
+
|
|
10
|
+
- **Fix**: A file's read window is shared between the readers it has, instead of being granted whole to each. The window is stated in seconds of playback and the piece store's memory is one budget for the whole torrent, so a viewer with a picture and a separately published audio track asked for twice what the budget was written against, and a warm-up made it three times. On 2026-08-15 that ended as it had to: every resident piece held at once, a read that returned zero bytes, and every encoder on the file taking that for the end of it. This is the first step of the sliding window, not the whole of it — pieces still leave memory only by the store's own eviction.
|
|
11
|
+
|
|
1
12
|
## 2.19.0
|
|
2
13
|
|
|
3
14
|
- **New**: What the torrent itself costs this machine is measured and charged. Downloading a file, verifying every piece of it and pushing segments down a data channel are work on the same box as the encoder, they scale with the file's own bitrate, and the budget counted none of it — measured on the addon host with every encoder suspended, the machine was still 20-29 % busy. The figure is taken only while NOTHING is encoding, which is the one moment it can be attributed without arithmetic, and it is expressed per megabyte moved so any file's rate can be priced from it. A viewer's file is then charged at its own byte rate when deciding what quality this host can offer.
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -155,13 +155,10 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin,
|
|
|
155
155
|
// What every torrent here has moved, so the proxy can price its own
|
|
156
156
|
// downloading, hashing and delivery against the machine (roadmap item 7).
|
|
157
157
|
getTorrentTotals: async () => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const downloaded = Number(torrent?.downloaded);
|
|
161
|
-
const uploaded = Number(torrent?.uploaded);
|
|
162
|
-
bytesMoved += (Number.isFinite(downloaded) ? downloaded : 0) + (Number.isFinite(uploaded) ? uploaded : 0);
|
|
158
|
+
if (typeof torrentPool.getTorrentTotals !== "function") {
|
|
159
|
+
return null;
|
|
163
160
|
}
|
|
164
|
-
return
|
|
161
|
+
return torrentPool.getTorrentTotals();
|
|
165
162
|
},
|
|
166
163
|
getSourceStats: async (sourceKey, fileIndex) => {
|
|
167
164
|
const record = sourceRegistry.get(sourceKey);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Turning two readings of a running encoder into a speed.
|
|
3
|
+
*
|
|
4
|
+
* ffmpeg reports `speed=` cumulatively, over the whole run. That figure counts
|
|
5
|
+
* every second the encoder spent SIGSTOPped by the look-ahead cap, and a COPY
|
|
6
|
+
* spends most of its life there — it reaches the cap in about fifteen seconds
|
|
7
|
+
* and then waits a minute. Read that way, a copy running at eight times
|
|
8
|
+
* realtime reports 1.6x and falling; filed as the price of copying, it would
|
|
9
|
+
* refuse quality rungs on arithmetic that had measured a pause.
|
|
10
|
+
*
|
|
11
|
+
* The difference between two readings of an uninterrupted stretch does not have
|
|
12
|
+
* that fault, and it is the same technique the startup benchmarks use.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} EncoderReading
|
|
17
|
+
* @property {number} takenAt - Wall clock, in milliseconds.
|
|
18
|
+
* @property {number} processedSeconds - Output position ffmpeg has reached.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Speed between two readings, or null when the pair cannot answer.
|
|
23
|
+
*
|
|
24
|
+
* @param {EncoderReading | null} previous
|
|
25
|
+
* @param {EncoderReading | null} current
|
|
26
|
+
* @param {number} minimumWindowSec - The narrowest stretch worth dividing by.
|
|
27
|
+
* @returns {number | null} Video seconds produced per second of clock.
|
|
28
|
+
*/
|
|
29
|
+
export function speedFromReadings(previous, current, minimumWindowSec) {
|
|
30
|
+
if (!previous || !current) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const wallSeconds = (current.takenAt - previous.takenAt) / 1000;
|
|
34
|
+
const producedSeconds = current.processedSeconds - previous.processedSeconds;
|
|
35
|
+
if (!Number.isFinite(wallSeconds) || !Number.isFinite(producedSeconds)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
// A window too narrow to divide by, or one in which nothing was produced —
|
|
39
|
+
// the second happens when a run has just been repositioned and has not yet
|
|
40
|
+
// reached the position it is restarting from.
|
|
41
|
+
if (!(wallSeconds >= minimumWindowSec) || !(producedSeconds > 0)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return producedSeconds / wallSeconds;
|
|
45
|
+
}
|
|
@@ -19,6 +19,7 @@ import { createRequire } from "node:module";
|
|
|
19
19
|
import { logger } from "../utils/logger.js";
|
|
20
20
|
import { readKeyframeIndex } from "./container-index/index.js";
|
|
21
21
|
import { readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "./host-load.js";
|
|
22
|
+
import { speedFromReadings } from "./encoder-readings.js";
|
|
22
23
|
|
|
23
24
|
/** Own package version, stamped onto session-start log lines. */
|
|
24
25
|
const PROXY_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -468,6 +469,8 @@ const DEFAULT_STARTUP_WAIT_MS = 5_000;
|
|
|
468
469
|
const BUDGET_CHECK_INTERVAL_MS = 5_000;
|
|
469
470
|
/** Below this, a tick has not moved enough of the torrent to price it. */
|
|
470
471
|
const TORRENT_COST_MIN_MEGABYTES = 2;
|
|
472
|
+
/** The narrowest stretch of uninterrupted encoding a speed may be read from. */
|
|
473
|
+
const LEARN_WINDOW_MIN_SEC = 3;
|
|
471
474
|
// Speed below this (cumulative ffmpeg average) counts as "slow"; recovery to
|
|
472
475
|
// realtime resets the slow window (hysteresis).
|
|
473
476
|
const BUDGET_SPEED_SLOW = 0.95;
|
|
@@ -1170,14 +1173,21 @@ function normalizeLogFileName(fileName, fileIndex) {
|
|
|
1170
1173
|
* the machine must fetch, verify and deliver it while they watch.
|
|
1171
1174
|
*
|
|
1172
1175
|
* @param {HlsSession} session
|
|
1176
|
+
* @param {number | null} fileLengthBytes
|
|
1173
1177
|
* @returns {number | null}
|
|
1174
1178
|
*/
|
|
1175
|
-
function sourceMegabytesPerSecond(session) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
+
function sourceMegabytesPerSecond(session, fileLengthBytes) {
|
|
1180
|
+
// The FILE's rate, not the video stream's. What the torrent moves is the
|
|
1181
|
+
// container: on the releases this serves, two or three AC-3 tracks add 10-25 %
|
|
1182
|
+
// to what the picture alone would suggest, and `sourceDecode` deliberately
|
|
1183
|
+
// carries the video stream's own bitrate because the decode model was fitted
|
|
1184
|
+
// on video-only clips.
|
|
1185
|
+
const fileLength = Number(fileLengthBytes);
|
|
1186
|
+
const durationSeconds = Number(session.durationSeconds);
|
|
1187
|
+
if (Number.isFinite(fileLength) && fileLength > 0 && Number.isFinite(durationSeconds) && durationSeconds > 0) {
|
|
1188
|
+
return fileLength / durationSeconds / 1e6;
|
|
1179
1189
|
}
|
|
1180
|
-
return
|
|
1190
|
+
return null;
|
|
1181
1191
|
}
|
|
1182
1192
|
|
|
1183
1193
|
export class HlsSessionManager {
|
|
@@ -1226,6 +1236,14 @@ export class HlsSessionManager {
|
|
|
1226
1236
|
#hostLoadSample = null;
|
|
1227
1237
|
/** The previous reading taken while nothing was encoding, for the torrent's own cost. */
|
|
1228
1238
|
#idleLoadSample = null;
|
|
1239
|
+
/**
|
|
1240
|
+
* How long each file is in bytes, from the stats call the read window already
|
|
1241
|
+
* makes. The torrent moves the CONTAINER, so this — not the video stream's
|
|
1242
|
+
* bitrate — is what its work should be priced against.
|
|
1243
|
+
*
|
|
1244
|
+
* @type {Map<string, number>}
|
|
1245
|
+
*/
|
|
1246
|
+
#fileLengthByKey = new Map();
|
|
1229
1247
|
/** Seconds of this process's CPU per megabyte the torrent moves, once measured. */
|
|
1230
1248
|
#observedTorrentCostPerMegabyte = null;
|
|
1231
1249
|
/** @type {number[]} Recent readings behind that median. */
|
|
@@ -2475,6 +2493,9 @@ export class HlsSessionManager {
|
|
|
2475
2493
|
* which leaves the reader on its own default.
|
|
2476
2494
|
*/
|
|
2477
2495
|
async #readWindowBytesFor(sourceKey, fileIndex, durationSeconds) {
|
|
2496
|
+
// The length read here is also what prices the torrent's own work for this
|
|
2497
|
+
// file, so it is remembered rather than discarded.
|
|
2498
|
+
|
|
2478
2499
|
if (!this.getSourceStats || !Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
2479
2500
|
return 0;
|
|
2480
2501
|
}
|
|
@@ -2488,11 +2509,44 @@ export class HlsSessionManager {
|
|
|
2488
2509
|
if (!Number.isFinite(fileLength) || fileLength <= 0) {
|
|
2489
2510
|
return 0;
|
|
2490
2511
|
}
|
|
2512
|
+
this.#fileLengthByKey.set(`${sourceKey}:${fileIndex}`, fileLength);
|
|
2491
2513
|
const bytesPerSecond = fileLength / durationSeconds;
|
|
2492
|
-
|
|
2514
|
+
// Shared between the readers this file already has. The window is stated in
|
|
2515
|
+
// seconds of playback and the store's memory is one budget for the whole
|
|
2516
|
+
// torrent, so N readers asking for thirty seconds each ask for N times what
|
|
2517
|
+
// was provided for — and on 2026-08-15 that is exactly what happened: a
|
|
2518
|
+
// viewer with a picture and an audio track had every resident piece held at
|
|
2519
|
+
// once, a read ended with zero bytes, and every encoder on the file took
|
|
2520
|
+
// that for the end of it.
|
|
2521
|
+
//
|
|
2522
|
+
// Dividing keeps the promise the budget was written against. It is not the
|
|
2523
|
+
// sliding window of roadmap item 8 — pieces still leave only by the store's
|
|
2524
|
+
// own eviction — but it removes the multiplication that broke it.
|
|
2525
|
+
const readers = Math.max(1, this.#readersOn(sourceKey, fileIndex));
|
|
2526
|
+
const wanted = Math.round((bytesPerSecond * READ_WINDOW_SECONDS) / readers);
|
|
2493
2527
|
return Math.min(READ_WINDOW_MAX_BYTES, Math.max(READ_WINDOW_MIN_BYTES, wanted));
|
|
2494
2528
|
}
|
|
2495
2529
|
|
|
2530
|
+
/**
|
|
2531
|
+
* How many live sessions read this file: the picture, any rung being warmed
|
|
2532
|
+
* beside it, and any audio track published on its own.
|
|
2533
|
+
*
|
|
2534
|
+
* @param {string} sourceKey
|
|
2535
|
+
* @param {number} fileIndex
|
|
2536
|
+
* @returns {number}
|
|
2537
|
+
*/
|
|
2538
|
+
#readersOn(sourceKey, fileIndex) {
|
|
2539
|
+
let readers = 0;
|
|
2540
|
+
for (const session of this.sessionsById.values()) {
|
|
2541
|
+
if (session?.sourceKey === sourceKey &&
|
|
2542
|
+
session.fileIndex === fileIndex &&
|
|
2543
|
+
session.state !== "disposed") {
|
|
2544
|
+
readers += 1;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return readers;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2496
2550
|
#enforceLookAhead() {
|
|
2497
2551
|
for (const session of this.sessionsById.values()) {
|
|
2498
2552
|
this.#enforceLookAheadFor(session);
|
|
@@ -2642,6 +2696,8 @@ export class HlsSessionManager {
|
|
|
2642
2696
|
* @returns {void}
|
|
2643
2697
|
*/
|
|
2644
2698
|
#pauseEncoder(session, reason) {
|
|
2699
|
+
// Any pair spanning this would count a stopped encoder as slow.
|
|
2700
|
+
session.learnSample = null;
|
|
2645
2701
|
if (session.encoderPaused || session.encoderPauseUnsupported || !session.ffmpeg?.pid) {
|
|
2646
2702
|
return;
|
|
2647
2703
|
}
|
|
@@ -2670,6 +2726,8 @@ export class HlsSessionManager {
|
|
|
2670
2726
|
* @returns {void}
|
|
2671
2727
|
*/
|
|
2672
2728
|
#resumeEncoder(session, reason) {
|
|
2729
|
+
// Any pair spanning this would count a stopped encoder as slow.
|
|
2730
|
+
session.learnSample = null;
|
|
2673
2731
|
if (!session.encoderPaused || !session.ffmpeg?.pid) {
|
|
2674
2732
|
return;
|
|
2675
2733
|
}
|
|
@@ -2720,7 +2778,13 @@ export class HlsSessionManager {
|
|
|
2720
2778
|
}
|
|
2721
2779
|
const elapsedSec = (now.takenAt - previous.takenAt) / 1000;
|
|
2722
2780
|
const megabytes = (now.bytes - previous.bytes) / 1e6;
|
|
2723
|
-
|
|
2781
|
+
// Divided by the cores, because `process.cpuUsage()` adds up every thread
|
|
2782
|
+
// while everything this figure is later added to is measured in WALL
|
|
2783
|
+
// seconds per second of video. Left undivided on the four-core addon host
|
|
2784
|
+
// it overstated the torrent by four times, which on the field's own rung
|
|
2785
|
+
// was the difference between offering it and refusing it.
|
|
2786
|
+
const cores = Math.max(1, os.cpus().length);
|
|
2787
|
+
const cpuSeconds = (now.cpuSeconds - previous.cpuSeconds) / cores;
|
|
2724
2788
|
// Enough movement to divide by: a tick with almost nothing downloaded
|
|
2725
2789
|
// measures the idle loop, not the torrent.
|
|
2726
2790
|
if (!(elapsedSec > 0) || !(megabytes >= TORRENT_COST_MIN_MEGABYTES) || !(cpuSeconds > 0)) {
|
|
@@ -2754,8 +2818,12 @@ export class HlsSessionManager {
|
|
|
2754
2818
|
}
|
|
2755
2819
|
try {
|
|
2756
2820
|
const totals = await this.getTorrentTotals();
|
|
2757
|
-
|
|
2758
|
-
|
|
2821
|
+
// Downloaded bytes only. Every one of them is verified against the piece
|
|
2822
|
+
// hash and written to the store; a byte sent back to the swarm is neither,
|
|
2823
|
+
// and adding the two would price both at whatever the mixture happened to
|
|
2824
|
+
// be on the day.
|
|
2825
|
+
const downloaded = Number(totals?.downloaded);
|
|
2826
|
+
return Number.isFinite(downloaded) ? downloaded : null;
|
|
2759
2827
|
} catch {
|
|
2760
2828
|
return null; // the pool is busy or gone; a reading missed is not a fault
|
|
2761
2829
|
}
|
|
@@ -3076,6 +3144,9 @@ export class HlsSessionManager {
|
|
|
3076
3144
|
* @returns {Promise<void>}
|
|
3077
3145
|
*/
|
|
3078
3146
|
async #startEncodeRun(session, startIndex) {
|
|
3147
|
+
// A new run starts its own reckoning: a pair spanning the restart would
|
|
3148
|
+
// count the gap between two runs as slow encoding.
|
|
3149
|
+
session.learnSample = null;
|
|
3079
3150
|
// Where a restart's seconds go. A seek costs 5-8 s in the field and the
|
|
3080
3151
|
// recorded reason — waiting for the previous ffmpeg to exit, measured at
|
|
3081
3152
|
// 0.54-1.47 s — does not account for it. Before rebuilding the hottest path
|
|
@@ -4652,7 +4723,14 @@ export class HlsSessionManager {
|
|
|
4652
4723
|
// Everything else is fixed for the session's life.
|
|
4653
4724
|
const observed = this.#observedDecodeCost.get(`${owner.sourceKey}:${owner.fileIndex}`) ?? null;
|
|
4654
4725
|
const playing = this.variantHeightOf(this.#activeVariant(owner));
|
|
4655
|
-
|
|
4726
|
+
// Everything the answer is derived from belongs in what identifies it. The
|
|
4727
|
+
// copy's price and the torrent's are inputs now, and left out of this key
|
|
4728
|
+
// the menu would keep the answer computed before either was measured — on
|
|
4729
|
+
// a copied picture, which is the case they exist for, the decode version
|
|
4730
|
+
// never moves at all, so the cache would never be recomputed.
|
|
4731
|
+
const copyVersion = this.#observedCopyCost.get(`${owner.sourceKey}:${owner.fileIndex}`)?.version ?? 0;
|
|
4732
|
+
const torrentCost = this.#observedTorrentCostPerMegabyte ?? 0;
|
|
4733
|
+
const version = `${observed?.version ?? 0}:${playing}:${copyVersion}:${torrentCost.toFixed(6)}`;
|
|
4656
4734
|
if (Array.isArray(owner.offeredHeightsCache) && owner.offeredHeightsVersion === version) {
|
|
4657
4735
|
return owner.offeredHeightsCache;
|
|
4658
4736
|
}
|
|
@@ -4760,19 +4838,42 @@ export class HlsSessionManager {
|
|
|
4760
4838
|
session.state === "failed" ||
|
|
4761
4839
|
!session.ffmpeg ||
|
|
4762
4840
|
session.encoderPaused === true ||
|
|
4763
|
-
session.
|
|
4841
|
+
session.audioOnly === true
|
|
4764
4842
|
) {
|
|
4843
|
+
// An audio rendition is excluded because its speed is the price of a
|
|
4844
|
+
// soundtrack, which neither decoding nor copying the picture is. A COPY
|
|
4845
|
+
// is NOT excluded any more: it says what copying costs, and that used to
|
|
4846
|
+
// be counted as nothing.
|
|
4847
|
+
session.learnSample = null;
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
// Measured as a DELTA between two readings of a run that was going for the
|
|
4851
|
+
// whole interval, not from ffmpeg's cumulative `speed=`. The cumulative
|
|
4852
|
+
// figure counts every second the encoder spent SIGSTOPped by the look-ahead
|
|
4853
|
+
// cap in its denominator, and a copy spends most of its life there — it
|
|
4854
|
+
// reaches the cap in about fifteen seconds and then waits a minute. Read
|
|
4855
|
+
// that way a copy running at 8x reports 1.6x and falling, which would be
|
|
4856
|
+
// filed as the price of copying and refuse rungs on arithmetic that
|
|
4857
|
+
// measured a pause.
|
|
4858
|
+
const processedSeconds = Number(session.progress?.processedSeconds);
|
|
4859
|
+
const takenAt = Date.now();
|
|
4860
|
+
const previous = session.learnSample ?? null;
|
|
4861
|
+
session.learnSample = { takenAt, processedSeconds };
|
|
4862
|
+
if (previous === null || !Number.isFinite(processedSeconds) || !Number.isFinite(previous.processedSeconds)) {
|
|
4765
4863
|
return;
|
|
4766
4864
|
}
|
|
4767
|
-
const speed =
|
|
4768
|
-
if (speed === null
|
|
4865
|
+
const speed = speedFromReadings(previous, { takenAt, processedSeconds }, LEARN_WINDOW_MIN_SEC);
|
|
4866
|
+
if (speed === null) {
|
|
4769
4867
|
return;
|
|
4770
4868
|
}
|
|
4771
4869
|
if (speed < BUDGET_SPEED_OK && await this.#classifyTranscodeBound(session) === "download") {
|
|
4772
4870
|
return; // the torrent is what is short; this says nothing about the host
|
|
4773
4871
|
}
|
|
4774
|
-
session.
|
|
4775
|
-
|
|
4872
|
+
if (session.transcodeVideo === true) {
|
|
4873
|
+
this.#learnDecodeCost(session, speed);
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4876
|
+
await this.#learnCopyCost(session, speed);
|
|
4776
4877
|
}
|
|
4777
4878
|
|
|
4778
4879
|
/**
|
|
@@ -4949,9 +5050,18 @@ export class HlsSessionManager {
|
|
|
4949
5050
|
const observedDecodeCostSec = mediaInfo?.sourceKey !== undefined
|
|
4950
5051
|
? (this.#observedDecodeCost.get(`${mediaInfo.sourceKey}:${mediaInfo.fileIndex}`)?.costSec ?? null)
|
|
4951
5052
|
: null;
|
|
5053
|
+
// What this file costs the machine merely by being fetched and delivered is
|
|
5054
|
+
// known before any session exists, so the FIRST offer — the one the viewer
|
|
5055
|
+
// actually sees when they open a file — is priced with it too. Without this
|
|
5056
|
+
// the plan and a live session answer differently about the same file.
|
|
5057
|
+
const torrentCostSec = this.#observedTorrentCostPerMegabyte !== null && mediaInfo?.fileLength > 0 &&
|
|
5058
|
+
mediaInfo?.durationSeconds > 0
|
|
5059
|
+
? this.#observedTorrentCostPerMegabyte * (mediaInfo.fileLength / mediaInfo.durationSeconds / 1e6)
|
|
5060
|
+
: 0;
|
|
4952
5061
|
const forBranch = (transcodeVideo) =>
|
|
4953
5062
|
this.#sustainableHeights({
|
|
4954
5063
|
heights,
|
|
5064
|
+
concurrentCostSec: torrentCostSec,
|
|
4955
5065
|
observedDecodeCostSec,
|
|
4956
5066
|
// Nothing is running yet, so nothing is exempt from being predicted —
|
|
4957
5067
|
// except the copy itself, which the branch flag already covers.
|
|
@@ -5008,7 +5118,10 @@ export class HlsSessionManager {
|
|
|
5008
5118
|
// per megabyte from readings taken while nothing was encoding, so the two
|
|
5009
5119
|
// measurements do not contain each other.
|
|
5010
5120
|
const perMegabyte = this.#observedTorrentCostPerMegabyte;
|
|
5011
|
-
const megabytesPerSecond = sourceMegabytesPerSecond(
|
|
5121
|
+
const megabytesPerSecond = sourceMegabytesPerSecond(
|
|
5122
|
+
session,
|
|
5123
|
+
this.#fileLengthByKey.get(`${session.sourceKey}:${session.fileIndex}`) ?? null
|
|
5124
|
+
);
|
|
5012
5125
|
if (perMegabyte !== null && megabytesPerSecond !== null) {
|
|
5013
5126
|
cost += perMegabyte * megabytesPerSecond;
|
|
5014
5127
|
}
|