@torrent-tv/proxy 2.91.0 → 2.92.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/bin/cli.js +3 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* and nothing about a viewer enters what is decided here.
|
|
11
11
|
*
|
|
12
12
|
* What the file says about itself is read by the media component and handed in
|
|
13
|
-
* (`
|
|
13
|
+
* (`readSourceMedia`), as is everything else this reads.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { createRequire } from "node:module";
|
|
@@ -44,7 +44,7 @@ export class OutputOpening {
|
|
|
44
44
|
#host;
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* @param {object} host - `logger`, `
|
|
47
|
+
* @param {object} host - `logger`, `readSourceMedia`, `enabled`, `segmentFormat`, `renditions`, `sourceFiles`, `getCachedMediaInfo`, `localBaseUrl`, `tonemapSupported`, `videoEncoder`, `hostLoad`, `keyframeTables`, `timelines`, `segmentDurationSec`, `softwarePresetBenchmark`, `decodeCostModel`, `encodeCost`, `admission`, `outputs`, `segmentOutputFiles`, `hostTimings`, `returns`, `encodeRuns`
|
|
48
48
|
* @param {SegmentOutputFiles} host.segmentOutputFiles - The storage
|
|
49
49
|
* operations needed while selecting or opening an output.
|
|
50
50
|
*/
|
|
@@ -292,7 +292,6 @@ export class OutputOpening {
|
|
|
292
292
|
// statement as "the file it reads is not the file of the picture", so it is
|
|
293
293
|
// that comparison now and there is nothing to keep in step.
|
|
294
294
|
const audioFile = this.#host.sourceFiles.get(sourceKey, audioSource.fileIndex, audioSource.name);
|
|
295
|
-
const inputFile = audioOnly === true && audioSource.isSidecar ? audioFile : file;
|
|
296
295
|
// Media info (duration/resolution/fps/startTime/HDR) up front, so we can
|
|
297
296
|
// serve a complete VOD playlist (#EXT-X-ENDLIST) with the correct total
|
|
298
297
|
// duration and a fully seekable timeline before a single segment exists.
|
|
@@ -317,10 +316,16 @@ export class OutputOpening {
|
|
|
317
316
|
// No session id on it: this read is a probe of the picture, not this
|
|
318
317
|
// session's own delivery, and counting it against the session would tell a
|
|
319
318
|
// waiting browser that its film is arriving when what arrived was a header.
|
|
320
|
-
const pictureUrl = file.streamUrl(this.#host.localBaseUrl);
|
|
321
319
|
const mediaInfo = cachedUsable
|
|
322
320
|
? cachedMediaInfo
|
|
323
|
-
:
|
|
321
|
+
: this.#host.readSourceMedia
|
|
322
|
+
? await this.#host.readSourceMedia({ sourceKey, fileIndex })
|
|
323
|
+
: (() => {
|
|
324
|
+
const error = new Error("Source media declarations are not configured.");
|
|
325
|
+
error.code = "SOURCE_MEDIA_READER_UNAVAILABLE";
|
|
326
|
+
error.canRetry = false;
|
|
327
|
+
throw error;
|
|
328
|
+
})();
|
|
324
329
|
const mediaInfoMs = Date.now() - mediaInfoStartMs;
|
|
325
330
|
const mediaInfoSource = cachedUsable ? "cached" : "probed";
|
|
326
331
|
// The file takes in what the probe said. It is the same answer for every
|
|
@@ -329,6 +334,7 @@ export class OutputOpening {
|
|
|
329
334
|
// fresher reading updates it: on a cold torrent the first probe can come
|
|
330
335
|
// back without a duration, and the second is the one that has it.
|
|
331
336
|
file.learn(mediaInfo);
|
|
337
|
+
await this.#host.hostLoad.readFileLength(sourceKey, fileIndex);
|
|
332
338
|
const durationSeconds = file.durationSeconds ?? 0;
|
|
333
339
|
const sourceWidth = file.width;
|
|
334
340
|
const sourceHeight = file.height;
|
|
@@ -383,11 +389,6 @@ export class OutputOpening {
|
|
|
383
389
|
// duration, so the picture's byte rate would buy a window twenty times
|
|
384
390
|
// wider than the seconds it is meant to represent, and the piece store
|
|
385
391
|
// would hold it.
|
|
386
|
-
const readWindowBytes = await this.#host.hostLoad.readWindowBytesFor(
|
|
387
|
-
inputFile.sourceKey,
|
|
388
|
-
inputFile.fileIndex,
|
|
389
|
-
durationSeconds
|
|
390
|
-
);
|
|
391
392
|
if (!hasDuration) {
|
|
392
393
|
this.#host.logger.warn(
|
|
393
394
|
`transcode "${logName}": could not probe duration; falling back to ` +
|
|
@@ -428,13 +429,6 @@ export class OutputOpening {
|
|
|
428
429
|
// segment boundaries (the playlist itself), so this MUST block session
|
|
429
430
|
// creation — an incorrect playlist is worse than a slower start.
|
|
430
431
|
//
|
|
431
|
-
// The wait is bounded, and the bound is what the read costs on a real
|
|
432
|
-
// host rather than a figure picked here (`KEYFRAME_TABLE_BUDGET_MS`). A
|
|
433
|
-
// comment in this place used to promise a short timeout and "never more
|
|
434
|
-
// than ~6 s to session start" when no timeout existed at all; the file
|
|
435
|
-
// comes off a torrent, so the bytes the table lives in may still be
|
|
436
|
-
// arriving, and a session used to wait for them without limit.
|
|
437
|
-
//
|
|
438
432
|
// What is read is the container's OWN table (Cues/stss) rather than a
|
|
439
433
|
// scan of the media. On the copy path ffmpeg can only cut at the source's
|
|
440
434
|
// existing keyframes, so these times ARE the segment boundaries —
|
|
@@ -449,18 +443,9 @@ export class OutputOpening {
|
|
|
449
443
|
const { arrived } = await this.#host.keyframeTables.within({ sourceKey, fileIndex, logName });
|
|
450
444
|
keyframeMs = Date.now() - keyframeStartMs;
|
|
451
445
|
if (!arrived) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
// and the distinction is the table's own (`answered` against
|
|
456
|
-
// `readable`). Recorded as an absence it would make a passing shortage
|
|
457
|
-
// of bytes look like a property of the bytes, and every later session
|
|
458
|
-
// of the file would re-encode a picture that can be copied.
|
|
459
|
-
this.#host.logger.warn(
|
|
460
|
-
`transcode: the keyframe table for "${logName}" has not arrived after ${keyframeMs}ms ` +
|
|
461
|
-
`(budget ${Math.round(this.#host.keyframeTables.budgetMs / 1000)}s), so this session re-encodes the picture ` +
|
|
462
|
-
"instead of copying it; the read goes on and the next session of this file gets the copy"
|
|
463
|
-
);
|
|
446
|
+
const error = new Error("Source keyframe declarations require more available bytes.");
|
|
447
|
+
error.code = "MEDIA_BYTES_UNAVAILABLE";
|
|
448
|
+
throw error;
|
|
464
449
|
}
|
|
465
450
|
if (!keyframes.readable) {
|
|
466
451
|
// No index, so there is no honest grid for a COPY: a copied picture can
|
|
@@ -474,9 +459,7 @@ export class OutputOpening {
|
|
|
474
459
|
// then PLACED at our own cut times rather than found, so the grid is
|
|
475
460
|
// correct by construction whatever the container. MPEG-TS is the case
|
|
476
461
|
// this exists for — measured 2026-08-21, 669 real keyframes and no
|
|
477
|
-
// index of any kind to read them from
|
|
478
|
-
// could not be read in the budget lands here too, which is right for
|
|
479
|
-
// the same reason.
|
|
462
|
+
// index of any kind to read them from.
|
|
480
463
|
transcodeVideo = true;
|
|
481
464
|
if (keyframes.answered) {
|
|
482
465
|
this.#host.logger.warn(
|
|
@@ -486,36 +469,6 @@ export class OutputOpening {
|
|
|
486
469
|
);
|
|
487
470
|
}
|
|
488
471
|
}
|
|
489
|
-
} else if (hasDuration && transcodeVideo) {
|
|
490
|
-
// Re-encode path: keyframeTimes are ONLY used to snap a LATER seek (see
|
|
491
|
-
// #startEncodeRun) — segment boundaries stay on the uniform grid either
|
|
492
|
-
// way. So this does NOT need to block session creation / the first
|
|
493
|
-
// segment's start. Run it in the background with a FULL budget instead of
|
|
494
|
-
// the 6 s cap: AVI-class containers need a full packet scan, which 6 s can
|
|
495
|
-
// never afford without delaying playback start — that starved budget is
|
|
496
|
-
// exactly why the probe kept missing on the container where the seek bug
|
|
497
|
-
// was field-diagnosed. A run reads the file's table on every call, so a
|
|
498
|
-
// seek that happens AFTER this finishes picks it up automatically; one
|
|
499
|
-
// that happens before falls back to the existing circuit breaker as a
|
|
500
|
-
// safety net (no regression either way).
|
|
501
|
-
keyframeMs = -2;
|
|
502
|
-
const backgroundStartedAt = Date.now();
|
|
503
|
-
void this.#host.probeKeyframeTimes(inputFile.streamUrl(this.#host.localBaseUrl).toString(), 25_000).then((times) => {
|
|
504
|
-
// Into the FILE's table, which the picture, its quality steps and a
|
|
505
|
-
// second viewer's session all hold — so nothing has to be alive for the
|
|
506
|
-
// answer to be kept, and the session this probe was started for may
|
|
507
|
-
// long since have gone. It used to be written onto whichever session
|
|
508
|
-
// was still there, and dropped outright when none was.
|
|
509
|
-
this.#host.keyframeTables.learn({ sourceKey, fileIndex }, { times, format: "packet probe" });
|
|
510
|
-
const elapsedMs = Date.now() - backgroundStartedAt;
|
|
511
|
-
this.#host.logger.info(
|
|
512
|
-
times
|
|
513
|
-
? `transcode: background keyframe probe found ${times.length} keyframes ` +
|
|
514
|
-
`(${elapsedMs}ms) for "${logName}" — later seeks will snap to them`
|
|
515
|
-
: `transcode: background keyframe probe unavailable (${elapsedMs}ms) for "${logName}" ` +
|
|
516
|
-
`— seeks keep using the raw target (falls back to the circuit breaker on failure)`
|
|
517
|
-
);
|
|
518
|
-
});
|
|
519
472
|
}
|
|
520
473
|
this.#host.logger.info(
|
|
521
474
|
`cold-start "${logName}": media-info=${mediaInfoMs}ms (${mediaInfoSource}) ` +
|
|
@@ -584,7 +537,7 @@ export class OutputOpening {
|
|
|
584
537
|
encodesPicture: transcodeVideo && carriesVideo,
|
|
585
538
|
exact: forceExactSize,
|
|
586
539
|
target: this.#targetFor({ normalizedTargetWidth, normalizedTargetHeight, visiblePicture, sourceWidth, sourceHeight }),
|
|
587
|
-
source: { width: sourceWidth, height: sourceHeight, megabitsPerSecond: file.
|
|
540
|
+
source: { width: sourceWidth, height: sourceHeight, megabitsPerSecond: file.megabitsPerSecond, decode: file.decode },
|
|
588
541
|
fps: outputFps,
|
|
589
542
|
encoder: this.#host.videoEncoder,
|
|
590
543
|
benchmark: this.#host.softwarePresetBenchmark,
|
|
@@ -722,7 +675,6 @@ export class OutputOpening {
|
|
|
722
675
|
this.#host.segmentOutputFiles.directoryFor(spec.toKey());
|
|
723
676
|
this.#host.segmentOutputFiles.useFormat(spec.toKey(), segmentFormat);
|
|
724
677
|
this.#host.hostTimings.noteOutputCreated(session, createEntryMs);
|
|
725
|
-
this.#host.encodeRuns.setReadWindow(session, readWindowBytes);
|
|
726
678
|
this.#host.encodeCost.notePredictionFor(session, output.encodeHeight);
|
|
727
679
|
// Decided before the key was built and only recorded here. Whether the audio
|
|
728
680
|
// travels separately decides the ffmpeg arguments, what the master says,
|
|
@@ -14,7 +14,6 @@ import { variantHeightsFor } from "./output/ladder.js";
|
|
|
14
14
|
import { isSameMaterial, outputSuits } from "./quality/serving-output.js";
|
|
15
15
|
import { SOUNDTRACK_MODE_CAUSE, chooseSoundtrackMode, linkAnswerFigures, linkCouldCarry, loadOf, soundtrackLoadOf, videoLoadOfLimit, videoLoadOfSpec } from "./quality/link-budget.js";
|
|
16
16
|
import { maxrateKbpsFor } from "./args.js";
|
|
17
|
-
import { encoderInputs } from "./run-inputs.js";
|
|
18
17
|
/**
|
|
19
18
|
* How a base files the audio renditions it has made.
|
|
20
19
|
*
|
|
@@ -132,7 +131,7 @@ export class Renditions {
|
|
|
132
131
|
wanted,
|
|
133
132
|
judge: (spec) => linkCouldCarry(
|
|
134
133
|
linkMbps,
|
|
135
|
-
loadOf(videoLoadOfSpec(spec, base.file.
|
|
134
|
+
loadOf(videoLoadOfSpec(spec, base.file.megabitsPerSecond, this.#host.observedPeakMbps?.(spec) ?? null), audioLoad)
|
|
136
135
|
)
|
|
137
136
|
});
|
|
138
137
|
}
|
|
@@ -1661,21 +1660,6 @@ export class Renditions {
|
|
|
1661
1660
|
) ?? null;
|
|
1662
1661
|
}
|
|
1663
1662
|
|
|
1664
|
-
inputOf(session, runToken) {
|
|
1665
|
-
return encoderInputs({
|
|
1666
|
-
picture: session.file,
|
|
1667
|
-
soundtrack: session.spec.audio
|
|
1668
|
-
? this.#host.sourceFiles.get(session.file.sourceKey, session.spec.audioFileIndex)
|
|
1669
|
-
: session.file,
|
|
1670
|
-
carries: session.spec.carries,
|
|
1671
|
-
audioSeparate: this.servesAudioSeparately(session),
|
|
1672
|
-
sessionId: session.id,
|
|
1673
|
-
runToken,
|
|
1674
|
-
readWindowBytes: this.#host.encodeRuns.readWindowFor(session),
|
|
1675
|
-
baseUrl: this.#host.localBaseUrl
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
1663
|
/**
|
|
1680
1664
|
* One file of an audio rendition: its playlist, its init segment or one of
|
|
1681
1665
|
* its segments.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AUDIO_TRANSCODE_KBPS } from "./args.js";
|
|
2
|
+
import { ffmpegSeconds, publishedStartTime, publishedGridFor } from "./run-command.js";
|
|
3
|
+
import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "./output/index.js";
|
|
4
|
+
|
|
5
|
+
/** One held Matroska stdin, with no container seek or second input. */
|
|
6
|
+
export function buildAdmittedCommand({ admittedInput, timeline, output, segmentFormat, transcodeVideo,
|
|
7
|
+
transcodeAudio, audioOnly, audioSeparate, rateControl, startIndex, endIndex, videoEncoder, segmentDurationSec }) {
|
|
8
|
+
const safeIndex = Math.max(0, Number.isInteger(startIndex) ? startIndex : 0);
|
|
9
|
+
const startSeconds = publishedStartTime(timeline, safeIndex, segmentDurationSec);
|
|
10
|
+
const grid = publishedGridFor(timeline);
|
|
11
|
+
const endSeconds = grid?.[endIndex + 1];
|
|
12
|
+
if (!(endSeconds > startSeconds)) throw new Error("An admitted run requires a complete finite output interval.");
|
|
13
|
+
const origin = admittedInput.originSeconds;
|
|
14
|
+
const relativeStart = Math.max(0, startSeconds - origin);
|
|
15
|
+
const relativeEnd = endSeconds - origin;
|
|
16
|
+
const cutTimes = grid.slice(safeIndex + 1, endIndex + 1).map(time => time - startSeconds);
|
|
17
|
+
const args = ["-hide_banner", "-nostats", "-loglevel", "error", "-progress", "pipe:1"];
|
|
18
|
+
if (transcodeVideo && Array.isArray(videoEncoder.inputArgs)) args.push(...videoEncoder.inputArgs);
|
|
19
|
+
args.push("-f", "matroska", "-i", "pipe:0", "-copyts", "-avoid_negative_ts", "disabled", "-output_ts_offset", ffmpegSeconds(origin));
|
|
20
|
+
const video = transcodeVideo ? videoEncoder.buildVideoArgs({
|
|
21
|
+
targetWidth: output.encodeWidth, targetHeight: output.encodeHeight, segmentDurationSec,
|
|
22
|
+
fps: output.outputFps, preset: output.softwarePreset ?? undefined, tonemap: output.applyTonemap === true,
|
|
23
|
+
forcedKeyframeTimes: [relativeStart, ...cutTimes.map(time => relativeStart + time)], rateControl: rateControl ?? null
|
|
24
|
+
}) : ["-c:v", "copy"];
|
|
25
|
+
if (transcodeVideo) {
|
|
26
|
+
video.push("-fps_mode:v", "passthrough", "-enc_time_base:v", "1:1000000");
|
|
27
|
+
const existing = video.indexOf("-vf");
|
|
28
|
+
const trim = `trim=start=${ffmpegSeconds(relativeStart)}:end=${ffmpegSeconds(relativeEnd)}`;
|
|
29
|
+
if (existing >= 0) {
|
|
30
|
+
// Anchor the frame cadence at the requested interval, rather than the
|
|
31
|
+
// earlier audio preroll that determined the shared stdin origin.
|
|
32
|
+
const filters = video[existing + 1].replace(/fps=(\d+)(?=,|$)/g,
|
|
33
|
+
(_match, fps) => `setpts=PTS+${ffmpegSeconds(origin)}/TB,fps=${fps}:start_time=${ffmpegSeconds(startSeconds)},settb=1/1000000,setpts=PTS-${ffmpegSeconds(origin)}/TB`);
|
|
34
|
+
video[existing + 1] = `${trim},${filters},${trim}`;
|
|
35
|
+
}
|
|
36
|
+
else video.push("-vf", trim);
|
|
37
|
+
}
|
|
38
|
+
const audio = transcodeAudio ? ["-c:a", "aac", "-ac", "2", "-b:a", `${AUDIO_TRANSCODE_KBPS}k`,
|
|
39
|
+
"-af", `atrim=start=${ffmpegSeconds(relativeStart)}:end=${ffmpegSeconds(relativeEnd)}`] : ["-c:a", "copy"];
|
|
40
|
+
if (audioOnly) args.push("-vn", "-map", "0:a:0", ...audio);
|
|
41
|
+
else if (audioSeparate) args.push("-an", "-map", "0:v:0", ...video);
|
|
42
|
+
else args.push("-map", "0:v:0", "-map", "0:a:0?", ...video, ...audio);
|
|
43
|
+
args.push("-to", ffmpegSeconds(relativeEnd), "-f", "segment");
|
|
44
|
+
if (cutTimes.length) args.push("-segment_times", cutTimes.join(","));
|
|
45
|
+
else args.push("-segment_time", ffmpegSeconds(endSeconds - startSeconds));
|
|
46
|
+
const formatArgs = segmentFormat.explicitTimesMuxerArgs?.();
|
|
47
|
+
if (!Array.isArray(formatArgs)) throw new Error("The segment format cannot publish admitted packet input.");
|
|
48
|
+
const muxOptions = formatArgs.indexOf("-segment_format_options");
|
|
49
|
+
const muxFormat = formatArgs.indexOf("-segment_format");
|
|
50
|
+
if (muxFormat >= 0 && formatArgs[muxFormat + 1] === "mp4" && muxOptions >= 0) {
|
|
51
|
+
// Millisecond movie edits cannot retain a sample-accurate AAC origin.
|
|
52
|
+
formatArgs[muxOptions + 1] += ":movie_timescale=1000000";
|
|
53
|
+
}
|
|
54
|
+
// Audio has no keyframe rounding: the video tolerance can remove several
|
|
55
|
+
// audio frames from the declared interval before the requested cut.
|
|
56
|
+
args.push("-segment_time_delta", String(audioOnly ? 0 : SEGMENT_CUT_TIME_DELTA_SECONDS), "-segment_start_number", String(safeIndex),
|
|
57
|
+
"-segment_list", "pipe:3", "-segment_list_flags", "+live", ...formatArgs,
|
|
58
|
+
segmentFormat.makingFileNameTemplate(admittedInput.runTag ?? `${safeIndex}-${admittedInput.fingerprint.slice(0, 12)}`));
|
|
59
|
+
return { args, safeIndex, startSeconds, cutTimes };
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Convert borrowed Annex B payload slices to Matroska AVC length framing. */
|
|
2
|
+
export function avcPacketSlices(slices) {
|
|
3
|
+
if (!Array.isArray(slices) || slices.some(slice => !Buffer.isBuffer(slice))) throw new TypeError("AVC packet slices must be buffers.");
|
|
4
|
+
const units = [];
|
|
5
|
+
let position = 0, zeroes = 0, start = null;
|
|
6
|
+
for (const slice of slices) {
|
|
7
|
+
for (const byte of slice) {
|
|
8
|
+
if (byte === 1 && zeroes >= 2) {
|
|
9
|
+
const end = position - zeroes;
|
|
10
|
+
if (start !== null) {
|
|
11
|
+
if (end <= start) throw new Error("AVC Annex B contains an empty NAL unit.");
|
|
12
|
+
units.push([start, end]);
|
|
13
|
+
} else if (end !== 0) throw new Error("AVC Annex B precedes its first start code with data.");
|
|
14
|
+
start = position + 1;
|
|
15
|
+
}
|
|
16
|
+
zeroes = byte === 0 ? zeroes + 1 : 0;
|
|
17
|
+
position++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (start === null || position - zeroes <= start) throw new Error("AVC Annex B has no complete NAL unit.");
|
|
21
|
+
units.push([start, position - zeroes]);
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const [from, to] of units) {
|
|
24
|
+
const length = to - from;
|
|
25
|
+
if (length > 0xffffffff) throw new Error("AVC NAL unit exceeds its length declaration.");
|
|
26
|
+
const header = Buffer.alloc(4);
|
|
27
|
+
header.writeUInt32BE(length);
|
|
28
|
+
result.push(header);
|
|
29
|
+
let offset = 0;
|
|
30
|
+
for (const slice of slices) {
|
|
31
|
+
if (offset < to && offset + slice.length > from) result.push(slice.subarray(Math.max(0, from - offset), Math.min(slice.length, to - offset)));
|
|
32
|
+
offset += slice.length;
|
|
33
|
+
if (offset >= to) break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Normalize an admitted byte-sized payload that starts between source bytes. */
|
|
2
|
+
export function bitPayloadSlices(slices, bitOffset, bitLength) {
|
|
3
|
+
const total = slices.reduce((sum, slice) => sum + slice.length, 0);
|
|
4
|
+
if (!Number.isSafeInteger(bitOffset) || bitOffset < 0 || !Number.isSafeInteger(bitLength) ||
|
|
5
|
+
bitLength <= 0 || bitLength % 8 !== 0 || bitOffset + bitLength > total * 8) throw new Error("Admitted packet bit payload is incomplete.");
|
|
6
|
+
const first = Math.floor(bitOffset / 8), length = bitLength / 8, shift = bitOffset & 7;
|
|
7
|
+
if (!shift) {
|
|
8
|
+
let offset = 0;
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const slice of slices) {
|
|
11
|
+
if (offset < first + length && offset + slice.length > first) result.push(slice.subarray(Math.max(0, first - offset), Math.min(slice.length, first + length - offset)));
|
|
12
|
+
offset += slice.length;
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
const result = Buffer.alloc(length);
|
|
17
|
+
let sliceIndex = 0, base = 0;
|
|
18
|
+
const byteAt = position => {
|
|
19
|
+
while (base + slices[sliceIndex].length <= position) { base += slices[sliceIndex].length; sliceIndex++; }
|
|
20
|
+
return slices[sliceIndex][position - base];
|
|
21
|
+
};
|
|
22
|
+
let before = byteAt(first);
|
|
23
|
+
for (let offset = 0; offset < length; offset++) {
|
|
24
|
+
const after = byteAt(first + offset + 1);
|
|
25
|
+
result[offset] = ((before << shift) | (after >> (8 - shift))) & 255;
|
|
26
|
+
before = after;
|
|
27
|
+
}
|
|
28
|
+
return [result];
|
|
29
|
+
}
|
|
@@ -401,7 +401,7 @@ export class OutputCatalog {
|
|
|
401
401
|
// The probe's own reading of the video stream, which is known from the
|
|
402
402
|
// moment the session exists and covers the gap before the torrent has
|
|
403
403
|
// reported a length.
|
|
404
|
-
streamBitsPerSecond: (Number(session.file?.
|
|
404
|
+
streamBitsPerSecond: (Number(session.file?.megabitsPerSecond) || 0) * 1_000_000,
|
|
405
405
|
largest: this.largestPieceOf(session.outputKey ?? ""),
|
|
406
406
|
boundaries: session.timeline?.published ?? session.timeline?.boundaries ?? null,
|
|
407
407
|
producedHeight: this.producedHeightOf(session),
|
|
@@ -118,6 +118,17 @@ export class Timeline {
|
|
|
118
118
|
return this.sourceTimes[Math.min(index, this.sourceTimes.length - 1)] ?? null;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** Address source packets with exact cuts, translated only for another file. */
|
|
122
|
+
sourceInterval(fromIndex, toIndex, sourceStartTime = 0) {
|
|
123
|
+
const base = this.sourceStartOf(0);
|
|
124
|
+
const from = this.sourceStartOf(fromIndex), to = this.sourceStartOf(toIndex + 1);
|
|
125
|
+
const shift = sourceStartTime - (base ?? 0);
|
|
126
|
+
return {
|
|
127
|
+
from: from === null ? this.publishedStartOf(fromIndex) + sourceStartTime : from + shift,
|
|
128
|
+
to: to === null ? this.publishedStartOf(toIndex + 1) + sourceStartTime : to + shift
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
/**
|
|
122
133
|
* Where segment `index` begins on the live table — where a run cutting now
|
|
123
134
|
* will really put it.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** PCM variants that Matroska requires in a different byte representation. */
|
|
2
|
+
export function pcmNormalization(track) {
|
|
3
|
+
if (track.matroskaCodecId) return null;
|
|
4
|
+
const format = /^pcm_([suf])(8|16|24|32|64)(le|be)?$/.exec(track.codecId ?? "");
|
|
5
|
+
if (!format) return null;
|
|
6
|
+
const [, kind, bits, endian] = format;
|
|
7
|
+
const width = Number(bits) / 8;
|
|
8
|
+
const swap = kind === "f" && endian === "be";
|
|
9
|
+
const flipSign = (kind === "s" && width === 1) || (kind === "u" && width > 1);
|
|
10
|
+
return swap || flipSign ? { width, swap, flipSign, little: endian === "le" } : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Convert owned output bytes without modifying any shared source slice. */
|
|
14
|
+
export function normalizePcmSlices(slices, format) {
|
|
15
|
+
const length = slices.reduce((sum, slice) => sum + slice.length, 0);
|
|
16
|
+
if (length % format.width) throw new Error("PCM packet ends inside a sample.");
|
|
17
|
+
const output = Buffer.alloc(length);
|
|
18
|
+
let source = 0;
|
|
19
|
+
for (const slice of slices) {
|
|
20
|
+
for (const byte of slice) {
|
|
21
|
+
const sample = Math.floor(source / format.width) * format.width;
|
|
22
|
+
const within = source % format.width;
|
|
23
|
+
const target = sample + (format.swap ? format.width - 1 - within : within);
|
|
24
|
+
const signByte = format.little ? format.width - 1 : 0;
|
|
25
|
+
output[target] = format.flipSign && within === signByte ? byte ^ 0x80 : byte;
|
|
26
|
+
source++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return output;
|
|
30
|
+
}
|
|
@@ -40,11 +40,15 @@ export function cutOf(timeline, index) {
|
|
|
40
40
|
* @param {number | undefined} cutSeconds - From {@link cutOf}.
|
|
41
41
|
* @returns {{ whole: boolean, throughSeconds: number | null }}
|
|
42
42
|
*/
|
|
43
|
-
export function judgePiece(format, ranges, cutSeconds) {
|
|
43
|
+
export function judgePiece(format, ranges, cutSeconds, interval) {
|
|
44
44
|
const throughSeconds = format.producedThroughSeconds?.(ranges) ?? null;
|
|
45
45
|
if (throughSeconds === null) {
|
|
46
46
|
return { whole: false, throughSeconds };
|
|
47
47
|
}
|
|
48
|
+
if (interval) {
|
|
49
|
+
const reason = intervalFailure(ranges, interval);
|
|
50
|
+
return reason ? { whole: false, throughSeconds, reason } : { whole: true, throughSeconds };
|
|
51
|
+
}
|
|
48
52
|
const cutMicros = ranges?.production?.cutMicros ??
|
|
49
53
|
(Number.isFinite(cutSeconds) ? BigInt(Math.round(cutSeconds * 1_000_000)) : null);
|
|
50
54
|
if (cutMicros !== null && ranges?.tracks?.length) {
|
|
@@ -72,6 +76,7 @@ export function judgePiece(format, ranges, cutSeconds) {
|
|
|
72
76
|
return { whole, throughSeconds };
|
|
73
77
|
}
|
|
74
78
|
if (cutMicros !== null && throughSeconds < Number(cutMicros) / 1_000_000) {
|
|
79
|
+
|
|
75
80
|
return { whole: false, throughSeconds };
|
|
76
81
|
}
|
|
77
82
|
return { whole: true, throughSeconds };
|
|
@@ -102,3 +107,51 @@ export function originOf(ranges, endMicros, kind) {
|
|
|
102
107
|
return ticks >= 0n ? (2n * ticks + track.timescale) / (2n * track.timescale) :
|
|
103
108
|
-((-2n * ticks + track.timescale) / (2n * track.timescale));
|
|
104
109
|
}
|
|
110
|
+
|
|
111
|
+
/** Compare adjacent production ranges in their declared track clocks. */
|
|
112
|
+
export function judgeNeighbors(left, right) {
|
|
113
|
+
if (!left?.tracks?.length || !right?.tracks?.length) return { whole: false, reason: "neighbor-tracks-are-missing" };
|
|
114
|
+
for (const track of right.tracks) {
|
|
115
|
+
if (!left.tracks.some(previous => previous.kind === track.kind)) return { whole: false, reason: `neighbor-missing-${track.kind}` };
|
|
116
|
+
}
|
|
117
|
+
for (const track of left?.tracks ?? []) {
|
|
118
|
+
const next = right?.tracks?.find(candidate => candidate.kind === track.kind);
|
|
119
|
+
if (!next?.ranges?.length || !track.ranges?.length) return { whole: false, reason: `neighbor-missing-${track.kind}` };
|
|
120
|
+
if (!(track.timescale > 0n) || !(next.timescale > 0n)) return { whole: false, reason: "neighbor-clock-is-invalid" };
|
|
121
|
+
const end = track.ranges.at(-1), start = next.ranges[0];
|
|
122
|
+
const difference = start.start * track.timescale - end.end * next.timescale;
|
|
123
|
+
const leftFrame = (track.productionFrame ?? end.frame) * next.timescale;
|
|
124
|
+
const rightFrame = (next.productionFrame ?? start.frame) * track.timescale;
|
|
125
|
+
if (!(leftFrame > 0n) || !(rightFrame > 0n)) return { whole: false, reason: "neighbor-frame-is-invalid" };
|
|
126
|
+
const allowance = leftFrame > rightFrame ? leftFrame : rightFrame;
|
|
127
|
+
if (difference > allowance || difference < -allowance) return { whole: false, reason: `neighbor-discontinuity-${track.kind}` };
|
|
128
|
+
}
|
|
129
|
+
return { whole: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Every required track must cover the interval with at most one-frame error. */
|
|
133
|
+
function intervalFailure(coverage, { from, to, requiredKinds, sourceEnds = {} }) {
|
|
134
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from) || !requiredKinds?.length) return "segment-interval-is-not-declared";
|
|
135
|
+
for (const kind of requiredKinds) {
|
|
136
|
+
const track = coverage?.tracks?.find(track => track.kind === kind);
|
|
137
|
+
if (!track?.ranges?.length || !(track.timescale > 0n)) return `segment-missing-${kind}`;
|
|
138
|
+
const start = BigInt(Math.round(from * Number(track.timescale)));
|
|
139
|
+
const through = Number.isFinite(sourceEnds[kind]) ? Math.min(to, sourceEnds[kind]) : to;
|
|
140
|
+
if (!(through > from)) return `segment-interval-is-empty-${kind}`;
|
|
141
|
+
const end = BigInt(Math.round(through * Number(track.timescale)));
|
|
142
|
+
const first = track.ranges[0];
|
|
143
|
+
const frame = track.productionFrame ?? first.frame;
|
|
144
|
+
if (first.start > start + frame || first.start < start - frame) return `segment-start-outside-interval-${kind}`;
|
|
145
|
+
// The first sample's cadence already bounds the interval start. A shorter
|
|
146
|
+
// final sample must not replace that bound when this range is traversed.
|
|
147
|
+
let reached = first.start;
|
|
148
|
+
for (const range of track.ranges) {
|
|
149
|
+
if (!(range.frame > 0n) || range.end <= range.start || range.start > reached + range.frame) return `segment-gap-within-interval-${kind}`;
|
|
150
|
+
if (range.end > reached) reached = range.end;
|
|
151
|
+
}
|
|
152
|
+
const finalFrame = track.productionFrame ?? track.ranges.at(-1).frame;
|
|
153
|
+
if (reached < end - finalFrame || reached > end + finalFrame) return `segment-end-outside-interval-${kind}`;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
|
|
157
|
+
}
|
|
@@ -15,19 +15,6 @@ import { medianOf, movedBeyondScatter, READINGS_KEPT, scatterOf } from "../learn
|
|
|
15
15
|
import { ENCODE_RUN_STATE, processCanBeSignalled } from "../encode-run-state.js";
|
|
16
16
|
import { SourceFiles } from "../../media/SourceFile.js";
|
|
17
17
|
|
|
18
|
-
// How far ahead of its own read head a reader asks the swarm for, expressed in
|
|
19
|
-
// seconds of PLAYBACK. The torrent thread can only think in bytes, and a fixed
|
|
20
|
-
// byte window is wrong at both ends of the range: 32 MB is half a minute of a
|
|
21
|
-
// 1080p film and about four seconds of a disc remux. Duration and file size are
|
|
22
|
-
// both known here, so the window is sized where the knowledge is and sent down
|
|
23
|
-
// on the ffmpeg input URL.
|
|
24
|
-
const READ_WINDOW_SECONDS = 30;
|
|
25
|
-
// Bounds, so a wrong or unusual byte rate cannot ask for something absurd. The
|
|
26
|
-
// floor keeps a few pieces in flight on a low-bitrate file; the ceiling keeps
|
|
27
|
-
// one reader from claiming more than a fraction of the piece store.
|
|
28
|
-
const READ_WINDOW_MIN_BYTES = 16 * 1024 * 1024;
|
|
29
|
-
const READ_WINDOW_MAX_BYTES = 96 * 1024 * 1024;
|
|
30
|
-
|
|
31
18
|
export class HostLoad {
|
|
32
19
|
/** What this reads and asks of the rest of the proxy, and nothing else. @type {object} */
|
|
33
20
|
#host;
|
|
@@ -42,8 +29,7 @@ export class HostLoad {
|
|
|
42
29
|
#idleLoadSample = null;
|
|
43
30
|
|
|
44
31
|
/**
|
|
45
|
-
* How long each file is in bytes, from the
|
|
46
|
-
* makes. The torrent moves the CONTAINER, so this — not the video stream's
|
|
32
|
+
* How long each file is in bytes, from its declared source statistics. The torrent moves the CONTAINER, so this — not the video stream's
|
|
47
33
|
* bitrate — is what its work should be priced against.
|
|
48
34
|
*
|
|
49
35
|
* @type {Map<string, number>}
|
|
@@ -96,67 +82,18 @@ export class HostLoad {
|
|
|
96
82
|
this.#host = host;
|
|
97
83
|
}
|
|
98
84
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*
|
|
103
|
-
* @param {string} sourceKey
|
|
104
|
-
* @param {number} fileIndex
|
|
105
|
-
* @param {number} durationSeconds
|
|
106
|
-
* @returns {Promise<number>} Zero when the byte rate cannot be established,
|
|
107
|
-
* which leaves the reader on its own default.
|
|
108
|
-
*/
|
|
109
|
-
async readWindowBytesFor(sourceKey, fileIndex, durationSeconds) {
|
|
110
|
-
// The length read here is also what prices the torrent's own work for this
|
|
111
|
-
// file, so it is remembered rather than discarded.
|
|
112
|
-
|
|
113
|
-
if (!this.#host.getSourceStats || !Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
114
|
-
return 0;
|
|
115
|
-
}
|
|
116
|
-
let fileLength = 0;
|
|
85
|
+
/** Read the declared source length for pricing; this does not request file bytes. */
|
|
86
|
+
async readFileLength(sourceKey, fileIndex) {
|
|
87
|
+
if (!this.#host.getSourceStats) return null;
|
|
117
88
|
try {
|
|
118
89
|
const stats = await this.#host.getSourceStats(sourceKey, fileIndex);
|
|
119
|
-
|
|
90
|
+
const length = Number(stats?.fileLength);
|
|
91
|
+
if (!Number.isFinite(length) || length <= 0) return null;
|
|
92
|
+
this.fileLengthByKey.set(SourceFiles.keyFor(sourceKey, fileIndex), length);
|
|
93
|
+
return length;
|
|
120
94
|
} catch {
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
if (!Number.isFinite(fileLength) || fileLength <= 0) {
|
|
124
|
-
return 0;
|
|
125
|
-
}
|
|
126
|
-
this.fileLengthByKey.set(SourceFiles.keyFor(sourceKey, fileIndex), fileLength);
|
|
127
|
-
const bytesPerSecond = fileLength / durationSeconds;
|
|
128
|
-
// Shared between the readers this file already has. The window is stated in
|
|
129
|
-
// seconds of playback and the store's memory is one budget for the whole
|
|
130
|
-
// torrent, so N readers asking for thirty seconds each ask for N times what
|
|
131
|
-
// was provided for — and on 2026-08-15 that is exactly what happened: a
|
|
132
|
-
// viewer with a picture and an audio track had every resident piece held at
|
|
133
|
-
// once, a read ended with zero bytes, and every encoder on the file took
|
|
134
|
-
// that for the end of it.
|
|
135
|
-
//
|
|
136
|
-
// Dividing keeps the promise the budget was written against. It is not the
|
|
137
|
-
// sliding window of roadmap item 8 — pieces still leave only by the store's
|
|
138
|
-
// own eviction — but it removes the multiplication that broke it.
|
|
139
|
-
const readers = Math.max(1, this.#readersOn(sourceKey, fileIndex));
|
|
140
|
-
const wanted = Math.round((bytesPerSecond * READ_WINDOW_SECONDS) / readers);
|
|
141
|
-
return Math.min(READ_WINDOW_MAX_BYTES, Math.max(READ_WINDOW_MIN_BYTES, wanted));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* How many live sessions read this file: the picture, any rung being warmed
|
|
146
|
-
* beside it, and any audio track published on its own.
|
|
147
|
-
*
|
|
148
|
-
* @param {string} sourceKey
|
|
149
|
-
* @param {number} fileIndex
|
|
150
|
-
* @returns {number}
|
|
151
|
-
*/
|
|
152
|
-
#readersOn(sourceKey, fileIndex) {
|
|
153
|
-
let readers = 0;
|
|
154
|
-
for (const session of this.#host.outputs.values()) {
|
|
155
|
-
if (session?.file.sourceKey === sourceKey && session.file.fileIndex === fileIndex) {
|
|
156
|
-
readers += 1;
|
|
157
|
-
}
|
|
95
|
+
return null;
|
|
158
96
|
}
|
|
159
|
-
return readers;
|
|
160
97
|
}
|
|
161
98
|
|
|
162
99
|
/**
|
|
@@ -172,16 +109,23 @@ export class HostLoad {
|
|
|
172
109
|
* is the torrent's.
|
|
173
110
|
*/
|
|
174
111
|
async #learnTorrentCost() {
|
|
112
|
+
const activityBefore = this.#host.readMetadataActivity?.();
|
|
113
|
+
const bytes = await this.#torrentBytesMoved();
|
|
175
114
|
const now = {
|
|
176
115
|
takenAt: Date.now(),
|
|
177
116
|
cpuSeconds: this.#host.readProxyCpuSeconds(),
|
|
178
|
-
bytes
|
|
117
|
+
bytes,
|
|
118
|
+
metadata: this.#host.readMetadataActivity?.()
|
|
179
119
|
};
|
|
180
120
|
const previous = this.#idleLoadSample;
|
|
181
121
|
this.#idleLoadSample = now;
|
|
182
122
|
if (previous === null || now.bytes === null || previous.bytes === null) {
|
|
183
123
|
return;
|
|
184
124
|
}
|
|
125
|
+
// Process CPU includes metadata parsing. Keep the sample as a baseline,
|
|
126
|
+
// but never attribute an interval containing that work to torrent bytes.
|
|
127
|
+
if (activityBefore?.active || now.metadata?.active || previous.metadata?.active ||
|
|
128
|
+
activityBefore?.epoch !== now.metadata?.epoch || previous.metadata?.epoch !== now.metadata?.epoch) return;
|
|
185
129
|
const elapsedSec = (now.takenAt - previous.takenAt) / 1000;
|
|
186
130
|
const megabytes = (now.bytes - previous.bytes) / 1e6;
|
|
187
131
|
// Divided by the cores, because `process.cpuUsage()` adds up every thread
|
|
@@ -260,6 +204,8 @@ export class HostLoad {
|
|
|
260
204
|
// client, whichever of its files are being read. Kept per source and
|
|
261
205
|
// divided among the files being watched, so two episodes of one pack
|
|
262
206
|
// do not each charge the machine for the whole download.
|
|
207
|
+
const length = Number(stats?.fileLength);
|
|
208
|
+
if (Number.isFinite(length) && length > 0) this.fileLengthByKey.set(key, length);
|
|
263
209
|
const rate = Number(stats?.downloadSpeed);
|
|
264
210
|
if (Number.isFinite(rate) && rate >= 0) {
|
|
265
211
|
measured.set(source.sourceKey, rate);
|
|
@@ -409,7 +409,7 @@ export class QualityOffer {
|
|
|
409
409
|
return videoLoadForFrame({
|
|
410
410
|
sourceHeight,
|
|
411
411
|
copiesAtSource: !base.spec.transcodesVideo,
|
|
412
|
-
sourceMbps: base.file.
|
|
412
|
+
sourceMbps: base.file.megabitsPerSecond,
|
|
413
413
|
encoderKind
|
|
414
414
|
}, frame);
|
|
415
415
|
}
|