@torrent-tv/proxy 2.87.21 → 2.88.1
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 +2242 -2234
- 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/browser-proxy-contract.md +1 -1
- 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 +50 -50
- 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 +106 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- 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 +138 -138
- package/routes/api/transcode-sessions/post.js +173 -173
- 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 +380 -380
- 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/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- 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 +758 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -2184
- 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 +1141 -1141
- 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 +315 -315
- 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 +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +515 -515
- 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/audio-inventory.js +245 -245
- 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 +339 -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/playback-planner.js +931 -931
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +696 -696
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +992 -992
- 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 +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +382 -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/episode-naming.js +182 -0
- 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/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- 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/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- 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 +691 -691
- 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-inventory.test.js +212 -212
- 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 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- 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 +162 -162
- 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/episode-naming.test.js +131 -0
- 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-presentation-clock.test.js +147 -147
- 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 +401 -401
- 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 +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- 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/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- 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 +12 -2
- 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 +209 -209
- 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-metadata.test.js +15 -0
- 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,376 +1,376 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Whether this machine can take one more encoder, counted across every
|
|
3
|
-
* output at once (roadmap item 97, step 13).
|
|
4
|
-
*
|
|
5
|
-
* WHAT WAS MISSING. How many encoders an output may run was answered per
|
|
6
|
-
* output (`EncodeRuns.maxRunsForOutput`) and always at least one, so a second
|
|
7
|
-
* output got its encoder whatever the first was already costing — and on the
|
|
8
|
-
* addon host two 1080p encodes run at 0.99x and 0.98x, so opening one more
|
|
9
|
-
* output slowed everybody below realtime. Nothing could refuse.
|
|
10
|
-
*
|
|
11
|
-
* THE UNIT is seconds of work per second of film, the one the quality offer
|
|
12
|
-
* judges every step in (`EncodeCost`). An encoder costs what it costs: a
|
|
13
|
-
* soundtrack is a small fraction of a re-encoded picture, and counting them as
|
|
14
|
-
* processes would refuse a viewer their sound for the sake of a number. Costs
|
|
15
|
-
* add, and a set of encoders is affordable when the machine, corrected for the
|
|
16
|
-
* share of it nobody has priced, still makes a second of film per second:
|
|
17
|
-
* `share / Σ cost >= 1`. On the addon host that reproduces the measurement it
|
|
18
|
-
* is meant to hold: 1080p alone at 1.96x costs 0.51 s/s, two cost 1.02 and make
|
|
19
|
-
* 0.98x, measured 0.99x and 0.98x. At 480p it is pessimistic — two at 7.12x
|
|
20
|
-
* each predict 3.56x and were measured at 4.20x — and pessimistic is the side
|
|
21
|
-
* a refusal may err on.
|
|
22
|
-
*
|
|
23
|
-
* WHO HOLDS A PLACE, and nothing is stored here to say so:
|
|
24
|
-
*
|
|
25
|
-
* 1. every live encoder, at the cost of its output;
|
|
26
|
-
* 2. every output a present viewer is being PREPARED onto — a step being
|
|
27
|
-
* warmed, or a move to another limit of the height on screen — once per
|
|
28
|
-
* output however many viewers wait for it, and only while no encoder is
|
|
29
|
-
* running there yet, since a running one already holds its place;
|
|
30
|
-
* 3. every output a present viewer is WATCHING that still has something left
|
|
31
|
-
* to make and no encoder on it yet — the output just opened for them,
|
|
32
|
-
* which the plan has not placed an encoder on in the same moment. Without
|
|
33
|
-
* it the place an output is opened on is nobody's until its first encoder
|
|
34
|
-
* starts, and two viewers opening two films in that gap are both told there
|
|
35
|
-
* is room for one (roadmap item 97, step 14).
|
|
36
|
-
*
|
|
37
|
-
* The second is read off the viewers' own records. A preparation is recorded
|
|
38
|
-
* where it belongs, on the viewer, and the place it holds ends the moment that
|
|
39
|
-
* record does: no second register to keep in step, and no release to forget
|
|
40
|
-
* on one of the paths a preparation can end by.
|
|
41
|
-
*
|
|
42
|
-
* AN ADMITTED ENCODER IS NEVER TAKEN AWAY. A refusal only ever stops something
|
|
43
|
-
* from starting; what runs keeps running. So "the admission removed an encoder
|
|
44
|
-
* from an output being prepared" does not happen by construction.
|
|
45
|
-
*
|
|
46
|
-
* WHAT IS NOT SOLVED, stated so it is not discovered: places go to whoever
|
|
47
|
-
* asks first. A second encoder on one output — one catching up behind another
|
|
48
|
-
* — takes a place a viewer opening another film may be waiting for, and the
|
|
49
|
-
* viewer waits until it ends.
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
import { correctForAvailability } from "./available-share.js";
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @typedef {object} OutputLoad
|
|
56
|
-
* @property {number | null} costSec - One encoder on it, in seconds of work
|
|
57
|
-
* per second of film. Null when nothing has priced it.
|
|
58
|
-
* @property {string} fileKey - The file it is made from.
|
|
59
|
-
* @property {number} fileCostSec - What that file costs by being fetched and
|
|
60
|
-
* delivered, once however many of its outputs are encoding.
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @typedef {object} AdmissionHost
|
|
65
|
-
* @property {() => Map<string, number>} liveRunsByAddress - How many encoders
|
|
66
|
-
* are alive on each output.
|
|
67
|
-
* @property {() => Set<string>} preparedAddresses - Every output a present
|
|
68
|
-
* viewer is being prepared onto.
|
|
69
|
-
* @property {() => Set<string>} [watchedAddresses] - Every output a present
|
|
70
|
-
* viewer is watching that still has something left to make.
|
|
71
|
-
* @property {(address: string) => OutputLoad | null} loadOf
|
|
72
|
-
* @property {(spec: object, file: object) => OutputLoad | null} [loadForCandidate]
|
|
73
|
-
* @property {() => ({ share: number, known: boolean } | null)} availability
|
|
74
|
-
*/
|
|
75
|
-
|
|
76
|
-
export class EncodeAdmission {
|
|
77
|
-
/** @type {AdmissionHost} */
|
|
78
|
-
#host;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* @param {AdmissionHost} host
|
|
82
|
-
*/
|
|
83
|
-
constructor(host) {
|
|
84
|
-
for (const name of ["liveRunsByAddress", "preparedAddresses", "loadOf", "availability"]) {
|
|
85
|
-
if (typeof host?.[name] !== "function") {
|
|
86
|
-
throw new TypeError(`EncodeAdmission requires ${name}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
this.#host = host;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Seconds of film per second this machine makes of each output while all of
|
|
94
|
-
* this is running, corrected for what nobody has priced.
|
|
95
|
-
*
|
|
96
|
-
* @param {number} costSec
|
|
97
|
-
* @returns {number}
|
|
98
|
-
*/
|
|
99
|
-
#speedAt(costSec) {
|
|
100
|
-
if (!(costSec > 0)) {
|
|
101
|
-
return Number.POSITIVE_INFINITY;
|
|
102
|
-
}
|
|
103
|
-
return correctForAvailability(1 / costSec, this.#host.availability());
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* What everything holding a place costs, leaving one output out.
|
|
108
|
-
*
|
|
109
|
-
* @param {string | null} except
|
|
110
|
-
* @param {string | null} [exceptFileKey] - Leave out every output of this file.
|
|
111
|
-
* @returns {{ costSec: number, files: Set<string>, unpriced: string[] }}
|
|
112
|
-
*/
|
|
113
|
-
#occupied(except, exceptFileKey = null) {
|
|
114
|
-
const live = this.#host.liveRunsByAddress();
|
|
115
|
-
/** @type {Map<string, number>} */
|
|
116
|
-
const units = new Map();
|
|
117
|
-
for (const [address, count] of live) {
|
|
118
|
-
if (address !== except && count > 0) {
|
|
119
|
-
units.set(address, count);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
for (const address of this.#heldWithoutRun()) {
|
|
123
|
-
if (address !== except && !units.has(address)) {
|
|
124
|
-
units.set(address, 1);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
let costSec = 0;
|
|
128
|
-
const files = new Set();
|
|
129
|
-
const unpriced = [];
|
|
130
|
-
for (const [address, count] of units) {
|
|
131
|
-
const load = this.#host.loadOf(address);
|
|
132
|
-
if (exceptFileKey && load?.fileKey === exceptFileKey) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (!load) {
|
|
136
|
-
unpriced.push(address);
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (load.costSec === null) {
|
|
140
|
-
unpriced.push(address);
|
|
141
|
-
} else {
|
|
142
|
-
costSec += count * load.costSec;
|
|
143
|
-
}
|
|
144
|
-
if (load.fileKey && !files.has(load.fileKey)) {
|
|
145
|
-
files.add(load.fileKey);
|
|
146
|
-
costSec += load.fileCostSec;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return { costSec, files, unpriced };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* What `count` encoders on this output add to what is already held.
|
|
154
|
-
*
|
|
155
|
-
* @param {OutputLoad} load
|
|
156
|
-
* @param {Set<string>} files
|
|
157
|
-
* @param {number} count
|
|
158
|
-
* @returns {number}
|
|
159
|
-
*/
|
|
160
|
-
static #added(load, files, count) {
|
|
161
|
-
if (count <= 0) {
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
|
-
const file = load.fileKey && !files.has(load.fileKey) ? load.fileCostSec : 0;
|
|
165
|
-
return file + count * (load.costSec ?? 0);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Whether a viewer may be prepared onto this output now.
|
|
170
|
-
*
|
|
171
|
-
* Asked BEFORE the preparation is recorded, and answered in the same
|
|
172
|
-
* synchronous stretch as the record is written: two preparations onto two
|
|
173
|
-
* different outputs then cannot both be told there is room for one, because
|
|
174
|
-
* the second is asked after the first has been recorded and is counted.
|
|
175
|
-
*
|
|
176
|
-
* An output that already holds a place — an encoder running there, or
|
|
177
|
-
* another viewer being prepared onto it — is admitted without arithmetic: it
|
|
178
|
-
* needs one encoder however many wait for it.
|
|
179
|
-
*
|
|
180
|
-
* @param {string} address
|
|
181
|
-
* @returns {{ admitted: boolean, reason: string, speedX: number | null }}
|
|
182
|
-
*/
|
|
183
|
-
admitsPreparation(address) {
|
|
184
|
-
if (!address) {
|
|
185
|
-
return { admitted: false, reason: "the output has no address", speedX: null };
|
|
186
|
-
}
|
|
187
|
-
if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
|
|
188
|
-
return { admitted: true, reason: "an encoder already runs there", speedX: null };
|
|
189
|
-
}
|
|
190
|
-
if (this.#heldWithoutRun().has(address)) {
|
|
191
|
-
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
192
|
-
}
|
|
193
|
-
return this.#assessCandidate(address, this.#host.loadOf(address));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Whether a not-yet-opened format can take a place on the whole machine.
|
|
198
|
-
* The output choice calls this for each candidate rung; the final claim uses
|
|
199
|
-
* `admitsWatching` with the same arithmetic after registering the output.
|
|
200
|
-
*
|
|
201
|
-
* @param {object} spec
|
|
202
|
-
* @param {object} file
|
|
203
|
-
* @returns {{ admitted: boolean, reason: string, speedX: number | null }}
|
|
204
|
-
*/
|
|
205
|
-
previewCandidate(spec, file) {
|
|
206
|
-
const address = typeof spec?.toKey === "function" ? spec.toKey() : "";
|
|
207
|
-
if (!address) {
|
|
208
|
-
return { admitted: false, reason: "the output has no address", speedX: null };
|
|
209
|
-
}
|
|
210
|
-
if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
|
|
211
|
-
return { admitted: true, reason: "an encoder already runs there", speedX: null };
|
|
212
|
-
}
|
|
213
|
-
if (this.#heldWithoutRun().has(address)) {
|
|
214
|
-
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
215
|
-
}
|
|
216
|
-
const load = this.#host.loadForCandidate?.(spec, file) ?? null;
|
|
217
|
-
return this.#assessCandidate(address, load);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Projected production rate for outputs currently holding machine capacity.
|
|
222
|
-
* A live encoder reading takes precedence; this measured-cost projection
|
|
223
|
-
* supplies the playback trajectory before its first run sample arrives.
|
|
224
|
-
*
|
|
225
|
-
* @returns {number | null}
|
|
226
|
-
*/
|
|
227
|
-
projectedSpeedX() {
|
|
228
|
-
const occupied = this.#occupied(null);
|
|
229
|
-
if (occupied.unpriced.length > 0) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
const speedX = this.#speedAt(occupied.costSec);
|
|
233
|
-
return Number.isFinite(speedX) && speedX > 0 ? speedX : null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
#assessCandidate(address, load) {
|
|
237
|
-
if (!load || load.costSec === null) {
|
|
238
|
-
return { admitted: false, reason: "the output has no measured encoding cost", speedX: null };
|
|
239
|
-
}
|
|
240
|
-
const others = this.#occupied(address);
|
|
241
|
-
if (others.unpriced.length > 0) {
|
|
242
|
-
return {
|
|
243
|
-
admitted: false,
|
|
244
|
-
reason: `capacity is unknown while ${others.unpriced.length} occupied output(s) have no measured cost`,
|
|
245
|
-
speedX: null
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
const speedX = this.#speedAt(others.costSec + EncodeAdmission.#added(load, others.files, 1));
|
|
249
|
-
if (speedX >= 1) {
|
|
250
|
-
return { admitted: true, reason: "", speedX: Number.isFinite(speedX) ? speedX : null };
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
admitted: false,
|
|
254
|
-
reason:
|
|
255
|
-
`this machine would make ${speedX.toFixed(2)}x of every output with one more encoder ` +
|
|
256
|
-
`(${(others.costSec + EncodeAdmission.#added(load, others.files, 1)).toFixed(3)} s of work ` +
|
|
257
|
-
`per second of film)` +
|
|
258
|
-
(others.unpriced.length > 0 ? `, not counting ${others.unpriced.length} output(s) nothing has priced` : ""),
|
|
259
|
-
speedX
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Whether a viewer may be put on this output now — asked when an output is
|
|
265
|
-
* OPENED for somebody, or when somebody joins one nothing is making.
|
|
266
|
-
*
|
|
267
|
-
* The same arithmetic as a preparation, and asked the same way: before the
|
|
268
|
-
* viewer is placed, in the same synchronous stretch as the placing, so the
|
|
269
|
-
* next opening is asked after this one is counted. An output that needs no
|
|
270
|
-
* encoder — everything it will serve already made — takes no place at all.
|
|
271
|
-
*
|
|
272
|
-
* @param {string} address
|
|
273
|
-
* @returns {{ admitted: boolean, reason: string, speedX: number | null }}
|
|
274
|
-
*/
|
|
275
|
-
admitsWatching(address) {
|
|
276
|
-
if (address && this.#host.finished?.(address) === true) {
|
|
277
|
-
return { admitted: true, reason: "everything it serves is already made", speedX: null };
|
|
278
|
-
}
|
|
279
|
-
return this.admitsPreparation(address);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* What everything holding a place costs now, in seconds of work per second
|
|
284
|
-
* of film, and what that leaves: the figure a question about ANOTHER file is
|
|
285
|
-
* priced beside — the pool asking whether this host could serve a film it
|
|
286
|
-
* has only been told about.
|
|
287
|
-
*
|
|
288
|
-
* Outputs of the file being asked about are left out: a viewer of a film
|
|
289
|
-
* this host already makes joins what is made, and that costs it nothing new.
|
|
290
|
-
*
|
|
291
|
-
* @param {{ exceptFileKey?: string | null }} [options]
|
|
292
|
-
* @returns {{ costSec: number, unpriced: number }}
|
|
293
|
-
*/
|
|
294
|
-
occupiedCost({ exceptFileKey = null } = {}) {
|
|
295
|
-
const { costSec, unpriced } = this.#occupied(null, exceptFileKey);
|
|
296
|
-
return { costSec, unpriced: unpriced.length };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* How much room this machine has left for encoding, as the pool ranks it
|
|
301
|
-
* BEFORE any film is chosen: the speed every output would run at with what
|
|
302
|
-
* holds a place now, corrected for the share nobody has priced. At or below
|
|
303
|
-
* realtime the machine has no room for one more encode of anything; the
|
|
304
|
-
* pool's own proxy choice reads it to leave such a proxy out (roadmap item
|
|
305
|
-
* 97, step 14). Null where nothing holds a place — all the room there is.
|
|
306
|
-
*
|
|
307
|
-
* @returns {{ encodeSpeedX: number | null, occupiedCostSec: number }}
|
|
308
|
-
*/
|
|
309
|
-
headroom() {
|
|
310
|
-
const { costSec, unpriced } = this.#occupied(null);
|
|
311
|
-
const speed = this.#speedAt(costSec);
|
|
312
|
-
return {
|
|
313
|
-
// Unknown occupied work is not spare capacity. Zero here means that the
|
|
314
|
-
// pool must not prefer this host while another reports usable headroom.
|
|
315
|
-
encodeSpeedX: unpriced.length > 0 ? 0 : Number.isFinite(speed) ? Number(speed.toFixed(3)) : null,
|
|
316
|
-
occupiedCostSec: Number(costSec.toFixed(4))
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Every output that holds a place without an encoder running on it yet: one
|
|
322
|
-
* a present viewer is being prepared onto, and one a present viewer watches
|
|
323
|
-
* that still has something left to make.
|
|
324
|
-
*
|
|
325
|
-
* @returns {Set<string>}
|
|
326
|
-
*/
|
|
327
|
-
#heldWithoutRun() {
|
|
328
|
-
const held = new Set(this.#host.preparedAddresses());
|
|
329
|
-
for (const address of this.#host.watchedAddresses?.() ?? []) {
|
|
330
|
-
held.add(address);
|
|
331
|
-
}
|
|
332
|
-
return held;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* How many encoders this output may run, as far as the whole machine goes.
|
|
337
|
-
*
|
|
338
|
-
* Never fewer than already run — an admitted encoder is not taken away — and
|
|
339
|
-
* never fewer than one where the output holds a place for a preparation,
|
|
340
|
-
* because that place was promised when the preparation was admitted.
|
|
341
|
-
* Otherwise as many as still leave every output at realtime.
|
|
342
|
-
*
|
|
343
|
-
* @param {string} address
|
|
344
|
-
* @param {number} running - Encoders alive on it now.
|
|
345
|
-
* @returns {{ runs: number, because: string }}
|
|
346
|
-
*/
|
|
347
|
-
placesFor(address, running) {
|
|
348
|
-
const floor = Math.max(
|
|
349
|
-
running,
|
|
350
|
-
this.#heldWithoutRun().has(address) ? 1 : 0
|
|
351
|
-
);
|
|
352
|
-
const load = this.#host.loadOf(address);
|
|
353
|
-
if (!load || load.costSec === null) {
|
|
354
|
-
// Nothing to price it with, so nothing to refuse it on: the per-output
|
|
355
|
-
// limit decides, as it did before there was a machine-wide one.
|
|
356
|
-
return { runs: Number.POSITIVE_INFINITY, because: "the output is not priced" };
|
|
357
|
-
}
|
|
358
|
-
const others = this.#occupied(address);
|
|
359
|
-
let runs = 0;
|
|
360
|
-
for (;;) {
|
|
361
|
-
const next = runs + 1;
|
|
362
|
-
if (this.#speedAt(others.costSec + EncodeAdmission.#added(load, others.files, next)) < 1) {
|
|
363
|
-
break;
|
|
364
|
-
}
|
|
365
|
-
runs = next;
|
|
366
|
-
if (!(load.costSec > 0)) {
|
|
367
|
-
// Free to run: the per-output limit is the only bound.
|
|
368
|
-
return { runs: Number.POSITIVE_INFINITY, because: "it costs nothing measurable" };
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (runs >= floor) {
|
|
372
|
-
return { runs, because: "the machine" };
|
|
373
|
-
}
|
|
374
|
-
return { runs: floor, because: runs < running ? "what already runs" : "a place held for a preparation" };
|
|
375
|
-
}
|
|
376
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether this machine can take one more encoder, counted across every
|
|
3
|
+
* output at once (roadmap item 97, step 13).
|
|
4
|
+
*
|
|
5
|
+
* WHAT WAS MISSING. How many encoders an output may run was answered per
|
|
6
|
+
* output (`EncodeRuns.maxRunsForOutput`) and always at least one, so a second
|
|
7
|
+
* output got its encoder whatever the first was already costing — and on the
|
|
8
|
+
* addon host two 1080p encodes run at 0.99x and 0.98x, so opening one more
|
|
9
|
+
* output slowed everybody below realtime. Nothing could refuse.
|
|
10
|
+
*
|
|
11
|
+
* THE UNIT is seconds of work per second of film, the one the quality offer
|
|
12
|
+
* judges every step in (`EncodeCost`). An encoder costs what it costs: a
|
|
13
|
+
* soundtrack is a small fraction of a re-encoded picture, and counting them as
|
|
14
|
+
* processes would refuse a viewer their sound for the sake of a number. Costs
|
|
15
|
+
* add, and a set of encoders is affordable when the machine, corrected for the
|
|
16
|
+
* share of it nobody has priced, still makes a second of film per second:
|
|
17
|
+
* `share / Σ cost >= 1`. On the addon host that reproduces the measurement it
|
|
18
|
+
* is meant to hold: 1080p alone at 1.96x costs 0.51 s/s, two cost 1.02 and make
|
|
19
|
+
* 0.98x, measured 0.99x and 0.98x. At 480p it is pessimistic — two at 7.12x
|
|
20
|
+
* each predict 3.56x and were measured at 4.20x — and pessimistic is the side
|
|
21
|
+
* a refusal may err on.
|
|
22
|
+
*
|
|
23
|
+
* WHO HOLDS A PLACE, and nothing is stored here to say so:
|
|
24
|
+
*
|
|
25
|
+
* 1. every live encoder, at the cost of its output;
|
|
26
|
+
* 2. every output a present viewer is being PREPARED onto — a step being
|
|
27
|
+
* warmed, or a move to another limit of the height on screen — once per
|
|
28
|
+
* output however many viewers wait for it, and only while no encoder is
|
|
29
|
+
* running there yet, since a running one already holds its place;
|
|
30
|
+
* 3. every output a present viewer is WATCHING that still has something left
|
|
31
|
+
* to make and no encoder on it yet — the output just opened for them,
|
|
32
|
+
* which the plan has not placed an encoder on in the same moment. Without
|
|
33
|
+
* it the place an output is opened on is nobody's until its first encoder
|
|
34
|
+
* starts, and two viewers opening two films in that gap are both told there
|
|
35
|
+
* is room for one (roadmap item 97, step 14).
|
|
36
|
+
*
|
|
37
|
+
* The second is read off the viewers' own records. A preparation is recorded
|
|
38
|
+
* where it belongs, on the viewer, and the place it holds ends the moment that
|
|
39
|
+
* record does: no second register to keep in step, and no release to forget
|
|
40
|
+
* on one of the paths a preparation can end by.
|
|
41
|
+
*
|
|
42
|
+
* AN ADMITTED ENCODER IS NEVER TAKEN AWAY. A refusal only ever stops something
|
|
43
|
+
* from starting; what runs keeps running. So "the admission removed an encoder
|
|
44
|
+
* from an output being prepared" does not happen by construction.
|
|
45
|
+
*
|
|
46
|
+
* WHAT IS NOT SOLVED, stated so it is not discovered: places go to whoever
|
|
47
|
+
* asks first. A second encoder on one output — one catching up behind another
|
|
48
|
+
* — takes a place a viewer opening another film may be waiting for, and the
|
|
49
|
+
* viewer waits until it ends.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { correctForAvailability } from "./available-share.js";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} OutputLoad
|
|
56
|
+
* @property {number | null} costSec - One encoder on it, in seconds of work
|
|
57
|
+
* per second of film. Null when nothing has priced it.
|
|
58
|
+
* @property {string} fileKey - The file it is made from.
|
|
59
|
+
* @property {number} fileCostSec - What that file costs by being fetched and
|
|
60
|
+
* delivered, once however many of its outputs are encoding.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {object} AdmissionHost
|
|
65
|
+
* @property {() => Map<string, number>} liveRunsByAddress - How many encoders
|
|
66
|
+
* are alive on each output.
|
|
67
|
+
* @property {() => Set<string>} preparedAddresses - Every output a present
|
|
68
|
+
* viewer is being prepared onto.
|
|
69
|
+
* @property {() => Set<string>} [watchedAddresses] - Every output a present
|
|
70
|
+
* viewer is watching that still has something left to make.
|
|
71
|
+
* @property {(address: string) => OutputLoad | null} loadOf
|
|
72
|
+
* @property {(spec: object, file: object) => OutputLoad | null} [loadForCandidate]
|
|
73
|
+
* @property {() => ({ share: number, known: boolean } | null)} availability
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
export class EncodeAdmission {
|
|
77
|
+
/** @type {AdmissionHost} */
|
|
78
|
+
#host;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {AdmissionHost} host
|
|
82
|
+
*/
|
|
83
|
+
constructor(host) {
|
|
84
|
+
for (const name of ["liveRunsByAddress", "preparedAddresses", "loadOf", "availability"]) {
|
|
85
|
+
if (typeof host?.[name] !== "function") {
|
|
86
|
+
throw new TypeError(`EncodeAdmission requires ${name}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.#host = host;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Seconds of film per second this machine makes of each output while all of
|
|
94
|
+
* this is running, corrected for what nobody has priced.
|
|
95
|
+
*
|
|
96
|
+
* @param {number} costSec
|
|
97
|
+
* @returns {number}
|
|
98
|
+
*/
|
|
99
|
+
#speedAt(costSec) {
|
|
100
|
+
if (!(costSec > 0)) {
|
|
101
|
+
return Number.POSITIVE_INFINITY;
|
|
102
|
+
}
|
|
103
|
+
return correctForAvailability(1 / costSec, this.#host.availability());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What everything holding a place costs, leaving one output out.
|
|
108
|
+
*
|
|
109
|
+
* @param {string | null} except
|
|
110
|
+
* @param {string | null} [exceptFileKey] - Leave out every output of this file.
|
|
111
|
+
* @returns {{ costSec: number, files: Set<string>, unpriced: string[] }}
|
|
112
|
+
*/
|
|
113
|
+
#occupied(except, exceptFileKey = null) {
|
|
114
|
+
const live = this.#host.liveRunsByAddress();
|
|
115
|
+
/** @type {Map<string, number>} */
|
|
116
|
+
const units = new Map();
|
|
117
|
+
for (const [address, count] of live) {
|
|
118
|
+
if (address !== except && count > 0) {
|
|
119
|
+
units.set(address, count);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const address of this.#heldWithoutRun()) {
|
|
123
|
+
if (address !== except && !units.has(address)) {
|
|
124
|
+
units.set(address, 1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let costSec = 0;
|
|
128
|
+
const files = new Set();
|
|
129
|
+
const unpriced = [];
|
|
130
|
+
for (const [address, count] of units) {
|
|
131
|
+
const load = this.#host.loadOf(address);
|
|
132
|
+
if (exceptFileKey && load?.fileKey === exceptFileKey) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!load) {
|
|
136
|
+
unpriced.push(address);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (load.costSec === null) {
|
|
140
|
+
unpriced.push(address);
|
|
141
|
+
} else {
|
|
142
|
+
costSec += count * load.costSec;
|
|
143
|
+
}
|
|
144
|
+
if (load.fileKey && !files.has(load.fileKey)) {
|
|
145
|
+
files.add(load.fileKey);
|
|
146
|
+
costSec += load.fileCostSec;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { costSec, files, unpriced };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* What `count` encoders on this output add to what is already held.
|
|
154
|
+
*
|
|
155
|
+
* @param {OutputLoad} load
|
|
156
|
+
* @param {Set<string>} files
|
|
157
|
+
* @param {number} count
|
|
158
|
+
* @returns {number}
|
|
159
|
+
*/
|
|
160
|
+
static #added(load, files, count) {
|
|
161
|
+
if (count <= 0) {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
const file = load.fileKey && !files.has(load.fileKey) ? load.fileCostSec : 0;
|
|
165
|
+
return file + count * (load.costSec ?? 0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether a viewer may be prepared onto this output now.
|
|
170
|
+
*
|
|
171
|
+
* Asked BEFORE the preparation is recorded, and answered in the same
|
|
172
|
+
* synchronous stretch as the record is written: two preparations onto two
|
|
173
|
+
* different outputs then cannot both be told there is room for one, because
|
|
174
|
+
* the second is asked after the first has been recorded and is counted.
|
|
175
|
+
*
|
|
176
|
+
* An output that already holds a place — an encoder running there, or
|
|
177
|
+
* another viewer being prepared onto it — is admitted without arithmetic: it
|
|
178
|
+
* needs one encoder however many wait for it.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} address
|
|
181
|
+
* @returns {{ admitted: boolean, reason: string, speedX: number | null }}
|
|
182
|
+
*/
|
|
183
|
+
admitsPreparation(address) {
|
|
184
|
+
if (!address) {
|
|
185
|
+
return { admitted: false, reason: "the output has no address", speedX: null };
|
|
186
|
+
}
|
|
187
|
+
if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
|
|
188
|
+
return { admitted: true, reason: "an encoder already runs there", speedX: null };
|
|
189
|
+
}
|
|
190
|
+
if (this.#heldWithoutRun().has(address)) {
|
|
191
|
+
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
192
|
+
}
|
|
193
|
+
return this.#assessCandidate(address, this.#host.loadOf(address));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Whether a not-yet-opened format can take a place on the whole machine.
|
|
198
|
+
* The output choice calls this for each candidate rung; the final claim uses
|
|
199
|
+
* `admitsWatching` with the same arithmetic after registering the output.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} spec
|
|
202
|
+
* @param {object} file
|
|
203
|
+
* @returns {{ admitted: boolean, reason: string, speedX: number | null }}
|
|
204
|
+
*/
|
|
205
|
+
previewCandidate(spec, file) {
|
|
206
|
+
const address = typeof spec?.toKey === "function" ? spec.toKey() : "";
|
|
207
|
+
if (!address) {
|
|
208
|
+
return { admitted: false, reason: "the output has no address", speedX: null };
|
|
209
|
+
}
|
|
210
|
+
if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
|
|
211
|
+
return { admitted: true, reason: "an encoder already runs there", speedX: null };
|
|
212
|
+
}
|
|
213
|
+
if (this.#heldWithoutRun().has(address)) {
|
|
214
|
+
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
215
|
+
}
|
|
216
|
+
const load = this.#host.loadForCandidate?.(spec, file) ?? null;
|
|
217
|
+
return this.#assessCandidate(address, load);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Projected production rate for outputs currently holding machine capacity.
|
|
222
|
+
* A live encoder reading takes precedence; this measured-cost projection
|
|
223
|
+
* supplies the playback trajectory before its first run sample arrives.
|
|
224
|
+
*
|
|
225
|
+
* @returns {number | null}
|
|
226
|
+
*/
|
|
227
|
+
projectedSpeedX() {
|
|
228
|
+
const occupied = this.#occupied(null);
|
|
229
|
+
if (occupied.unpriced.length > 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const speedX = this.#speedAt(occupied.costSec);
|
|
233
|
+
return Number.isFinite(speedX) && speedX > 0 ? speedX : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#assessCandidate(address, load) {
|
|
237
|
+
if (!load || load.costSec === null) {
|
|
238
|
+
return { admitted: false, reason: "the output has no measured encoding cost", speedX: null };
|
|
239
|
+
}
|
|
240
|
+
const others = this.#occupied(address);
|
|
241
|
+
if (others.unpriced.length > 0) {
|
|
242
|
+
return {
|
|
243
|
+
admitted: false,
|
|
244
|
+
reason: `capacity is unknown while ${others.unpriced.length} occupied output(s) have no measured cost`,
|
|
245
|
+
speedX: null
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const speedX = this.#speedAt(others.costSec + EncodeAdmission.#added(load, others.files, 1));
|
|
249
|
+
if (speedX >= 1) {
|
|
250
|
+
return { admitted: true, reason: "", speedX: Number.isFinite(speedX) ? speedX : null };
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
admitted: false,
|
|
254
|
+
reason:
|
|
255
|
+
`this machine would make ${speedX.toFixed(2)}x of every output with one more encoder ` +
|
|
256
|
+
`(${(others.costSec + EncodeAdmission.#added(load, others.files, 1)).toFixed(3)} s of work ` +
|
|
257
|
+
`per second of film)` +
|
|
258
|
+
(others.unpriced.length > 0 ? `, not counting ${others.unpriced.length} output(s) nothing has priced` : ""),
|
|
259
|
+
speedX
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Whether a viewer may be put on this output now — asked when an output is
|
|
265
|
+
* OPENED for somebody, or when somebody joins one nothing is making.
|
|
266
|
+
*
|
|
267
|
+
* The same arithmetic as a preparation, and asked the same way: before the
|
|
268
|
+
* viewer is placed, in the same synchronous stretch as the placing, so the
|
|
269
|
+
* next opening is asked after this one is counted. An output that needs no
|
|
270
|
+
* encoder — everything it will serve already made — takes no place at all.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} address
|
|
273
|
+
* @returns {{ admitted: boolean, reason: string, speedX: number | null }}
|
|
274
|
+
*/
|
|
275
|
+
admitsWatching(address) {
|
|
276
|
+
if (address && this.#host.finished?.(address) === true) {
|
|
277
|
+
return { admitted: true, reason: "everything it serves is already made", speedX: null };
|
|
278
|
+
}
|
|
279
|
+
return this.admitsPreparation(address);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* What everything holding a place costs now, in seconds of work per second
|
|
284
|
+
* of film, and what that leaves: the figure a question about ANOTHER file is
|
|
285
|
+
* priced beside — the pool asking whether this host could serve a film it
|
|
286
|
+
* has only been told about.
|
|
287
|
+
*
|
|
288
|
+
* Outputs of the file being asked about are left out: a viewer of a film
|
|
289
|
+
* this host already makes joins what is made, and that costs it nothing new.
|
|
290
|
+
*
|
|
291
|
+
* @param {{ exceptFileKey?: string | null }} [options]
|
|
292
|
+
* @returns {{ costSec: number, unpriced: number }}
|
|
293
|
+
*/
|
|
294
|
+
occupiedCost({ exceptFileKey = null } = {}) {
|
|
295
|
+
const { costSec, unpriced } = this.#occupied(null, exceptFileKey);
|
|
296
|
+
return { costSec, unpriced: unpriced.length };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* How much room this machine has left for encoding, as the pool ranks it
|
|
301
|
+
* BEFORE any film is chosen: the speed every output would run at with what
|
|
302
|
+
* holds a place now, corrected for the share nobody has priced. At or below
|
|
303
|
+
* realtime the machine has no room for one more encode of anything; the
|
|
304
|
+
* pool's own proxy choice reads it to leave such a proxy out (roadmap item
|
|
305
|
+
* 97, step 14). Null where nothing holds a place — all the room there is.
|
|
306
|
+
*
|
|
307
|
+
* @returns {{ encodeSpeedX: number | null, occupiedCostSec: number }}
|
|
308
|
+
*/
|
|
309
|
+
headroom() {
|
|
310
|
+
const { costSec, unpriced } = this.#occupied(null);
|
|
311
|
+
const speed = this.#speedAt(costSec);
|
|
312
|
+
return {
|
|
313
|
+
// Unknown occupied work is not spare capacity. Zero here means that the
|
|
314
|
+
// pool must not prefer this host while another reports usable headroom.
|
|
315
|
+
encodeSpeedX: unpriced.length > 0 ? 0 : Number.isFinite(speed) ? Number(speed.toFixed(3)) : null,
|
|
316
|
+
occupiedCostSec: Number(costSec.toFixed(4))
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Every output that holds a place without an encoder running on it yet: one
|
|
322
|
+
* a present viewer is being prepared onto, and one a present viewer watches
|
|
323
|
+
* that still has something left to make.
|
|
324
|
+
*
|
|
325
|
+
* @returns {Set<string>}
|
|
326
|
+
*/
|
|
327
|
+
#heldWithoutRun() {
|
|
328
|
+
const held = new Set(this.#host.preparedAddresses());
|
|
329
|
+
for (const address of this.#host.watchedAddresses?.() ?? []) {
|
|
330
|
+
held.add(address);
|
|
331
|
+
}
|
|
332
|
+
return held;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* How many encoders this output may run, as far as the whole machine goes.
|
|
337
|
+
*
|
|
338
|
+
* Never fewer than already run — an admitted encoder is not taken away — and
|
|
339
|
+
* never fewer than one where the output holds a place for a preparation,
|
|
340
|
+
* because that place was promised when the preparation was admitted.
|
|
341
|
+
* Otherwise as many as still leave every output at realtime.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} address
|
|
344
|
+
* @param {number} running - Encoders alive on it now.
|
|
345
|
+
* @returns {{ runs: number, because: string }}
|
|
346
|
+
*/
|
|
347
|
+
placesFor(address, running) {
|
|
348
|
+
const floor = Math.max(
|
|
349
|
+
running,
|
|
350
|
+
this.#heldWithoutRun().has(address) ? 1 : 0
|
|
351
|
+
);
|
|
352
|
+
const load = this.#host.loadOf(address);
|
|
353
|
+
if (!load || load.costSec === null) {
|
|
354
|
+
// Nothing to price it with, so nothing to refuse it on: the per-output
|
|
355
|
+
// limit decides, as it did before there was a machine-wide one.
|
|
356
|
+
return { runs: Number.POSITIVE_INFINITY, because: "the output is not priced" };
|
|
357
|
+
}
|
|
358
|
+
const others = this.#occupied(address);
|
|
359
|
+
let runs = 0;
|
|
360
|
+
for (;;) {
|
|
361
|
+
const next = runs + 1;
|
|
362
|
+
if (this.#speedAt(others.costSec + EncodeAdmission.#added(load, others.files, next)) < 1) {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
runs = next;
|
|
366
|
+
if (!(load.costSec > 0)) {
|
|
367
|
+
// Free to run: the per-output limit is the only bound.
|
|
368
|
+
return { runs: Number.POSITIVE_INFINITY, because: "it costs nothing measurable" };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (runs >= floor) {
|
|
372
|
+
return { runs, because: "the machine" };
|
|
373
|
+
}
|
|
374
|
+
return { runs: floor, because: runs < running ? "what already runs" : "a place held for a preparation" };
|
|
375
|
+
}
|
|
376
|
+
}
|