@torrent-tv/proxy 2.86.0 → 2.87.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 +2138 -1915
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +745 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +931 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/wire-outputs.js +958 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -45
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -515
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +40 -8
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -202
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9608
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/services/viewer/link-readings.js +0 -56
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
package/test/decode-cost.test.js
CHANGED
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
|
|
24
24
|
import test from "node:test";
|
|
25
25
|
import assert from "node:assert/strict";
|
|
26
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
26
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
27
27
|
import { createRequire } from "node:module";
|
|
28
28
|
import { spawn } from "node:child_process";
|
|
29
29
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
30
|
-
import { decodeCostOf, decodeFamilyOf } from "../services/decode-cost-fit.js";
|
|
31
|
-
import { Output } from "../services/output/Output.js";
|
|
30
|
+
import { decodeCostOf, decodeFamilyOf } from "../services/encode/decode-cost-fit.js";
|
|
31
|
+
import { Output } from "../services/encode/output/Output.js";
|
|
32
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
32
33
|
import os from "node:os";
|
|
33
34
|
import path from "node:path";
|
|
34
35
|
import {
|
|
@@ -36,11 +37,11 @@ import {
|
|
|
36
37
|
decodeSpeedFor,
|
|
37
38
|
predictedRealtimeSpeed,
|
|
38
39
|
speedBar
|
|
39
|
-
} from "../services/hwaccel.js";
|
|
40
|
-
import {
|
|
41
|
-
import { SourceFile, sourceDecodeCharacteristics } from "../services/
|
|
42
|
-
import { parseFfmpegBitrateKbps, parseFfmpegVideoDimensions, parseFfmpegVideoFps } from "../services/ffmpeg-banner.js";
|
|
43
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
40
|
+
} from "../services/encode/hwaccel.js";
|
|
41
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
42
|
+
import { SourceFile, sourceDecodeCharacteristics } from "../services/media/SourceFile.js";
|
|
43
|
+
import { parseFfmpegBitrateKbps, parseFfmpegVideoDimensions, parseFfmpegVideoFps } from "../services/media/ffmpeg-banner.js";
|
|
44
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
44
45
|
|
|
45
46
|
const require = createRequire(import.meta.url);
|
|
46
47
|
const ffmpegBin = require("ffmpeg-static");
|
|
@@ -187,15 +188,22 @@ test("an observation prices a host whose clips were never fitted", () => {
|
|
|
187
188
|
assert.ok(priced.speed < 1, `priced ${priced.speed?.toFixed(2)}x`);
|
|
188
189
|
});
|
|
189
190
|
|
|
190
|
-
test("
|
|
191
|
-
const
|
|
192
|
-
benchmark:
|
|
191
|
+
test("not calibrated means nothing judged; calibrated with nothing qualified means refused", () => {
|
|
192
|
+
const uncalibrated = canSustainOutput({
|
|
193
|
+
benchmark: null,
|
|
193
194
|
decodeModel: null,
|
|
194
195
|
source: null,
|
|
195
196
|
outputPixelsPerSec: 1920 * 1080 * 24
|
|
196
197
|
});
|
|
198
|
+
assert.deepEqual(uncalibrated, { speed: null, sustainable: true });
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
const nothingQualified = canSustainOutput({
|
|
201
|
+
benchmark: [],
|
|
202
|
+
decodeModel: null,
|
|
203
|
+
source: null,
|
|
204
|
+
outputPixelsPerSec: 1920 * 1080 * 24
|
|
205
|
+
});
|
|
206
|
+
assert.deepEqual(nothingQualified, { speed: null, sustainable: false });
|
|
199
207
|
});
|
|
200
208
|
|
|
201
209
|
test("a real ffmpeg banner reads into the figures the budget prices", async () => {
|
|
@@ -285,7 +293,7 @@ test("the source's decode figures come off the probe, or not at all", () => {
|
|
|
285
293
|
|
|
286
294
|
test("the OFFER drops the rungs the host cannot hold, and the master keeps addressing them", async (t) => {
|
|
287
295
|
const dirPath = await mkdtemp(path.join(os.tmpdir(), "decode-cost-"));
|
|
288
|
-
const manager =
|
|
296
|
+
const manager = wireOutputs({
|
|
289
297
|
enabled: true,
|
|
290
298
|
ffmpegBin: "ffmpeg",
|
|
291
299
|
localBindHost: "127.0.0.1",
|
|
@@ -296,12 +304,13 @@ test("the OFFER drops the rungs the host cannot hold, and the master keeps addre
|
|
|
296
304
|
decodeCostModel: ADDON_HOST_MODEL
|
|
297
305
|
});
|
|
298
306
|
t.after(async () => {
|
|
299
|
-
await manager.disposeAll();
|
|
307
|
+
await manager.lifecycle.disposeAll();
|
|
300
308
|
await rm(dirPath, { recursive: true, force: true });
|
|
301
309
|
});
|
|
302
310
|
|
|
303
311
|
const session = {
|
|
304
|
-
id: "
|
|
312
|
+
id: "ccccccccddddeeee",
|
|
313
|
+
spec: outputSpec({ transcodeVideo: false, cutGrid: "keyframe" }),
|
|
305
314
|
dirPath,
|
|
306
315
|
// Where this file is cut, held by the file. A fixture that stated it
|
|
307
316
|
// on the session was describing what production no longer does.
|
|
@@ -348,10 +357,10 @@ test("the OFFER drops the rungs the host cannot hold, and the master keeps addre
|
|
|
348
357
|
segmentCount: 100,
|
|
349
358
|
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
|
|
350
359
|
};
|
|
351
|
-
manager.
|
|
360
|
+
manager.outputs.set(session.id, session);
|
|
352
361
|
|
|
353
362
|
assert.deepEqual(
|
|
354
|
-
manager.offeredHeights(session),
|
|
363
|
+
manager.quality.offeredHeights(session),
|
|
355
364
|
[1080],
|
|
356
365
|
"every rung under the copy runs below realtime here, so there is nothing to switch to"
|
|
357
366
|
);
|
|
@@ -360,7 +369,7 @@ test("the OFFER drops the rungs the host cannot hold, and the master keeps addre
|
|
|
360
369
|
// session's life: the browser is handed its address at creation, and a live
|
|
361
370
|
// figure that withdrew it left a session answering 404 to itself (field
|
|
362
371
|
// 2026-08-18, "Moana (2016).mkv" — nothing played at all).
|
|
363
|
-
const weakMaster = manager.buildMasterPlaylist(session.id);
|
|
372
|
+
const weakMaster = manager.renditions.buildMasterPlaylist(session.id);
|
|
364
373
|
assert.ok(weakMaster, "published once, whatever the host is managing this second");
|
|
365
374
|
assert.deepEqual(
|
|
366
375
|
[...weakMaster.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
|
|
@@ -371,14 +380,14 @@ test("the OFFER drops the rungs the host cannot hold, and the master keeps addre
|
|
|
371
380
|
// A host with a little more encoder keeps the rungs it can actually hold. A
|
|
372
381
|
// second session, because the answer is settled once per session.
|
|
373
382
|
manager.softwarePresetBenchmark = [{ preset: "ultrafast", pixelsPerSec: 12e6 }];
|
|
374
|
-
const stronger = { ...session, id: "
|
|
375
|
-
manager.
|
|
383
|
+
const stronger = { ...session, id: "ddddddddeeeeffff", offeredHeightsCache: undefined };
|
|
384
|
+
manager.outputs.set(stronger.id, stronger);
|
|
376
385
|
assert.deepEqual(
|
|
377
|
-
manager.offeredHeights(stronger),
|
|
386
|
+
manager.quality.offeredHeights(stronger),
|
|
378
387
|
[1080, 360, 240],
|
|
379
388
|
"nothing is known about this swarm, so the bar is realtime"
|
|
380
389
|
);
|
|
381
|
-
const master = manager.buildMasterPlaylist(stronger.id);
|
|
390
|
+
const master = manager.renditions.buildMasterPlaylist(stronger.id);
|
|
382
391
|
assert.ok(master, "1080p copied plus every rung that can be spliced beside it");
|
|
383
392
|
assert.deepEqual(
|
|
384
393
|
[...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
|
|
@@ -391,13 +400,13 @@ test("the OFFER drops the rungs the host cannot hold, and the master keeps addre
|
|
|
391
400
|
// 1.67x of any step, and the rungs that only just cleared realtime go.
|
|
392
401
|
const onAThinSwarm = {
|
|
393
402
|
...stronger,
|
|
394
|
-
id: "
|
|
403
|
+
id: "eeeeeeeeffff0000",
|
|
395
404
|
offeredHeightsCache: undefined,
|
|
396
405
|
supplyFigures: { requiredSpeed: 1.67, worstWaitSec: 1.49, medianIntervalSec: 2.22, samples: 12 }
|
|
397
406
|
};
|
|
398
|
-
manager.
|
|
407
|
+
manager.outputs.set(onAThinSwarm.id, onAThinSwarm);
|
|
399
408
|
assert.deepEqual(
|
|
400
|
-
manager.offeredHeights(onAThinSwarm),
|
|
409
|
+
manager.quality.offeredHeights(onAThinSwarm),
|
|
401
410
|
[1080],
|
|
402
411
|
"the copied height costs no encoder and stays; nothing re-encoded survives that supply"
|
|
403
412
|
);
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
|
|
12
|
-
import { DemandRegister } from "../services/demand/DemandRegister.js";
|
|
13
|
-
import { unionOf, Window } from "../services/demand/Window.js";
|
|
12
|
+
import { DemandRegister } from "../services/torrent/demand/DemandRegister.js";
|
|
13
|
+
import { unionOf, Window } from "../services/torrent/demand/Window.js";
|
|
14
14
|
import {
|
|
15
15
|
isConditional,
|
|
16
16
|
mayDisplaceSlowPeer,
|
|
17
17
|
selectionPriority,
|
|
18
18
|
Urgency
|
|
19
|
-
} from "../services/demand/Urgency.js";
|
|
20
|
-
import { nearestFirst, piecesNeededFor, piecesOf, piecesWithin } from "../services/demand/pieces.js";
|
|
19
|
+
} from "../services/torrent/demand/Urgency.js";
|
|
20
|
+
import { nearestFirst, piecesNeededFor, piecesOf, piecesWithin } from "../services/torrent/demand/pieces.js";
|
|
21
21
|
|
|
22
22
|
const MEGABYTE = 1024 * 1024;
|
|
23
23
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
15
|
import test from "node:test";
|
|
16
16
|
|
|
17
|
-
import { dhtNodeCount, parseBootstrapEntry, resolveDhtBootstrap } from "../services/torrent-pool.js";
|
|
17
|
+
import { dhtNodeCount, parseBootstrapEntry, resolveDhtBootstrap } from "../services/torrent/torrent-pool.js";
|
|
18
18
|
|
|
19
19
|
test("a bootstrap entry is split into host and port", () => {
|
|
20
20
|
assert.deepEqual(parseBootstrapEntry("dht.libtorrent.org:25401"), {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file When a move to a smaller output is prepared for a viewer, and when it is
|
|
3
|
+
* not (roadmap item 98), and which way their buffer is going.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { drainThreat } from "../services/encode/quality/drain-threat.js";
|
|
9
|
+
import { bufferTrend } from "../services/viewer/buffer-trend.js";
|
|
10
|
+
|
|
11
|
+
test("a buffer that falls but lasts past the new piece's readiness moves nothing", () => {
|
|
12
|
+
// 60 s held, draining at 0.5 s/s: empty in 120 s. The new output would have
|
|
13
|
+
// the piece in 8 s, and the next report comes in 10 s.
|
|
14
|
+
const answer = drainThreat({ bufferedSec: 60, slope: -0.5, reportGapSec: 10, secondsToReady: 8 });
|
|
15
|
+
assert.equal(answer.threat, false);
|
|
16
|
+
assert.equal(answer.secondsToEmpty, 120);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a buffer that would end before the new piece is ready starts the move", () => {
|
|
20
|
+
// 6 s held, draining at 0.5 s/s: empty in 12 s — sooner than 8 s of making
|
|
21
|
+
// plus the 10 s until this viewer is heard from again.
|
|
22
|
+
assert.equal(drainThreat({ bufferedSec: 6, slope: -0.5, reportGapSec: 10, secondsToReady: 8 }).threat, true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("the wait for the next report is part of the reckoning", () => {
|
|
26
|
+
const without = drainThreat({ bufferedSec: 10, slope: -1, reportGapSec: 0, secondsToReady: 8 });
|
|
27
|
+
const withGap = drainThreat({ bufferedSec: 10, slope: -1, reportGapSec: 3, secondsToReady: 8 });
|
|
28
|
+
assert.equal(without.threat, false, "10 s left against 8 s of making");
|
|
29
|
+
assert.equal(withGap.threat, true, "but the next chance to act is 3 s away, too late");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a buffer that holds or grows is no threat, and neither is one with no trend yet", () => {
|
|
33
|
+
assert.equal(drainThreat({ bufferedSec: 1, slope: 0, reportGapSec: 10, secondsToReady: 30 }).threat, false);
|
|
34
|
+
assert.equal(drainThreat({ bufferedSec: 1, slope: 0.4, reportGapSec: 10, secondsToReady: 30 }).threat, false);
|
|
35
|
+
assert.equal(drainThreat({ bufferedSec: 1, slope: null, reportGapSec: 10, secondsToReady: 30 }).threat, false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a readiness nobody can state is not assumed to be quick", () => {
|
|
39
|
+
assert.equal(drainThreat({ bufferedSec: 600, slope: -0.1, reportGapSec: 10, secondsToReady: null }).threat, true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("the trend is read over one segment's period, not from two readings", () => {
|
|
43
|
+
// A sawtooth over a flat line: a 4 s segment arrives, then 4 s play out.
|
|
44
|
+
const at = (seconds) => seconds * 1000;
|
|
45
|
+
const flat = [
|
|
46
|
+
{ at: at(0), seconds: 20 },
|
|
47
|
+
{ at: at(2), seconds: 18 },
|
|
48
|
+
{ at: at(4), seconds: 20 },
|
|
49
|
+
{ at: at(6), seconds: 18 },
|
|
50
|
+
{ at: at(8), seconds: 20 }
|
|
51
|
+
];
|
|
52
|
+
const trend = bufferTrend(flat, 4);
|
|
53
|
+
assert.ok(Math.abs(trend.slope) < 0.5, `the last two readings alone say +1 s/s; the period says ${trend.slope}`);
|
|
54
|
+
assert.equal(trend.reportGapSec, 2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("readings that do not yet span one segment say nothing", () => {
|
|
58
|
+
assert.equal(bufferTrend([{ at: 0, seconds: 10 }, { at: 1000, seconds: 9 }], 4), null);
|
|
59
|
+
assert.equal(bufferTrend([{ at: 0, seconds: 10 }], 4), null);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a steady drain reads as the rate it drains at", () => {
|
|
63
|
+
const readings = [0, 5, 10].map((second) => ({ at: second * 1000, seconds: 30 - second }));
|
|
64
|
+
assert.equal(bufferTrend(readings, 4).slope, -1);
|
|
65
|
+
});
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
|
-
import { duplicateAddDecision } from "../services/torrent-pool.js";
|
|
20
|
+
import { duplicateAddDecision } from "../services/torrent/torrent-pool.js";
|
|
21
21
|
|
|
22
22
|
test("a ready torrent is adopted, whatever the new source carries", () => {
|
|
23
23
|
assert.equal(
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether the whole machine has a place for one more encoder (roadmap
|
|
3
|
+
* item 97, step 13).
|
|
4
|
+
*
|
|
5
|
+
* What is pinned here:
|
|
6
|
+
*
|
|
7
|
+
* 1. a place is held by an OUTPUT: two preparations onto one output take one
|
|
8
|
+
* place, and the second is admitted without arithmetic;
|
|
9
|
+
* 2. two preparations onto two DIFFERENT outputs with room for one: the first
|
|
10
|
+
* is admitted, the second refused, because it is asked after the first is
|
|
11
|
+
* recorded;
|
|
12
|
+
* 3. one of two viewers giving up a preparation of one output does not free
|
|
13
|
+
* its place;
|
|
14
|
+
* 4. the cost is what an encoder costs, not a count of processes: a picture
|
|
15
|
+
* and its soundtrack fit where two pictures do not — the addon host's own
|
|
16
|
+
* figures, 1080p alone at 1.96x;
|
|
17
|
+
* 5. an admitted encoder is never taken away: the machine's answer is never
|
|
18
|
+
* below what already runs, nor below one for an output a preparation holds;
|
|
19
|
+
* 6. a new output on a full machine is given no encoder.
|
|
20
|
+
*
|
|
21
|
+
* The records of who is being prepared are stood in for by a set the check
|
|
22
|
+
* writes to, which is what the viewers' records are read as.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import test from "node:test";
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
|
|
28
|
+
|
|
29
|
+
// 1080p re-encoded on the addon host: 1.96x alone.
|
|
30
|
+
const PICTURE_1080 = 1 / 1.96;
|
|
31
|
+
// A re-encoded soundtrack: a small fraction of a picture.
|
|
32
|
+
const SOUNDTRACK = 0.02;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A machine with the outputs named, their costs, what runs and who is being
|
|
36
|
+
* prepared onto what.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ costs: Record<string, number | null>, running?: Record<string, number>, share?: number | null }} how
|
|
39
|
+
*/
|
|
40
|
+
function machine(how) {
|
|
41
|
+
const running = new Map(Object.entries(how.running ?? {}));
|
|
42
|
+
/** Output address to the viewers being prepared onto it. @type {Map<string, Set<string>>} */
|
|
43
|
+
const preparing = new Map();
|
|
44
|
+
const admission = new EncodeAdmission({
|
|
45
|
+
liveRunsByAddress: () => new Map([...running].filter(([, count]) => count > 0)),
|
|
46
|
+
preparedAddresses: () => new Set([...preparing].filter(([, viewers]) => viewers.size > 0).map(([address]) => address)),
|
|
47
|
+
loadOf: (address) => (address in how.costs ? { costSec: how.costs[address], fileKey: "", fileCostSec: 0 } : null),
|
|
48
|
+
availability: () => (how.share === undefined || how.share === null ? null : { share: how.share, known: true })
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
admission,
|
|
52
|
+
running,
|
|
53
|
+
/** A viewer's preparation recorded, as `Viewer` records it. */
|
|
54
|
+
prepare: (address, viewer) => {
|
|
55
|
+
const viewers = preparing.get(address) ?? new Set();
|
|
56
|
+
viewers.add(viewer);
|
|
57
|
+
preparing.set(address, viewers);
|
|
58
|
+
},
|
|
59
|
+
/** A viewer's preparation ended, on whichever path. */
|
|
60
|
+
giveUp: (address, viewer) => preparing.get(address)?.delete(viewer)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test("two preparations onto one output take one place", () => {
|
|
65
|
+
const { admission, prepare } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
|
|
66
|
+
assert.equal(admission.admitsPreparation("a").admitted, true);
|
|
67
|
+
prepare("a", "viewer-1");
|
|
68
|
+
const second = admission.admitsPreparation("a");
|
|
69
|
+
assert.equal(second.admitted, true, "the second viewer shares the place");
|
|
70
|
+
assert.match(second.reason, /already holds a place/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("two preparations onto two different outputs with room for one: the second is refused", () => {
|
|
74
|
+
const { admission, prepare } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
|
|
75
|
+
// The first is asked and recorded in one stretch, as the preparation does it.
|
|
76
|
+
assert.equal(admission.admitsPreparation("a").admitted, true);
|
|
77
|
+
prepare("a", "viewer-1");
|
|
78
|
+
const second = admission.admitsPreparation("b");
|
|
79
|
+
assert.equal(second.admitted, false, "both together would make 0.98x of each");
|
|
80
|
+
assert.ok(second.speedX < 1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("one of two viewers giving up a preparation of one output does not free its place", () => {
|
|
84
|
+
const { admission, prepare, giveUp } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
|
|
85
|
+
prepare("a", "viewer-1");
|
|
86
|
+
prepare("a", "viewer-2");
|
|
87
|
+
giveUp("a", "viewer-1");
|
|
88
|
+
assert.equal(admission.admitsPreparation("b").admitted, false, "viewer-2 still holds the place on a");
|
|
89
|
+
assert.equal(admission.placesFor("a", 0).runs, 1, "and a keeps its encoder's place");
|
|
90
|
+
giveUp("a", "viewer-2");
|
|
91
|
+
assert.equal(admission.admitsPreparation("b").admitted, true, "the place ends with the last record");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a picture and its soundtrack fit where two pictures do not", () => {
|
|
95
|
+
const { admission } = machine({
|
|
96
|
+
costs: { picture: PICTURE_1080, sound: SOUNDTRACK, other: PICTURE_1080 },
|
|
97
|
+
running: { picture: 1 }
|
|
98
|
+
});
|
|
99
|
+
assert.equal(admission.admitsPreparation("sound").admitted, true, "0.53 s/s makes 1.89x");
|
|
100
|
+
assert.equal(admission.placesFor("sound", 0).runs >= 1, true);
|
|
101
|
+
assert.equal(admission.admitsPreparation("other").admitted, false, "1.02 s/s makes 0.98x");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("an admitted encoder is never taken away, and a held place is kept", () => {
|
|
105
|
+
const { admission } = machine({
|
|
106
|
+
costs: { a: PICTURE_1080, b: PICTURE_1080 },
|
|
107
|
+
running: { a: 1, b: 1 }
|
|
108
|
+
});
|
|
109
|
+
assert.equal(admission.placesFor("a", 1).runs, 1, "both running: each keeps its one");
|
|
110
|
+
assert.equal(admission.placesFor("b", 1).runs, 1);
|
|
111
|
+
const { admission: held, prepare: prepareHeld } = machine({
|
|
112
|
+
costs: { a: PICTURE_1080, b: PICTURE_1080 },
|
|
113
|
+
running: { b: 1 }
|
|
114
|
+
});
|
|
115
|
+
prepareHeld("a", "viewer-1");
|
|
116
|
+
assert.equal(held.placesFor("a", 0).runs, 1, "a place promised to a preparation is not withdrawn");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a new output on a full machine is given no encoder", () => {
|
|
120
|
+
const { admission } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 }, running: { a: 1 } });
|
|
121
|
+
assert.equal(admission.placesFor("b", 0).runs, 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("the share of the machine nobody has priced is taken off what there is", () => {
|
|
125
|
+
// 0.6 s/s alone makes 1.67x; with a third of the machine taken by work
|
|
126
|
+
// nobody priced it makes 1.11x, and a soundtrack beside it still fits.
|
|
127
|
+
const quiet = machine({ costs: { a: 0.6, b: 0.4 }, share: 1 });
|
|
128
|
+
assert.equal(quiet.admission.admitsPreparation("a").admitted, true);
|
|
129
|
+
const busy = machine({ costs: { a: 0.6, b: 0.4 }, running: { a: 1 }, share: 0.6 });
|
|
130
|
+
assert.equal(busy.admission.admitsPreparation("b").admitted, false, "1.0 s/s at 0.6 of the machine makes 0.6x");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("an output with no measured cost is not admitted", () => {
|
|
134
|
+
const { admission } = machine({ costs: { a: PICTURE_1080, b: null }, running: { a: 1 } });
|
|
135
|
+
const answer = admission.admitsPreparation("b");
|
|
136
|
+
assert.equal(answer.admitted, false);
|
|
137
|
+
assert.match(answer.reason, /no measured encoding cost/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("unpriced occupied work is neither spare pool headroom nor capacity for another output", () => {
|
|
141
|
+
const { admission } = machine({ costs: { a: null, b: PICTURE_1080 }, running: { a: 1 } });
|
|
142
|
+
assert.equal(admission.headroom().encodeSpeedX, 0, "the pool sees no confirmed room");
|
|
143
|
+
const answer = admission.admitsPreparation("b");
|
|
144
|
+
assert.equal(answer.admitted, false);
|
|
145
|
+
assert.match(answer.reason, /capacity is unknown/);
|
|
146
|
+
});
|
package/test/encode-cost.test.js
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
|
-
import { EncodeCost } from "../services/quality/EncodeCost.js";
|
|
12
|
+
import { EncodeCost } from "../services/encode/quality/EncodeCost.js";
|
|
13
|
+
import { startRunOn } from "./helpers/encode-run.js";
|
|
14
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
15
|
+
import { runStateOf } from "../services/encode/encode-run-state.js";
|
|
16
|
+
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* @param {object} [readings]
|
|
@@ -19,7 +23,10 @@ function costOn(readings = {}) {
|
|
|
19
23
|
let asked = 0;
|
|
20
24
|
const host = { share: 1 };
|
|
21
25
|
const cost = new EncodeCost({
|
|
22
|
-
|
|
26
|
+
outputs: {
|
|
27
|
+
familyOf: (session) => readings.familyOf?.(session) ?? [],
|
|
28
|
+
variantHeightOf: () => 0
|
|
29
|
+
},
|
|
23
30
|
host: () => {
|
|
24
31
|
asked += 1;
|
|
25
32
|
return {
|
|
@@ -29,14 +36,60 @@ function costOn(readings = {}) {
|
|
|
29
36
|
availability: { known: true, share: host.share }
|
|
30
37
|
};
|
|
31
38
|
},
|
|
32
|
-
|
|
33
|
-
runningEncoders: () => 0,
|
|
39
|
+
runningEncoders: () => readings.runningEncoders?.() ?? 0,
|
|
34
40
|
encodersRunningNow: () => 0,
|
|
35
|
-
torrentCostSecFor: () => 0
|
|
41
|
+
torrentCostSecFor: () => 0,
|
|
42
|
+
boundBy: async () => readings.boundBy ?? "cpu",
|
|
43
|
+
runsFor: (session) => [...(session.runs ?? [])],
|
|
44
|
+
stateFor: (session) => runStateOf(session.runs),
|
|
45
|
+
progressFor: (session) => session.progress ?? null
|
|
36
46
|
});
|
|
37
47
|
return { cost, asked: () => asked, host };
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A session with one run going, of the kind this file measures.
|
|
52
|
+
*
|
|
53
|
+
* @param {{ audioOnly?: boolean, transcodeVideo?: boolean, audioSourceTrackIndex?: number, key?: string }} [what]
|
|
54
|
+
* @returns {object}
|
|
55
|
+
*/
|
|
56
|
+
function sessionProducing(what = {}) {
|
|
57
|
+
const session = {
|
|
58
|
+
id: "1111111122223333",
|
|
59
|
+
state: "ready",
|
|
60
|
+
spec: outputSpec({
|
|
61
|
+
audioOnly: what.audioOnly === true,
|
|
62
|
+
transcodeVideo: what.transcodeVideo === true,
|
|
63
|
+
audioSourceTrackIndex: what.audioSourceTrackIndex ?? 0
|
|
64
|
+
}),
|
|
65
|
+
file: { key: what.key ?? "torrent:abc:0", name: "film.mkv" },
|
|
66
|
+
output: { encodeWidth: 0, encodeHeight: 0, outputFps: 25, softwarePreset: null },
|
|
67
|
+
progress: { processedSeconds: 0 },
|
|
68
|
+
runs: new Set()
|
|
69
|
+
};
|
|
70
|
+
startRunOn(session, { from: 0, speedX: 1 });
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Two readings of one run, far enough apart to be a speed.
|
|
76
|
+
*
|
|
77
|
+
* @param {EncodeCost} cost
|
|
78
|
+
* @param {object} session
|
|
79
|
+
* @param {number} speedX - Seconds of film produced per second of clock.
|
|
80
|
+
* @returns {Promise<void>}
|
|
81
|
+
*/
|
|
82
|
+
async function watchItRun(cost, session, speedX) {
|
|
83
|
+
session.progress.processedSeconds = 0;
|
|
84
|
+
await cost.learnFrom(session);
|
|
85
|
+
// The pair is a DELTA, and the first reading has nothing to be a delta from.
|
|
86
|
+
// Backdated rather than waited for: the window a speed may be read over is
|
|
87
|
+
// seconds, and a test that sleeps through it measures the clock.
|
|
88
|
+
qualityStateOf(session).learnSample.takenAt -= 10_000;
|
|
89
|
+
session.progress.processedSeconds = 10 * speedX;
|
|
90
|
+
await cost.learnFrom(session);
|
|
91
|
+
}
|
|
92
|
+
|
|
40
93
|
test("the host is asked at the moment of the question, not when this was built", () => {
|
|
41
94
|
// The share of the machine that is free is re-read every few seconds. Copied
|
|
42
95
|
// into this object when it was made, every later rung would be priced against
|
|
@@ -85,10 +138,146 @@ test("a rung measured below realtime is withdrawn, and a copied source height is
|
|
|
85
138
|
assert.deepEqual(kept, [1080], "the copy stays; the rung seen failing does not");
|
|
86
139
|
});
|
|
87
140
|
|
|
88
|
-
test("what an encoder taught this host is
|
|
141
|
+
test("what an encoder taught this host is learned here, and held here", () => {
|
|
142
|
+
// The three that learn were methods of the session manager, writing into maps
|
|
143
|
+
// this object owned — one keeper, a different author, and the maps public for
|
|
144
|
+
// it. Nothing outside can write a price now, and nothing outside needs to.
|
|
145
|
+
const { cost } = costOn();
|
|
146
|
+
|
|
147
|
+
assert.equal(typeof cost.learnFrom, "function");
|
|
148
|
+
assert.equal(cost.copyCost, undefined, "the store is not the surface");
|
|
149
|
+
assert.equal(cost.decodeCostFor("torrent:abc:0"), null, "and nothing is known until an encoder says so");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a copy is priced from what it was seen doing", async () => {
|
|
89
153
|
const { cost } = costOn();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
154
|
+
const session = sessionProducing({ transcodeVideo: false });
|
|
155
|
+
|
|
156
|
+
await watchItRun(cost, session, 8);
|
|
157
|
+
|
|
158
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 1, "the price is published, so every offer recomputes");
|
|
159
|
+
assert.equal(
|
|
160
|
+
qualityStateOf(session).lastAloneSpeed.toFixed(2),
|
|
161
|
+
"8.00",
|
|
162
|
+
"and what it did with the machine to itself is on the session, which withdraws a step seen failing"
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a reading taken while the swarm is short is not filed as the price of this host", async () => {
|
|
167
|
+
// A run starved of torrent data reports a speed that measures the swarm. Filed
|
|
168
|
+
// as a price it reads as more work per second than the machine has, and every
|
|
169
|
+
// quality step is then refused on the download's account.
|
|
170
|
+
const { cost } = costOn({ boundBy: "download" });
|
|
171
|
+
const session = sessionProducing({ transcodeVideo: false });
|
|
172
|
+
|
|
173
|
+
await watchItRun(cost, session, 0.3);
|
|
174
|
+
|
|
175
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0, "nothing was learned about the machine");
|
|
176
|
+
assert.equal(qualityStateOf(session).lastAloneSpeed, undefined, "and nothing claims this rung was seen failing");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("a picture's reading taken beside another encoder is not filed", async () => {
|
|
180
|
+
// It contains that other encoder's work, and the budget ADDS the same work
|
|
181
|
+
// again when it predicts — so the price is counted twice and grows with every
|
|
182
|
+
// reading. Measured in the field 2026-08-15: copying, whose truth is 7.9x, was
|
|
183
|
+
// learned as 2.03x, and the quality menu collapsed to one height.
|
|
184
|
+
const { cost } = costOn({ runningEncoders: () => 2 });
|
|
185
|
+
const session = sessionProducing({ transcodeVideo: false });
|
|
186
|
+
|
|
187
|
+
await watchItRun(cost, session, 8);
|
|
188
|
+
|
|
189
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("one reading is not a speed", async () => {
|
|
193
|
+
const { cost } = costOn();
|
|
194
|
+
const session = sessionProducing({ transcodeVideo: false });
|
|
195
|
+
session.progress.processedSeconds = 40;
|
|
196
|
+
|
|
197
|
+
await cost.learnFrom(session);
|
|
198
|
+
|
|
199
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0, "a delta needs two readings of one run");
|
|
200
|
+
assert.ok(qualityStateOf(session).learnSample, "and the first is kept to be the other half of the next");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a pair that straddles a restart measures the seek, not the host", async () => {
|
|
204
|
+
const { cost } = costOn();
|
|
205
|
+
const session = sessionProducing({ transcodeVideo: false });
|
|
206
|
+
await cost.learnFrom(session);
|
|
207
|
+
qualityStateOf(session).learnSample.takenAt -= 10_000;
|
|
208
|
+
// A new run, at another place in the film: twenty minutes of film against five
|
|
209
|
+
// seconds of clock is a seek, and filed as a price it admits every step there
|
|
210
|
+
// is.
|
|
211
|
+
session.runs = new Set();
|
|
212
|
+
startRunOn(session, { from: 300, speedX: 1 });
|
|
213
|
+
session.progress.processedSeconds = 1200;
|
|
214
|
+
|
|
215
|
+
await cost.learnFrom(session);
|
|
216
|
+
|
|
217
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("two soundtracks of one file are priced apart", async () => {
|
|
221
|
+
// The key names the TRACK, not the file. A dub and the original run at
|
|
222
|
+
// different speeds — different codecs, different channel counts — and one key
|
|
223
|
+
// for both files each over the other's readings.
|
|
224
|
+
const { cost } = costOn();
|
|
225
|
+
const first = sessionProducing({ audioOnly: true });
|
|
226
|
+
const second = sessionProducing({ audioOnly: true, audioSourceTrackIndex: 1 });
|
|
227
|
+
|
|
228
|
+
await watchItRun(cost, first, 40);
|
|
229
|
+
|
|
230
|
+
assert.equal(cost.audioVersionFor(first), 1);
|
|
231
|
+
assert.equal(cost.audioVersionFor(second), 0, "the other track has been told nothing about itself");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("a soundtrack's own share is what is left after everything else running is paid for", async () => {
|
|
235
|
+
// A rendition runs for exactly as long as the picture it accompanies, so
|
|
236
|
+
// "alone" is a state it is never in and the price stayed unmeasured for ever.
|
|
237
|
+
// Its share is recovered by subtracting what the machine is already known to
|
|
238
|
+
// be spending — which is only possible once that other work HAS a price.
|
|
239
|
+
const picture = sessionProducing({ transcodeVideo: false });
|
|
240
|
+
const sound = sessionProducing({ audioOnly: true });
|
|
241
|
+
let running = 1;
|
|
242
|
+
const { cost } = costOn({
|
|
243
|
+
runningEncoders: () => running,
|
|
244
|
+
familyOf: () => [picture, sound]
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// The picture alone first, so copying has a price: 8x, i.e. 0.125 s/s.
|
|
248
|
+
await watchItRun(cost, picture, 8);
|
|
249
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 1);
|
|
250
|
+
|
|
251
|
+
// Now the pair. The reading off the soundtrack contains both, at 4x — 0.25 s/s
|
|
252
|
+
// together — so the sound's own share is 0.25 − 0.125 = 0.125 s/s.
|
|
253
|
+
running = 2;
|
|
254
|
+
await watchItRun(cost, sound, 4);
|
|
255
|
+
|
|
256
|
+
assert.equal(cost.audioVersionFor(sound), 1, "the soundtrack has a price at last");
|
|
257
|
+
// What was filed, read back the way the product reads it: the sum of what
|
|
258
|
+
// everything OTHER than the picture is costing. Charged the whole reading
|
|
259
|
+
// instead of its own share, a soundtrack is priced at twice the truth and
|
|
260
|
+
// refuses quality steps the host could hold.
|
|
261
|
+
running = 1;
|
|
262
|
+
assert.equal(
|
|
263
|
+
cost.pricedConcurrentCost(picture).toFixed(3),
|
|
264
|
+
"0.125",
|
|
265
|
+
"0.25 s/s together, less the 0.125 s/s the copy was already known to cost"
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("nothing is attributed to a soundtrack while something running has no price", async () => {
|
|
270
|
+
const picture = sessionProducing({ transcodeVideo: true });
|
|
271
|
+
const sound = sessionProducing({ audioOnly: true });
|
|
272
|
+
const { cost } = costOn({
|
|
273
|
+
runningEncoders: () => 2,
|
|
274
|
+
// A re-encoded picture with no benchmark to price it by and no speed of its
|
|
275
|
+
// own: unpriced work, which would otherwise land in the soundtrack's account
|
|
276
|
+
// and refuse quality steps on it.
|
|
277
|
+
familyOf: () => [picture, sound]
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await watchItRun(cost, sound, 4);
|
|
281
|
+
|
|
282
|
+
assert.equal(cost.audioVersionFor(sound), 0);
|
|
94
283
|
});
|