@torrent-tv/proxy 2.85.1 → 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 -1909
- 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 -489
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +46 -0
- 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 -44
- 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 -518
- 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 -172
- 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 -9689
- 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/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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What a viewer's link can carry, and what an output asks of it.
|
|
3
|
+
*
|
|
4
|
+
* PURE FUNCTIONS: the viewer's connection enters as a NUMBER — how many megabits
|
|
5
|
+
* the last report showed — and never as a reference to a viewer. Nothing here
|
|
6
|
+
* holds anybody, reads anything or can be asked at the wrong moment.
|
|
7
|
+
*
|
|
8
|
+
* WHAT A LINK CARRIES IS A LOAD, NOT A PICTURE (roadmap item 97, step 11). The
|
|
9
|
+
* viewer receives the picture AND the soundtrack they chose — inside the
|
|
10
|
+
* picture's own stream, or as a separate one — so both are added before the
|
|
11
|
+
* link is asked. And each part says how much its figure can be trusted:
|
|
12
|
+
*
|
|
13
|
+
* 1. `known` — a bound the encoder is held to: a re-encode's `maxrate`, or the
|
|
14
|
+
* constant rate a re-encoded soundtrack is produced at;
|
|
15
|
+
* 2. `estimated` — a figure the FILE states: a copied picture's average rate,
|
|
16
|
+
* a copied soundtrack's stated rate. An average is not a bound, so a load
|
|
17
|
+
* built on it is admitted only by estimate and is never confirmed;
|
|
18
|
+
* 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
|
|
19
|
+
* soundtrack whose rate nothing states.
|
|
20
|
+
*
|
|
21
|
+
* A load is as trustworthy as its least trustworthy part.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { maxrateKbpsFor, nominalKbpsFor } from "../args.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How much of a measured link may be spent on the stream.
|
|
28
|
+
*
|
|
29
|
+
* The rest is what the link does when it is not being perfect: transport
|
|
30
|
+
* framing, retransmission, the other tabs, the moment somebody else in the
|
|
31
|
+
* house starts something. A load sized to the whole reading stalls on the
|
|
32
|
+
* first of those.
|
|
33
|
+
*
|
|
34
|
+
* Chosen, not measured, and written as a constant rather than dressed up as a
|
|
35
|
+
* measurement. What would replace it is a reading of the transport's own
|
|
36
|
+
* overhead and of how far a link's throughput varies over a session, which
|
|
37
|
+
* nothing takes.
|
|
38
|
+
*/
|
|
39
|
+
export const LINK_SAFETY = 0.8;
|
|
40
|
+
|
|
41
|
+
/** How far a figure in a load can be trusted. */
|
|
42
|
+
export const PEAK_CLASS = Object.freeze({
|
|
43
|
+
KNOWN: "known",
|
|
44
|
+
ESTIMATED: "estimated",
|
|
45
|
+
UNKNOWN: "unknown"
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** What a link was found to do with a load. */
|
|
49
|
+
export const LINK_VERDICT = Object.freeze({
|
|
50
|
+
FITS: "fits",
|
|
51
|
+
ESTIMATED_TO_FIT: "estimated to fit",
|
|
52
|
+
DOES_NOT_FIT: "does not fit",
|
|
53
|
+
NO_SAFE_BOUND: "no safe bound",
|
|
54
|
+
NO_MEASUREMENT: "no measurement"
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{ mbps: number | null, peakClass: string }} LoadPart
|
|
61
|
+
* @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {number | null} mbps
|
|
66
|
+
* @param {string} peakClass
|
|
67
|
+
* @returns {LoadPart}
|
|
68
|
+
*/
|
|
69
|
+
function part(mbps, peakClass) {
|
|
70
|
+
const figure = Number.isFinite(mbps) && mbps > 0 ? mbps : null;
|
|
71
|
+
// A figure that is missing is no figure, whatever the part claimed to be.
|
|
72
|
+
return { mbps: figure, peakClass: figure === null ? PEAK_CLASS.UNKNOWN : peakClass };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The picture part of a load, read from an output's identity.
|
|
77
|
+
*
|
|
78
|
+
* An encoder given no limit states no bound, and its part is `unknown` — unless
|
|
79
|
+
* this host has seen outputs of exactly this mode carry a peak (roadmap item
|
|
80
|
+
* 97, step 14), and then that peak is its figure, as an ESTIMATE: it is what
|
|
81
|
+
* was seen, not a bound anything holds the encoder to.
|
|
82
|
+
*
|
|
83
|
+
* @param {import("../output/OutputSpec.js").OutputSpec} spec
|
|
84
|
+
* @param {number | null} sourceMbps - The average rate the source file states.
|
|
85
|
+
* @param {number | null} [observedPeakMbps] - The largest peak outputs of this
|
|
86
|
+
* mode were seen carrying on this configuration, or null.
|
|
87
|
+
* @returns {LoadPart | null} Null for an output that carries no picture.
|
|
88
|
+
*/
|
|
89
|
+
export function videoLoadOfSpec(spec, sourceMbps, observedPeakMbps = null) {
|
|
90
|
+
if (!spec?.video) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const encode = spec.video.encode;
|
|
94
|
+
if (!encode) {
|
|
95
|
+
return part(sourceMbps, PEAK_CLASS.ESTIMATED);
|
|
96
|
+
}
|
|
97
|
+
const rateControl = encode.rateControl;
|
|
98
|
+
if (rateControl) {
|
|
99
|
+
return part(rateControl.maxrateKbps / 1000, PEAK_CLASS.KNOWN);
|
|
100
|
+
}
|
|
101
|
+
return Number.isFinite(observedPeakMbps) && observedPeakMbps > 0
|
|
102
|
+
? part(observedPeakMbps, PEAK_CLASS.ESTIMATED)
|
|
103
|
+
: part(null, PEAK_CLASS.UNKNOWN);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The picture part of a load for a height nothing has produced yet, the way
|
|
108
|
+
* it WOULD be produced here: at the nominal limit on the software encoder,
|
|
109
|
+
* with no limit on a hardware one, and as the source's own stream when the
|
|
110
|
+
* picture is copied at the source's height.
|
|
111
|
+
*
|
|
112
|
+
* The nominal limit is the one of the FRAME the height would be encoded at,
|
|
113
|
+
* chosen by its area (`nominalKbpsFor`) — the same row the output is then
|
|
114
|
+
* opened in, so the offer and the output cannot price one step differently.
|
|
115
|
+
*
|
|
116
|
+
* @param {{ sourceHeight: number, copiesAtSource: boolean, sourceMbps: number | null, encoderKind: string }} picture
|
|
117
|
+
* @param {{ width: number, height: number }} frame - The frame this height is
|
|
118
|
+
* encoded at for this source.
|
|
119
|
+
* @returns {LoadPart}
|
|
120
|
+
*/
|
|
121
|
+
export function videoLoadForFrame(picture, frame) {
|
|
122
|
+
const sourceHeight = Math.round(Number(picture?.sourceHeight) || 0);
|
|
123
|
+
if (picture?.copiesAtSource === true && frame.height === sourceHeight) {
|
|
124
|
+
return part(picture?.sourceMbps ?? null, PEAK_CLASS.ESTIMATED);
|
|
125
|
+
}
|
|
126
|
+
if (picture?.encoderKind !== "software") {
|
|
127
|
+
return part(null, PEAK_CLASS.UNKNOWN);
|
|
128
|
+
}
|
|
129
|
+
return part(maxrateKbpsFor(nominalKbpsFor(frame)) / 1000, PEAK_CLASS.KNOWN);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The picture part of a load for a software encode at a nominal limit nothing
|
|
134
|
+
* has produced yet. The bound is the one `softwareRateControlFor` would give
|
|
135
|
+
* it, so an output opened at that limit carries exactly this load.
|
|
136
|
+
*
|
|
137
|
+
* @param {number} nominalKbps
|
|
138
|
+
* @returns {LoadPart}
|
|
139
|
+
*/
|
|
140
|
+
export function videoLoadOfLimit(nominalKbps) {
|
|
141
|
+
return part(maxrateKbpsFor(nominalKbps) / 1000, PEAK_CLASS.KNOWN);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The soundtrack part of a load: the track THIS viewer chose.
|
|
146
|
+
*
|
|
147
|
+
* @param {{ transcode: boolean, bitrateKbps: number | null } | null} track -
|
|
148
|
+
* Null when the viewer receives no sound from this output or beside it.
|
|
149
|
+
* @param {number} transcodeKbps - What a re-encoded track is produced at.
|
|
150
|
+
* @returns {LoadPart | null}
|
|
151
|
+
*/
|
|
152
|
+
export function audioLoadOf(track, transcodeKbps) {
|
|
153
|
+
if (!track) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (track.transcode === true) {
|
|
157
|
+
return part(transcodeKbps / 1000, PEAK_CLASS.KNOWN);
|
|
158
|
+
}
|
|
159
|
+
return part((Number(track.bitrateKbps) || 0) / 1000, PEAK_CLASS.ESTIMATED);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The whole load: the parts added, and the class of the least trustworthy.
|
|
164
|
+
*
|
|
165
|
+
* @param {LoadPart | null} video
|
|
166
|
+
* @param {LoadPart | null} audio
|
|
167
|
+
* @returns {Load}
|
|
168
|
+
*/
|
|
169
|
+
export function loadOf(video, audio) {
|
|
170
|
+
const parts = [video, audio].filter(Boolean);
|
|
171
|
+
const peakClass = parts.reduce(
|
|
172
|
+
(worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
|
|
173
|
+
PEAK_CLASS.KNOWN
|
|
174
|
+
);
|
|
175
|
+
const totalMbps = peakClass === PEAK_CLASS.UNKNOWN
|
|
176
|
+
? null
|
|
177
|
+
: parts.reduce((sum, one) => sum + one.mbps, 0);
|
|
178
|
+
return { video, audio, totalMbps, peakClass };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* What a link that measured `linkMbps` does with a load.
|
|
183
|
+
*
|
|
184
|
+
* 1. nothing measured the link — every class is admitted, since the link gave
|
|
185
|
+
* no ground to refuse, and nothing is confirmed;
|
|
186
|
+
* 2. an `unknown` load against a measured link — `no safe bound`, refused:
|
|
187
|
+
* there is no number to compare;
|
|
188
|
+
* 3. otherwise the load is compared with the usable share of the link: a
|
|
189
|
+
* `known` load that passes FITS and is confirmed, an `estimated` one that
|
|
190
|
+
* passes is admitted by estimate and is NOT confirmed.
|
|
191
|
+
*
|
|
192
|
+
* @param {number | null} linkMbps
|
|
193
|
+
* @param {Load} load
|
|
194
|
+
* @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
|
|
195
|
+
*/
|
|
196
|
+
export function linkCouldCarry(linkMbps, load) {
|
|
197
|
+
const measured = Number.isFinite(linkMbps) && linkMbps > 0;
|
|
198
|
+
const answer = (verdict, admitted, confirmed) => ({
|
|
199
|
+
verdict,
|
|
200
|
+
admitted,
|
|
201
|
+
confirmed,
|
|
202
|
+
linkMbps: measured ? linkMbps : null,
|
|
203
|
+
load
|
|
204
|
+
});
|
|
205
|
+
if (!measured) {
|
|
206
|
+
return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
|
|
207
|
+
}
|
|
208
|
+
if (load.peakClass === PEAK_CLASS.UNKNOWN) {
|
|
209
|
+
return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
|
|
210
|
+
}
|
|
211
|
+
if (linkMbps * LINK_SAFETY < load.totalMbps) {
|
|
212
|
+
return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
|
|
213
|
+
}
|
|
214
|
+
return load.peakClass === PEAK_CLASS.KNOWN
|
|
215
|
+
? answer(LINK_VERDICT.FITS, true, true)
|
|
216
|
+
: answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* How strongly an answer is admitted, for ordering: a load whose figure is a
|
|
221
|
+
* bound before one whose figure is an average, and both before one with no
|
|
222
|
+
* figure (which is only ever admitted with nothing measured). Zero for a
|
|
223
|
+
* refusal.
|
|
224
|
+
*
|
|
225
|
+
* @param {{ admitted: boolean, load: Load }} answer
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
export function admissionRank(answer) {
|
|
229
|
+
if (!answer.admitted) {
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The figures behind an answer, as plain values a log line or a report can
|
|
237
|
+
* carry.
|
|
238
|
+
*
|
|
239
|
+
* @param {{ verdict: string, linkMbps: number | null, load: Load }} answer
|
|
240
|
+
* @returns {object}
|
|
241
|
+
*/
|
|
242
|
+
export function linkAnswerFigures(answer) {
|
|
243
|
+
return {
|
|
244
|
+
verdict: answer.verdict,
|
|
245
|
+
linkMbps: answer.linkMbps,
|
|
246
|
+
linkSafety: LINK_SAFETY,
|
|
247
|
+
videoMbps: answer.load.video?.mbps ?? null,
|
|
248
|
+
videoClass: answer.load.video?.peakClass ?? null,
|
|
249
|
+
audioMbps: answer.load.audio?.mbps ?? null,
|
|
250
|
+
audioClass: answer.load.audio?.peakClass ?? null,
|
|
251
|
+
totalMbps: answer.load.totalMbps,
|
|
252
|
+
peakClass: answer.load.peakClass
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The format an output produces, decided before it is named — because
|
|
3
|
+
* the name IS the format (decided with the user 2026-09-16).
|
|
4
|
+
*
|
|
5
|
+
* A copy is the source's own picture. A re-encode is which encoder, the size,
|
|
6
|
+
* the frame rate, the speed setting, whether HDR is tone mapped, and the rate
|
|
7
|
+
* control:
|
|
8
|
+
*
|
|
9
|
+
* 1. a size produced exactly — every rung of a master — is the box asked for,
|
|
10
|
+
* fitted to the source, and only its speed setting is chosen;
|
|
11
|
+
* 2. a size left to the budget is the highest rung of the viewer's own ladder
|
|
12
|
+
* this machine holds, with the speed setting for that rung;
|
|
13
|
+
* 3. on a hardware encoder, or with no startup measurement, there is no ladder
|
|
14
|
+
* to choose from and the box asked for is produced.
|
|
15
|
+
*
|
|
16
|
+
* The rate control is part of the format: a software encode is bounded at a
|
|
17
|
+
* limit from `limitsFor(frame)` — the nominal one until a set of lower limits
|
|
18
|
+
* is decided — or at the limit asked for (`capKbps`), and declared at the
|
|
19
|
+
* level of that size's nominal output. A hardware encoder is given no bound.
|
|
20
|
+
*
|
|
21
|
+
* AND THE VIEWER'S LINK DECIDES WHETHER IT MAY BE GIVEN AT ALL (roadmap item
|
|
22
|
+
* 97, step 11). Every format considered is judged by what this viewer's link
|
|
23
|
+
* does with its whole load — the picture and the soundtrack THEY chose
|
|
24
|
+
* (`link-budget.js`). In order:
|
|
25
|
+
*
|
|
26
|
+
* 1. an output already here that suits them (`serving-output.js`);
|
|
27
|
+
* 2. the size wanted, at the highest limit their link admits;
|
|
28
|
+
* 3. in AUTO only, a smaller rung of their ladder at the highest limit their
|
|
29
|
+
* link admits — a smaller size proves nothing by itself;
|
|
30
|
+
* 4. otherwise NOTHING: the answer is `unavailable`, with the figures, and no
|
|
31
|
+
* output is named. A format known not to fit is never handed over.
|
|
32
|
+
*
|
|
33
|
+
* Nothing here reads the machine or the disk: what it needs arrives as values
|
|
34
|
+
* and as functions over them.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { computeOutputDimensions, nominalKbpsFor, softwareRateControlFor } from "../args.js";
|
|
38
|
+
import { buildResolutionLadder, canSustainOutput, pickSoftwarePreset } from "../hwaccel.js";
|
|
39
|
+
import { OutputSpec } from "../output/OutputSpec.js";
|
|
40
|
+
import { chooseServingOutput, nextRungAreaBelow, servingCandidates } from "./serving-output.js";
|
|
41
|
+
import { linkAnswerFigures, linkCouldCarry, loadOf, videoLoadOfSpec } from "./link-budget.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} params
|
|
45
|
+
* @param {boolean} params.encodesPicture - The output carries a picture and re-encodes it.
|
|
46
|
+
* @param {boolean} params.exact - The size is produced exactly as asked.
|
|
47
|
+
* @param {{ width: number, height: number }} params.target - The box asked for; zeroes mean the source's.
|
|
48
|
+
* @param {{ width: number, height: number, megabitsPerSecond: number | null, decode: object | null }} params.source
|
|
49
|
+
* @param {number} params.fps
|
|
50
|
+
* @param {{ kind: string, name: string }} params.encoder
|
|
51
|
+
* @param {unknown} params.benchmark - The startup measurement of the software encoder, or null.
|
|
52
|
+
* @param {{ decodeModel: object | null, observedDecodeCostSec: number | null, requiredSpeed: number | null }} params.cost
|
|
53
|
+
* @param {(params: object) => object | null} params.chooseBudget - The realtime budget's ladder choice.
|
|
54
|
+
* @param {boolean} params.tonemap
|
|
55
|
+
* @param {number | null} [params.capKbps] - A nominal limit asked for outright;
|
|
56
|
+
* above the size's own it is refused (`softwareRateControlFor`).
|
|
57
|
+
* @param {(frame: { width: number, height: number }) => number[]} [params.limitsFor] -
|
|
58
|
+
* The nominal limits a frame may be produced at, highest first; the row is
|
|
59
|
+
* chosen by the frame's area (`limitRowFor`).
|
|
60
|
+
* @param {import("./link-budget.js").LoadPart | null} [params.audioLoad] - The
|
|
61
|
+
* soundtrack this viewer receives with the picture, from it or beside it.
|
|
62
|
+
* @param {(encode: object | null) => OutputSpec} params.specWith - The output
|
|
63
|
+
* with this picture format and every other part as the request made it.
|
|
64
|
+
* @param {{ mode: "auto" | "manual", linkMbps: number | null, keys: Iterable<string>, readyAt: (key: string) => boolean, observedPeakMbps?: (spec: OutputSpec) => number | null }} params.serving
|
|
65
|
+
* @returns {{
|
|
66
|
+
* spec: OutputSpec | null,
|
|
67
|
+
* budget: object | null,
|
|
68
|
+
* wantedKey: string,
|
|
69
|
+
* reusedKey: string | null,
|
|
70
|
+
* answer: object | null,
|
|
71
|
+
* unavailable: { reason: string, figures: object | null } | null
|
|
72
|
+
* }}
|
|
73
|
+
*/
|
|
74
|
+
export function decideOutputFormat({
|
|
75
|
+
encodesPicture,
|
|
76
|
+
exact,
|
|
77
|
+
target,
|
|
78
|
+
source,
|
|
79
|
+
fps,
|
|
80
|
+
encoder,
|
|
81
|
+
benchmark,
|
|
82
|
+
cost,
|
|
83
|
+
chooseBudget,
|
|
84
|
+
tonemap,
|
|
85
|
+
capKbps = null,
|
|
86
|
+
limitsFor = (frame) => [nominalKbpsFor(frame)],
|
|
87
|
+
audioLoad = null,
|
|
88
|
+
specWith,
|
|
89
|
+
serving
|
|
90
|
+
}) {
|
|
91
|
+
const judge = (spec) => linkCouldCarry(
|
|
92
|
+
serving.linkMbps,
|
|
93
|
+
loadOf(videoLoadOfSpec(spec, source.megabitsPerSecond, serving.observedPeakMbps?.(spec) ?? null), audioLoad)
|
|
94
|
+
);
|
|
95
|
+
const given = (spec, answer, extra = {}) => ({
|
|
96
|
+
spec,
|
|
97
|
+
budget: null,
|
|
98
|
+
wantedKey: spec.toKey(),
|
|
99
|
+
reusedKey: null,
|
|
100
|
+
answer,
|
|
101
|
+
unavailable: null,
|
|
102
|
+
...extra
|
|
103
|
+
});
|
|
104
|
+
const refused = (spec, answer, reason, extra = {}) => ({
|
|
105
|
+
spec: null,
|
|
106
|
+
budget: null,
|
|
107
|
+
wantedKey: spec.toKey(),
|
|
108
|
+
reusedKey: null,
|
|
109
|
+
answer,
|
|
110
|
+
unavailable: { reason, figures: answer ? linkAnswerFigures(answer) : null, bound: "link" },
|
|
111
|
+
...extra
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!encodesPicture) {
|
|
115
|
+
const spec = specWith(null);
|
|
116
|
+
if (!spec.video) {
|
|
117
|
+
// A soundtrack on its own: the viewer's load was judged with the picture
|
|
118
|
+
// it plays beside, which counted this track.
|
|
119
|
+
return given(spec, null);
|
|
120
|
+
}
|
|
121
|
+
const answer = judge(spec);
|
|
122
|
+
return answer.admitted
|
|
123
|
+
? given(spec, answer)
|
|
124
|
+
: refused(spec, answer, "the source's own picture is more than this viewer's link carries");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ceiling = computeOutputDimensions(target.width, target.height, source.width, source.height);
|
|
128
|
+
const budget = exact
|
|
129
|
+
? null
|
|
130
|
+
: chooseBudget({
|
|
131
|
+
transcodeVideo: true,
|
|
132
|
+
targetWidth: target.width,
|
|
133
|
+
targetHeight: target.height,
|
|
134
|
+
sourceWidth: source.width,
|
|
135
|
+
sourceHeight: source.height,
|
|
136
|
+
outputFps: fps,
|
|
137
|
+
source: source.decode,
|
|
138
|
+
requiredSpeed: cost.requiredSpeed
|
|
139
|
+
});
|
|
140
|
+
const width = budget?.width ?? ceiling?.w ?? 0;
|
|
141
|
+
const height = budget?.height ?? ceiling?.h ?? 0;
|
|
142
|
+
const priced = {
|
|
143
|
+
decodeModel: cost.decodeModel,
|
|
144
|
+
source: source.decode,
|
|
145
|
+
observedDecodeCostSec: cost.observedDecodeCostSec,
|
|
146
|
+
requiredSpeed: cost.requiredSpeed
|
|
147
|
+
};
|
|
148
|
+
const software = encoder.kind === "software";
|
|
149
|
+
const measured = software && Boolean(benchmark);
|
|
150
|
+
// WHETHER THIS MACHINE HAS SHOWN IT CAN ENCODE THIS SIZE (roadmap item 97,
|
|
151
|
+
// step 14): a mode of this encoder qualified at startup, measured at this
|
|
152
|
+
// frame, and fast enough on its own for this source. A size that fails is not
|
|
153
|
+
// a format anybody is given here — not the one asked for, and not a lower
|
|
154
|
+
// one in its place. Whether the machine has room for it BESIDE what already
|
|
155
|
+
// runs is the admission's, asked when the output is opened.
|
|
156
|
+
const machineCan = (w, h) => {
|
|
157
|
+
if (!(w > 0 && h > 0)) {
|
|
158
|
+
return true; // no size to judge: the source's own, which the probe could not read
|
|
159
|
+
}
|
|
160
|
+
if (!Array.isArray(benchmark)) {
|
|
161
|
+
return true; // not calibrated: a wiring made without the startup calibration
|
|
162
|
+
}
|
|
163
|
+
if (benchmark.length === 0) {
|
|
164
|
+
return false; // calibrated, and no mode qualified
|
|
165
|
+
}
|
|
166
|
+
return canSustainOutput({ benchmark, ...priced, outputPixelsPerSec: w * h * fps, frame: { width: w, height: h } }).sustainable;
|
|
167
|
+
};
|
|
168
|
+
const presetFor = (w, h) => (budget && w === width && h === height
|
|
169
|
+
? budget.preset
|
|
170
|
+
: measured && w > 0
|
|
171
|
+
? pickSoftwarePreset(benchmark, w * h * fps, priced, { width: w, height: h })
|
|
172
|
+
: null);
|
|
173
|
+
// Every format at one size this viewer could be given, highest limit first.
|
|
174
|
+
// A hardware encoder is given no limit, so it has exactly one.
|
|
175
|
+
const formatsAt = (w, h) => {
|
|
176
|
+
const limits = !software || !(w > 0 && h > 0)
|
|
177
|
+
? [null]
|
|
178
|
+
: capKbps !== null && capKbps !== undefined && w === width && h === height
|
|
179
|
+
? [capKbps]
|
|
180
|
+
: limitsFor({ width: w, height: h });
|
|
181
|
+
return limits.map((limit) => specWith({
|
|
182
|
+
encoder: encoder.name,
|
|
183
|
+
width: w,
|
|
184
|
+
height: h,
|
|
185
|
+
fps,
|
|
186
|
+
preset: presetFor(w, h),
|
|
187
|
+
tonemap,
|
|
188
|
+
rateControl: limit === null ? null : softwareRateControlFor({ width: w, height: h, fps, capKbps: limit })
|
|
189
|
+
}));
|
|
190
|
+
};
|
|
191
|
+
const wantedFormats = formatsAt(width, height);
|
|
192
|
+
const wantedSpec = wantedFormats[0];
|
|
193
|
+
const wantedKey = wantedSpec.toKey();
|
|
194
|
+
|
|
195
|
+
// A LIMIT NAMED IN THE REQUEST IS WHAT WAS ASKED FOR, and no other output
|
|
196
|
+
// stands in for it. It is named when a viewer is being moved to another
|
|
197
|
+
// limit of the height they watch (roadmap item 97, step 12), and the output
|
|
198
|
+
// they are being moved OFF always suits them by the rule below — so letting
|
|
199
|
+
// the rule answer would hand them back the very output the move is for
|
|
200
|
+
// leaving. An output with exactly this limit is still reused: it has the same
|
|
201
|
+
// key, and the key is what `OutputOpening` looks up.
|
|
202
|
+
const limitNamed = capKbps !== null && capKbps !== undefined;
|
|
203
|
+
if (ceiling && !limitNamed) {
|
|
204
|
+
const served = chooseServingOutput({
|
|
205
|
+
mode: serving.mode,
|
|
206
|
+
wanted: { width, height },
|
|
207
|
+
nextLowerArea: nextRungAreaBelow(buildResolutionLadder(ceiling.w, ceiling.h), height),
|
|
208
|
+
atRisk: measured && !canSustainOutput({ benchmark, ...priced, outputPixelsPerSec: width * height * fps, frame: { width, height } }).sustainable,
|
|
209
|
+
judge: (candidate) => judge(candidate.spec),
|
|
210
|
+
candidates: servingCandidates({
|
|
211
|
+
wanted: wantedSpec,
|
|
212
|
+
keys: serving.keys,
|
|
213
|
+
source,
|
|
214
|
+
encoderName: encoder.name,
|
|
215
|
+
readyAt: serving.readyAt
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
if (served && served.key !== wantedKey) {
|
|
219
|
+
return given(OutputSpec.fromKey(served.key), served.answer, { budget, wantedKey, reusedKey: served.key });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let anySizeHeld = machineCan(width, height);
|
|
224
|
+
if (anySizeHeld) {
|
|
225
|
+
for (const spec of wantedFormats) {
|
|
226
|
+
const answer = judge(spec);
|
|
227
|
+
if (answer.admitted) {
|
|
228
|
+
return given(spec, answer, { budget, wantedKey });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (serving.mode !== "manual" && ceiling) {
|
|
233
|
+
for (const rung of buildResolutionLadder(ceiling.w, ceiling.h).filter((one) => one.height < height)) {
|
|
234
|
+
if (!machineCan(rung.width, rung.height)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
anySizeHeld = true;
|
|
238
|
+
for (const spec of formatsAt(rung.width, rung.height)) {
|
|
239
|
+
const answer = judge(spec);
|
|
240
|
+
if (answer.admitted) {
|
|
241
|
+
return given(spec, answer, { wantedKey });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!anySizeHeld) {
|
|
247
|
+
// Not the viewer's link: this machine. Said apart, because the answer to
|
|
248
|
+
// it is another proxy, found before anything plays.
|
|
249
|
+
return {
|
|
250
|
+
...refused(wantedSpec, null, `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`, { budget, wantedKey }),
|
|
251
|
+
unavailable: {
|
|
252
|
+
reason: `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
|
|
253
|
+
figures: { width, height, fps, encoder: encoder.name },
|
|
254
|
+
bound: "machine"
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return refused(
|
|
259
|
+
wantedSpec,
|
|
260
|
+
judge(wantedSpec),
|
|
261
|
+
serving.mode === "manual"
|
|
262
|
+
? "no limit of the size picked is admitted by this viewer's link"
|
|
263
|
+
: "no size and limit this viewer's ladder offers is admitted by their link",
|
|
264
|
+
{ budget, wantedKey }
|
|
265
|
+
);
|
|
266
|
+
}
|