@torrent-tv/proxy 2.80.8 → 2.80.10
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 +1675 -1658
- package/bin/cli.js +606 -596
- package/package.json +51 -50
- package/services/data-channel-handler.js +2 -4
- package/services/encode/CoverageMap.js +428 -401
- package/services/encode/EncodePlan.js +0 -3
- package/services/encode/EncodeRun.js +14 -0
- package/services/encode/SegmentStore.js +718 -669
- package/services/hls-session-manager.js +44 -162
- package/services/hwaccel.js +2 -2
- package/services/memory-report.js +596 -592
- package/services/orchestrators/EncodeOrchestrator.js +143 -11
- package/services/quality/EncodeCost.js +555 -555
- package/test/auto-quality-step.test.js +514 -507
- package/test/contention.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +187 -0
- package/test/coverage-map.test.js +195 -178
- package/test/encode-orchestrator.test.js +1 -2
- package/test/encode-plan-viewers.test.js +18 -19
- package/test/encode-plan.test.js +3 -4
- package/test/held-request-width.test.js +155 -154
- package/test/helpers/encode-run.js +136 -128
- package/test/orchestrator-wired.test.js +16 -11
- package/test/produced-copy-choice.test.js +358 -327
- package/test/run-intervals.test.js +100 -100
- package/test/segment-serve-wiring.test.js +76 -20
- package/test/segment-store.test.js +216 -187
- package/test/stale-request-after-seek.test.js +51 -77
- package/test/tracks-begin-together.test.js +176 -153
- package/test/viewer-outputs.test.js +278 -273
- package/test/behind-head-repair.test.js +0 -261
- /package/services/{contention.js → encode/contention.js} +0 -0
- /package/{test/decode-cost.test.js → test-measured/decode-cost.measured.js} +0 -0
- /package/{test/decode-measurement.test.js → test-measured/decode-measurement.measured.js} +0 -0
|
@@ -21,7 +21,7 @@ import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
|
21
21
|
import { readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "./host-load.js";
|
|
22
22
|
import { speedFromReadings } from "./encoder-readings.js";
|
|
23
23
|
import { availableShareFrom } from "./available-share.js";
|
|
24
|
-
import { contentionPenalty } from "./contention.js";
|
|
24
|
+
import { contentionPenalty } from "./encode/contention.js";
|
|
25
25
|
import { minimumBufferFrom } from "./supply-margin.js";
|
|
26
26
|
import { PriorityOrchestrator } from "./priority/PriorityOrchestrator.js";
|
|
27
27
|
import { baseDrawFrom, costPerMegabyteFrom } from "./torrent-cost.js";
|
|
@@ -468,21 +468,16 @@ const SEEK_SETTLE_MS = 300;
|
|
|
468
468
|
// has waited: repetition is the player saying it still needs this exact
|
|
469
469
|
// segment, while a delay only says time has passed. The floor below stays as a
|
|
470
470
|
// last guard against acting on a single stray poll.
|
|
471
|
-
const BEHIND_HEAD_REPAIR_MIN_ASKS = 2;
|
|
472
471
|
// More distinct indices than this behind the head at once is the player
|
|
473
472
|
// scanning the playlist rather than waiting for a frame.
|
|
474
|
-
const BEHIND_HEAD_SCAN_INDICES = 3;
|
|
475
473
|
// The window the count above is taken over. A player's scan lands inside half a
|
|
476
474
|
// second (field log 2026-08-02); a viewer waiting asks every few seconds.
|
|
477
|
-
const BEHIND_HEAD_SCAN_WINDOW_MS = 2_000;
|
|
478
|
-
const BEHIND_HEAD_REPAIR_MS = 400;
|
|
479
475
|
// How far behind the run a request may be and still be treated as the encoder
|
|
480
476
|
// standing in the wrong place rather than as a player scanning the playlist. A
|
|
481
477
|
// misplaced run is out by at most the buffer the player was holding — measured
|
|
482
478
|
// 2026-08-11 at 14 segments — while a scan probe is out by anything at all.
|
|
483
479
|
// Generous against that measurement, and far short of the hundreds of segments
|
|
484
480
|
// a scan reaches.
|
|
485
|
-
const BEHIND_HEAD_REPAIR_MAX_SEGMENTS = 60;
|
|
486
481
|
// How far the accounting of a backward restart looks for work about to be done
|
|
487
482
|
// twice. It runs on the restart path and a session an hour in has thousands of
|
|
488
483
|
// segments; the figure is for a comparison, not an inventory.
|
|
@@ -3798,10 +3793,7 @@ export class HlsSessionManager {
|
|
|
3798
3793
|
}
|
|
3799
3794
|
byOutput.set(address, [...(byOutput.get(address) ?? []), session]);
|
|
3800
3795
|
}
|
|
3801
|
-
const staleAfterMs = this.presenceStaleAfterMs();
|
|
3802
|
-
const now = Date.now();
|
|
3803
3796
|
for (const [address, sessions] of byOutput) {
|
|
3804
|
-
const coverage = this.encodeOrchestrator.coverageOf(address);
|
|
3805
3797
|
// From the TIMELINE, which is where how a file is cut has lived since
|
|
3806
3798
|
// 2.76.0. Read off the session it left, this was `undefined` on every
|
|
3807
3799
|
// session ever made: the map then held no length, and the walk that
|
|
@@ -3810,9 +3802,8 @@ export class HlsSessionManager {
|
|
|
3810
3802
|
// 2026-09-05 with the stack read out of the live process.
|
|
3811
3803
|
const segmentCount = Number(sessions[0].timeline?.segmentCount) || 0;
|
|
3812
3804
|
if (segmentCount > 0) {
|
|
3813
|
-
|
|
3805
|
+
this.encodeOrchestrator.setSegmentCount(address, segmentCount);
|
|
3814
3806
|
}
|
|
3815
|
-
coverage.markReadyAll(this.segmentStore.provenNumbers(address));
|
|
3816
3807
|
for (const session of sessions) {
|
|
3817
3808
|
for (const run of liveRunsOf(session)) {
|
|
3818
3809
|
this.encodeOrchestrator.adopt(address, run);
|
|
@@ -5418,22 +5409,12 @@ export class HlsSessionManager {
|
|
|
5418
5409
|
* @returns {number} The last number to work through, or `-1` for the end.
|
|
5419
5410
|
*/
|
|
5420
5411
|
#runEndFrom(session, startIndex, exceptRun = null) {
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
if (segmentCount > 0) {
|
|
5428
|
-
coverage.setSegmentCount(segmentCount);
|
|
5429
|
-
}
|
|
5430
|
-
coverage.markReadyAll(this.segmentStore.provenNumbers(key));
|
|
5431
|
-
const free = coverage.freeRunFrom(Math.max(0, startIndex), exceptRun);
|
|
5432
|
-
if (!Number.isFinite(free)) {
|
|
5433
|
-
return -1;
|
|
5434
|
-
}
|
|
5435
|
-
const end = Math.max(0, startIndex) + Math.max(1, free) - 1;
|
|
5436
|
-
return segmentCount > 0 && end >= segmentCount - 1 ? -1 : end;
|
|
5412
|
+
return this.encodeOrchestrator.freeStretchEnd({
|
|
5413
|
+
address: session.outputKey ?? "",
|
|
5414
|
+
from: startIndex,
|
|
5415
|
+
exceptRun,
|
|
5416
|
+
segmentCount: Number(session.timeline?.segmentCount) || 0
|
|
5417
|
+
});
|
|
5437
5418
|
}
|
|
5438
5419
|
|
|
5439
5420
|
// Returns the encoder it built, or nothing when there was nothing to build.
|
|
@@ -5497,7 +5478,6 @@ export class HlsSessionManager {
|
|
|
5497
5478
|
// for the seek that should move the encoder. It also stops the map growing
|
|
5498
5479
|
// for the life of a session.
|
|
5499
5480
|
session.firstWantedAt = new Map();
|
|
5500
|
-
session.behindHeadAsks = new Map();
|
|
5501
5481
|
// This attempt, so that a newer one can tell it has been overtaken. It used
|
|
5502
5482
|
// to be a counter compared against a copy of itself; the attempt is a thing,
|
|
5503
5483
|
// and comparing the thing says the same without a number to keep in step.
|
|
@@ -5960,16 +5940,6 @@ export class HlsSessionManager {
|
|
|
5960
5940
|
if (!session.firstWantedAt.has(index)) {
|
|
5961
5941
|
session.firstWantedAt.set(index, Date.now());
|
|
5962
5942
|
}
|
|
5963
|
-
// How often each index behind the run has been asked for, and how many
|
|
5964
|
-
// distinct ones there are. The repair reads both: one index asked twice is
|
|
5965
|
-
// a viewer waiting, a dozen asked once each is the player scanning. Kept
|
|
5966
|
-
// only for what is behind the head — everything ahead is ordinary
|
|
5967
|
-
// read-ahead — and cleared with each run, like the record above.
|
|
5968
|
-
if (index < (earliestRunStart(session) ?? 0)) {
|
|
5969
|
-
session.behindHeadAsks ??= new Map();
|
|
5970
|
-
const asked = session.behindHeadAsks.get(index);
|
|
5971
|
-
session.behindHeadAsks.set(index, { count: (asked?.count ?? 0) + 1, at: Date.now() });
|
|
5972
|
-
}
|
|
5973
5943
|
if (!session || session.state === "disposed" || index < 0) {
|
|
5974
5944
|
return;
|
|
5975
5945
|
}
|
|
@@ -6015,7 +5985,17 @@ export class HlsSessionManager {
|
|
|
6015
5985
|
// should move the encoder. Only a request still unanswerable after that is
|
|
6016
5986
|
// repaired here.
|
|
6017
5987
|
if (index < head) {
|
|
6018
|
-
|
|
5988
|
+
// SAID, NOT ACTED ON. What is missing in front of a viewer is stated by
|
|
5989
|
+
// the priority map, and putting encoders on it is the plan's work. This
|
|
5990
|
+
// used to move the encoder itself, from a segment REQUEST — a second
|
|
5991
|
+
// authority over where encoders go, with six chosen constants of its own,
|
|
5992
|
+
// and it survived the pass that removed the other two because it lives in
|
|
5993
|
+
// the path that answers a file rather than in the plan.
|
|
5994
|
+
this.#explainHold(
|
|
5995
|
+
session,
|
|
5996
|
+
session.segmentFormat.segmentFileName(index),
|
|
5997
|
+
`it is behind the run (#${head}); where the viewers are is what places encoders`
|
|
5998
|
+
);
|
|
6019
5999
|
return;
|
|
6020
6000
|
}
|
|
6021
6001
|
// Circuit breaker: this exact target has already failed MAX_FAILED_STARTS
|
|
@@ -6049,120 +6029,6 @@ export class HlsSessionManager {
|
|
|
6049
6029
|
// See research/hls-seek-prior-art-2026-08-02.md.
|
|
6050
6030
|
}
|
|
6051
6031
|
|
|
6052
|
-
/**
|
|
6053
|
-
* Move the encoder back to a segment it can no longer produce.
|
|
6054
|
-
*
|
|
6055
|
-
* A run only ever goes forward from where it began, so a request below that
|
|
6056
|
-
* point is not a claim the run may yet reach — it is a hole, and holding it
|
|
6057
|
-
* holds it for ever. Measured 2026-08-11: a run placed at #770 while the
|
|
6058
|
-
* player needed #757 held that request for two minutes forty-one, producing
|
|
6059
|
-
* 409 s of video nobody had asked for.
|
|
6060
|
-
*
|
|
6061
|
-
* Deliberately narrow, because moving the encoder from a segment REQUEST is
|
|
6062
|
-
* exactly what this codebase removed once already: a player that cannot get
|
|
6063
|
-
* what it wants scans the playlist, and those probes are scattered across the
|
|
6064
|
-
* whole file (field log: #178, #681, #725, #807, #74, #245, #387 within half
|
|
6065
|
-
* a second). Steering on the lowest of them put the encoder at the start of
|
|
6066
|
-
* the film and left the viewer's own requests unreachable ahead of it.
|
|
6067
|
-
*
|
|
6068
|
-
* What separates the two: a run placed wrongly is out by at most the buffer
|
|
6069
|
-
* the player had — 14 segments in the measured case — while a scan probe is
|
|
6070
|
-
* out by anything at all. So only a request within reach behind the head is
|
|
6071
|
-
* repaired; the rest are what they always were, claims that answer 503.
|
|
6072
|
-
*
|
|
6073
|
-
* @param {HlsSession} session
|
|
6074
|
-
* @param {number} index - The segment being held.
|
|
6075
|
-
* @param {number} head - Where the current run begins.
|
|
6076
|
-
* @returns {void}
|
|
6077
|
-
*/
|
|
6078
|
-
#repairBehindHead(session, index, head) {
|
|
6079
|
-
if (head - index > BEHIND_HEAD_REPAIR_MAX_SEGMENTS) {
|
|
6080
|
-
return;
|
|
6081
|
-
}
|
|
6082
|
-
// Nothing is encoding: a rung the viewer has switched away from is left
|
|
6083
|
-
// exactly so, and its held requests must not bring its encoder back.
|
|
6084
|
-
if (!processCanBeSignalled(runStateOf(session))) {
|
|
6085
|
-
return;
|
|
6086
|
-
}
|
|
6087
|
-
// A seek already settling is about to move the encoder to where the VIEWER
|
|
6088
|
-
// said they are. That statement outranks anything inferred here.
|
|
6089
|
-
if (session.seekSettleTimer != null) {
|
|
6090
|
-
return;
|
|
6091
|
-
}
|
|
6092
|
-
// And it outranks it AFTERWARDS too, which is what was missing. The guard
|
|
6093
|
-
// above only holds while the settle timer is armed — a second later it is
|
|
6094
|
-
// gone, and a request the browser issued BEFORE the seek is then treated as
|
|
6095
|
-
// fresh evidence. Field 2026-08-17: a seek to 2083.4 s put both runs at
|
|
6096
|
-
// #373, a request for #371 from before it arrived a second afterwards, and
|
|
6097
|
-
// this repair moved the encoder to #370 — three segments behind the viewer,
|
|
6098
|
-
// who waited for it to come back. A request BEHIND what the viewer
|
|
6099
|
-
// themselves reported cannot be describing where they are.
|
|
6100
|
-
const reportedSeconds = Number(session.viewerReportedSeconds);
|
|
6101
|
-
if (Number.isFinite(reportedSeconds)) {
|
|
6102
|
-
const reportedIndex = this.#segmentIndexForTime(session, reportedSeconds);
|
|
6103
|
-
if (index < reportedIndex) {
|
|
6104
|
-
this.#explainHold(
|
|
6105
|
-
session,
|
|
6106
|
-
session.segmentFormat.segmentFileName(index),
|
|
6107
|
-
`it is behind #${reportedIndex}, where the viewer said they are — answered, not obeyed`
|
|
6108
|
-
);
|
|
6109
|
-
return;
|
|
6110
|
-
}
|
|
6111
|
-
}
|
|
6112
|
-
// What separates a request the viewer is waiting for from the player
|
|
6113
|
-
// scanning the playlist is not TIME but what else it is asking for. On a
|
|
6114
|
-
// seek hls.js fires dozens of DIFFERENT indices within half a second (field
|
|
6115
|
-
// log: #178, #681, #725, #807, #74, #245, #387) and abandons them all; a
|
|
6116
|
-
// viewer waiting for audio asks for the SAME one, over and over, because it
|
|
6117
|
-
// is the only thing that will let playback continue.
|
|
6118
|
-
//
|
|
6119
|
-
// So: this index has been asked for at least twice, and it is the only
|
|
6120
|
-
// thing behind the head being asked for. Both are facts about the traffic,
|
|
6121
|
-
// available at once, where a delay is a guess about it — and it was three
|
|
6122
|
-
// seconds of the twenty a track change cost on 2026-08-15.
|
|
6123
|
-
const asked = session.behindHeadAsks?.get(index)?.count ?? 0;
|
|
6124
|
-
if (asked < BEHIND_HEAD_REPAIR_MIN_ASKS) {
|
|
6125
|
-
return;
|
|
6126
|
-
}
|
|
6127
|
-
// Counted over a WINDOW, not over the run: a scan is many indices at once,
|
|
6128
|
-
// while the same map left to accumulate would eventually hold every
|
|
6129
|
-
// behind-head request a long run ever saw and switch the repair off for
|
|
6130
|
-
// good.
|
|
6131
|
-
const scanSince = Date.now() - BEHIND_HEAD_SCAN_WINDOW_MS;
|
|
6132
|
-
let distinctBehind = 0;
|
|
6133
|
-
for (const record of session.behindHeadAsks?.values() ?? []) {
|
|
6134
|
-
if (record.at >= scanSince) {
|
|
6135
|
-
distinctBehind += 1;
|
|
6136
|
-
}
|
|
6137
|
-
}
|
|
6138
|
-
if (distinctBehind > BEHIND_HEAD_SCAN_INDICES) {
|
|
6139
|
-
// A scan, not a wait. Moving the encoder to one of these is moving it to
|
|
6140
|
-
// a number the player picked at random.
|
|
6141
|
-
return;
|
|
6142
|
-
}
|
|
6143
|
-
const wantedAt = session.firstWantedAt?.get(index);
|
|
6144
|
-
if (!Number.isFinite(wantedAt) || Date.now() - wantedAt < BEHIND_HEAD_REPAIR_MS) {
|
|
6145
|
-
return;
|
|
6146
|
-
}
|
|
6147
|
-
const target = Math.max(0, index - SEEK_BACKOFF_SEGMENTS);
|
|
6148
|
-
// The breaker has already refused this target repeatedly. Re-arming for it
|
|
6149
|
-
// would log and re-arm on every poll for as long as the request is held,
|
|
6150
|
-
// and move nothing.
|
|
6151
|
-
if (target === session.failedStartAt && session.failedStartCount >= MAX_FAILED_STARTS) {
|
|
6152
|
-
return;
|
|
6153
|
-
}
|
|
6154
|
-
logger.warn(
|
|
6155
|
-
`transcode ${session.id} segment #${index} is behind the run (#${head}) and has waited ` +
|
|
6156
|
-
`${Date.now() - wantedAt}ms — nothing this run does can produce it; moving the encoder there`
|
|
6157
|
-
);
|
|
6158
|
-
// Through the same settle a viewer's own seek goes through, so a burst of
|
|
6159
|
-
// behind-head requests produces one restart and not one each.
|
|
6160
|
-
session.seekTarget = target;
|
|
6161
|
-
session.seekFirstFarAt = Date.now();
|
|
6162
|
-
session.seekSettleTimer = setTimeout(() => this.#fireSettledSeek(session), SEEK_SETTLE_MS);
|
|
6163
|
-
session.seekSettleTimer.unref?.();
|
|
6164
|
-
}
|
|
6165
|
-
|
|
6166
6032
|
/**
|
|
6167
6033
|
* Fire a settled server-side seek: restart the encoder once at the target
|
|
6168
6034
|
* recorded during the settle window. Enforces the restart cooldown as a
|
|
@@ -8057,8 +7923,13 @@ export class HlsSessionManager {
|
|
|
8057
7923
|
// made — so the piece this one had open must not be left looking like one
|
|
8058
7924
|
// of them. Not awaited: the caller's own work does not depend on it, and
|
|
8059
7925
|
// the wait is for a process that has already been told to go.
|
|
7926
|
+
// WHAT THE RUN ITSELF NAMED, which is the only thing that can say a piece
|
|
7927
|
+
// is whole — a piece cut short still decodes. Written 2026-09-06 and never
|
|
7928
|
+
// passed from here, so the clearing-up ran with nothing proven and fell
|
|
7929
|
+
// back to bounding itself by the run's declared stretch alone.
|
|
7930
|
+
const provenName = typeof run.provenName === "string" ? run.provenName : null;
|
|
8060
7931
|
void waitForChildExit(ffmpeg, ENCODE_RUN_TERMINATE_GRACE_MS).then(() =>
|
|
8061
|
-
this.#discardUnfinishedPiece(session, session.dirPath, stoppedSpan)
|
|
7932
|
+
this.#discardUnfinishedPiece(session, session.dirPath, stoppedSpan, provenName)
|
|
8062
7933
|
);
|
|
8063
7934
|
}
|
|
8064
7935
|
logger.info(`transcode ${session.id} ${running.length} encoder(s) stopped: ${reason}`);
|
|
@@ -9952,7 +9823,7 @@ export class HlsSessionManager {
|
|
|
9952
9823
|
* @param {string | null | undefined} runDirPath
|
|
9953
9824
|
* @returns {Promise<void>}
|
|
9954
9825
|
*/
|
|
9955
|
-
async #discardUnfinishedPiece(session, runDirPath, within = null) {
|
|
9826
|
+
async #discardUnfinishedPiece(session, runDirPath, within = null, provenName = null) {
|
|
9956
9827
|
const canJudgeTracks =
|
|
9957
9828
|
typeof session.segmentFormat?.hasEveryTrack === "function" &&
|
|
9958
9829
|
session.initBytes &&
|
|
@@ -9968,7 +9839,8 @@ export class HlsSessionManager {
|
|
|
9968
9839
|
: raw;
|
|
9969
9840
|
return session.segmentFormat.hasEveryTrack(bytes, session.initBytes);
|
|
9970
9841
|
}
|
|
9971
|
-
: null
|
|
9842
|
+
: null,
|
|
9843
|
+
provenName
|
|
9972
9844
|
);
|
|
9973
9845
|
if (removed !== null) {
|
|
9974
9846
|
// Removed on purpose, so the index must not go on answering with it.
|
|
@@ -10003,21 +9875,31 @@ export class HlsSessionManager {
|
|
|
10003
9875
|
// failed did the player move to the segment it actually needed — 63 s of
|
|
10004
9876
|
// spinner after a track that had been made ready in 7.
|
|
10005
9877
|
//
|
|
10006
|
-
//
|
|
10007
|
-
//
|
|
10008
|
-
//
|
|
10009
|
-
//
|
|
9878
|
+
// WHETHER ANYBODY IS COMING FOR IT, asked of the priority map rather than
|
|
9879
|
+
// measured in segments. The distance used to decide, against the reach of
|
|
9880
|
+
// a repair that moved the encoder from a request — both are gone. A
|
|
9881
|
+
// number inside somebody's zone will be made, so holding it is holding it
|
|
9882
|
+
// for a viewer; a number in nobody's zone will not, and holding it spends
|
|
9883
|
+
// the player's patience for nothing.
|
|
9884
|
+
// AN EMPTY MAP IS NOT AN ANSWER. It says the map has not been built yet —
|
|
9885
|
+
// a session created a moment ago, before the first pass — and that is a
|
|
9886
|
+
// different thing from "nobody is coming", which is what this refusal
|
|
9887
|
+
// needs. Told apart, because conflating them answers every behind-head
|
|
9888
|
+
// request as absent for as long as a fresh session has no map.
|
|
9889
|
+
const zones = this.encodeOrchestrator.demand.mapOn(session.outputKey ?? "");
|
|
9890
|
+
const nobodyIsComing = zones.length > 0 &&
|
|
9891
|
+
!zones.some((zone) => requestedIndex >= zone.from && requestedIndex <= zone.to);
|
|
10010
9892
|
if (
|
|
10011
9893
|
Number.isFinite(requestedIndex) &&
|
|
10012
9894
|
requestedIndex < (earliestRunStart(session) ?? 0) &&
|
|
10013
|
-
|
|
9895
|
+
nobodyIsComing &&
|
|
10014
9896
|
liveRunsOf(session).length > 0 &&
|
|
10015
9897
|
session.seekTarget == null &&
|
|
10016
9898
|
session.seekSettleTimer == null
|
|
10017
9899
|
) {
|
|
10018
9900
|
logger.info(
|
|
10019
9901
|
`transcode ${session.id} segment #${requestedIndex} is ${(earliestRunStart(session) ?? 0) - requestedIndex} ` +
|
|
10020
|
-
|
|
9902
|
+
"segments behind the run and in nobody's zone; answered as absent rather than held"
|
|
10021
9903
|
);
|
|
10022
9904
|
return { kind: "not-found" };
|
|
10023
9905
|
}
|
package/services/hwaccel.js
CHANGED
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
|
-
import { mkdtempSync, readdirSync, rmSync, statSync
|
|
24
|
+
import { mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
25
25
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
26
26
|
import os from "node:os";
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { fitDecodeCost } from "./decode-cost-fit.js";
|
|
29
|
-
import { penaltiesFrom } from "./contention.js";
|
|
29
|
+
import { penaltiesFrom } from "./encode/contention.js";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import {
|
|
32
32
|
parseFfmpegBitrateKbps,
|