@torrent-tv/proxy 2.87.7 → 2.87.8
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/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +4 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/data-channel-handler.js +21 -8
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -0
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,266 +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
|
-
}
|
|
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
|
+
}
|