@torrent-tv/proxy 2.87.2 → 2.87.4
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 +2157 -2148
- 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 +94 -94
- 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/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/sources/warm/post.js +217 -181
- 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/server.js +721 -720
- 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/audio-inventory.js +212 -212
- 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/playback-planner.js +856 -848
- 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/{viewer → server}/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +36 -36
- 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/torrent-pool.js +2675 -2635
- package/services/torrent/torrent-source-key.js +38 -38
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -760
- 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 +365 -350
- package/services/torrent/worker/protocol.js +184 -182
- 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 -1116
- package/services/transport/data-channel-handler.js +1356 -1356
- 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 +73 -73
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +176 -176
- 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/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 -294
- 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/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
|
@@ -1,729 +1,729 @@
|
|
|
1
|
-
# Encode architecture — who decides where encoders go
|
|
2
|
-
|
|
3
|
-
One question, one answer, and the answer is arithmetic.
|
|
4
|
-
|
|
5
|
-
> Viewers are always independent and always reuse what can be reused. The
|
|
6
|
-
> number of encoders is however many are needed; how many are needed follows
|
|
7
|
-
> from which sets of output parameters are wanted and where the viewers stand
|
|
8
|
-
> inside each. Segments produced by ANY encoder are available to ANY viewer, and
|
|
9
|
-
> which viewer asked never enters the question.
|
|
10
|
-
|
|
11
|
-
## The one authority
|
|
12
|
-
|
|
13
|
-
`services/encode/EncodePlan.js` decides. Nothing else places an encoder,
|
|
14
|
-
nothing else takes one away for scheduling reasons, and there is exactly one
|
|
15
|
-
call to `#startEncodeRun` in the whole proxy — the one the plan asks through.
|
|
16
|
-
|
|
17
|
-
It decides from four things and no others:
|
|
18
|
-
|
|
19
|
-
| what | where it comes from |
|
|
20
|
-
|---|---|
|
|
21
|
-
| what is already made | the segment store, asked afresh before every decision |
|
|
22
|
-
| what is being made | the coverage map's live claims |
|
|
23
|
-
| what is wanted | the priority map of that output |
|
|
24
|
-
| how many the machine can hold | `run-budget.js`, from measurements of this host |
|
|
25
|
-
|
|
26
|
-
**No viewer reaches the decision.** `EncodePlan` and `EncodeOrchestrator` do not
|
|
27
|
-
import the viewer layer, name a consumer id or hold a person. What crosses is a
|
|
28
|
-
priority map: zones of segment numbers with a rank and a real time, and nobody's
|
|
29
|
-
name on it.
|
|
30
|
-
|
|
31
|
-
**The rest of `services/encode/` names people, and holds none.** A step and a
|
|
32
|
-
soundtrack are chosen, warmed and left per person, so `Renditions`,
|
|
33
|
-
`QualityController` and `CushionReport` say WHOSE — by consumer id, which is a
|
|
34
|
-
name and not an object. What they may ask about that name and what they may
|
|
35
|
-
tell the viewer layer about it is `services/viewer/choices.js`: the mode a size
|
|
36
|
-
was picked in, what a link measured, the soundtrack chosen, the step on screen,
|
|
37
|
-
what is being warmed, who is present, and the standing quality ask. Values in,
|
|
38
|
-
values out. No `Viewer` crosses the boundary and nothing outside the viewer
|
|
39
|
-
layer writes a viewer's record — which is the rule a position cost 77 encoder
|
|
40
|
-
starts for breaking (field 2026-09-13).
|
|
41
|
-
|
|
42
|
-
This paragraph used to claim the whole directory named nobody, which was never
|
|
43
|
-
true of `Renditions` and could not be: which soundtrack to make is a fact about
|
|
44
|
-
a person. What matters is that it is a NAME, so the viewer's record stays in
|
|
45
|
-
one place with one writer.
|
|
46
|
-
|
|
47
|
-
**What answered a request is the viewer's, per viewing** (roadmap item 97,
|
|
48
|
-
step 9). The page stamps every request with the GENERATION of its viewing — a
|
|
49
|
-
number it raises on the viewer's own seek, before it sends anything — and the
|
|
50
|
-
proxy reads it rather than counting one, so a request made before a seek and
|
|
51
|
-
delivered after it still says which viewing it belongs to. Three things follow,
|
|
52
|
-
and each is asked through `choices.js`, by name:
|
|
53
|
-
|
|
54
|
-
1. every file route asks FIRST whether that viewer still takes requests of that
|
|
55
|
-
generation, before a step or a soundtrack is resolved — resolving can create
|
|
56
|
-
an output and registers the viewer on it, which a request of a viewing
|
|
57
|
-
already left must not do;
|
|
58
|
-
2. a repeat of a request within one viewing is answered by the output that
|
|
59
|
-
answered it: the step route records the height asked for and the segment
|
|
60
|
-
(the init as −1), the picture's own route records under the height that
|
|
61
|
-
output is named after. A soundtrack records nothing of this kind;
|
|
62
|
-
3. a response holds its output from its first byte to its end, and no output is
|
|
63
|
-
disposed while an assignment holds it — except on a hardware encoder's
|
|
64
|
-
failure, where nothing can go on making what it made. The idle expiry asks
|
|
65
|
-
only the assignments, not whether anybody is registered, so a paused
|
|
66
|
-
viewer's claim on an output stays a separate decision.
|
|
67
|
-
|
|
68
|
-
**Which output serves a viewer is one rule, asked by every path, and the
|
|
69
|
-
choice is the viewer's own** (step 11). A request for a piece is answered, in
|
|
70
|
-
order, by what already answered that address in this viewing (`given`), by the
|
|
71
|
-
output chosen for this viewer at that height (`chosen`), and only then by the
|
|
72
|
-
rule — and a repeat whose output has gone is served from its stored piece or by
|
|
73
|
-
an output with a proven-compatible header, or answered `assignment-lost`. The
|
|
74
|
-
rule (`serving-output.js`, `outputSuits`) asks the picture's material, the size
|
|
75
|
-
a viewer who picked by hand requires, and what the viewer's OWN link does with
|
|
76
|
-
the output's whole load: the picture and the soundtrack they chose, each part
|
|
77
|
-
`known` (a limit), `estimated` (a stated average) or `unknown` (no bound), the
|
|
78
|
-
load as trustworthy as its worst part (`link-budget.js`). `unknown` is refused
|
|
79
|
-
against a measured link; `estimated` is admitted by estimate and reported to
|
|
80
|
-
the page as not confirmed; a bound is taken before an average. When nothing
|
|
81
|
-
suits, nothing is handed over: the answer is `output-unavailable`, with the
|
|
82
|
-
figures, and the page explains it. The shared `OutputCatalog.servedBy` record
|
|
83
|
-
and the worst link over all viewers are gone; the quality budget asks each
|
|
84
|
-
viewer's link for that viewer alone.
|
|
85
|
-
|
|
86
|
-
**A bitrate limit is part of an output, not a figure moved under it** (step
|
|
87
|
-
10). A software encode's `-maxrate`/`-bufsize` are named by its key
|
|
88
|
-
(`vbv=<maxrate>k-<bufsize>k@<level>`), so two limits are two outputs and a
|
|
89
|
-
slow viewer's limit can no longer land on every viewer of a picture — which is
|
|
90
|
-
what it did while it was a field of the output set by the worst of their links.
|
|
91
|
-
The level is pinned (`-level:v`) to the level of the NOMINAL output of that
|
|
92
|
-
size and frame rate, so every limit at one size writes the same header
|
|
93
|
-
and a viewer can be moved between them under the address they already play.
|
|
94
|
-
A limit above the nominal one is refused. Hardware encoders are given no limit
|
|
95
|
-
and state none (`vbv=-`). Whether the pinned level is the one x264 would choose
|
|
96
|
-
for the nominal output is checked before a release
|
|
97
|
-
(`stand/segment-compat/level-check.mjs`).
|
|
98
|
-
|
|
99
|
-
**Which limits a frame has is decided by its AREA** (step 14, decided with the
|
|
100
|
-
user 2026-09-24). The rows of limits are the ladder's frames at 16:9, sized by
|
|
101
|
-
the same function that sizes an encode; a frame takes the row nearest by the
|
|
102
|
-
ratio of areas, the larger row on the exact border, and a row with no nominal
|
|
103
|
-
of its own borrows the nearest row that has one (`limitRowFor`,
|
|
104
|
-
`nominalKbpsFor`). Every place that picks a row — opening an output, the move
|
|
105
|
-
between limits, the offer's price of a step and the link rule — is handed the
|
|
106
|
-
whole frame, so all of them name the same row. By height alone a 1920x800 film
|
|
107
|
-
fell in the 720 row although it is nearer the 1080 one. The proposed measured
|
|
108
|
-
table of lower limits was dropped on 2026-09-27. Step 14 now qualifies this
|
|
109
|
-
machine's encoder modes and does not set lower bitrate limits.
|
|
110
|
-
|
|
111
|
-
## Where a viewer is, and how it reaches the encoders
|
|
112
|
-
|
|
113
|
-
A viewer's position has ONE writer — the viewer — and is a function of time:
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
position(now) = stated + (playing ? 1 : 0) × (now − statedAt)
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
A viewer whose picture moves covers a second of film every second, so between
|
|
120
|
-
the moments they speak their position is known exactly; one whose picture is
|
|
121
|
-
stopped covers nothing. Nothing else writes it, and in particular a request for
|
|
122
|
-
a segment does not: a request says the viewer is still here, and the segment
|
|
123
|
-
either exists and is served or does not and is waited for.
|
|
124
|
-
|
|
125
|
-
Held as a stored number instead it was a staircase, flat for the ten seconds
|
|
126
|
-
between reports and then a jump — and with two writers filling it in turn the
|
|
127
|
-
steps went backwards as often as forwards. Field 2026-09-13, one output over one
|
|
128
|
-
second: `p100:#9..#10`, `p100:#18..#20`, `p100:#16..#18`, `p100:#18..#20`, while
|
|
129
|
-
the second viewer sat frozen at 1673.6 s. The placement follows the map, so the
|
|
130
|
-
machine spent six minutes on 77 encoder starts and 141 stops.
|
|
131
|
-
|
|
132
|
-
PRESENCE IS A SEPARATE FACT and it is the connection: a viewer is here until
|
|
133
|
-
something says otherwise. Silence never says it — a pause, a hidden tab whose
|
|
134
|
-
timers are throttled, and a full cushion are all silent and all still watching.
|
|
135
|
-
|
|
136
|
-
## Viewers become a map, and that is the whole crossing
|
|
137
|
-
|
|
138
|
-
```mermaid
|
|
139
|
-
flowchart TB
|
|
140
|
-
subgraph V["services/viewer — where people are"]
|
|
141
|
-
VW[Viewer<br/>position as a function of time, playing, buffered, chosen track]
|
|
142
|
-
VS[Viewers<br/>the relation, indexed from both ends]
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
subgraph P["services/priority — one fact, two scopes"]
|
|
146
|
-
PM[PriorityMap<br/>seconds of film to a rank]
|
|
147
|
-
PO[PriorityOrchestrator<br/>merge, publish, keep]
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
subgraph E["services/encode — where encoders go"]
|
|
151
|
-
CM[CoverageMap<br/>ready / making / free]
|
|
152
|
-
EP[EncodePlan<br/>argmin over arrangements]
|
|
153
|
-
RB[run-budget<br/>what this host can hold]
|
|
154
|
-
ER[EncodeRun<br/>one process, one interval]
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
subgraph D["services/torrent/demand + torrent/download — what the swarm is told"]
|
|
158
|
-
DR[DemandRegister]
|
|
159
|
-
SS[SwarmSelection]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
VW --> VS
|
|
163
|
-
VS -->|"positions, presence"| PO
|
|
164
|
-
PM --> PO
|
|
165
|
-
PO -->|"per FILE, in bytes"| DR
|
|
166
|
-
DR --> SS
|
|
167
|
-
PO -->|"per OUTPUT, in segments"| EO[EncodeOrchestrator]
|
|
168
|
-
EO --> EP
|
|
169
|
-
CM --> EP
|
|
170
|
-
RB --> EP
|
|
171
|
-
EP -->|"start / stop / move"| ER
|
|
172
|
-
ST[(SegmentStore<br/>the disk)] -->|"what is ready"| CM
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Two scopes of one map, and both are right
|
|
176
|
-
|
|
177
|
-
The map is built from where the viewers are, once, and read at two scopes.
|
|
178
|
-
|
|
179
|
-
**Per FILE, for the swarm.** The picture, a quality step and a soundtrack of one
|
|
180
|
-
film read the same bytes, so every viewer of any of them wants that file's
|
|
181
|
-
bytes. This is what `PriorityOrchestrator.mapFor(sourceKey, fileIndex)`
|
|
182
|
-
answers, and what is published to the download layer.
|
|
183
|
-
|
|
184
|
-
**Per OUTPUT, for the encoders.** A person watching 480p wants nothing of the
|
|
185
|
-
1080p output at all. This is `mapForOutput(address)`.
|
|
186
|
-
|
|
187
|
-
One map for both was the second authority over encoders. Handed the film's map,
|
|
188
|
-
the plan wanted an encoder on every output of the film; what actually stopped
|
|
189
|
-
the ones nobody was watching was the session manager killing them by its own
|
|
190
|
-
judgement — and since a viewer moving between steps announces itself, the plan
|
|
191
|
-
started them again on the very next pass. Two parties answering "should this
|
|
192
|
-
encoder exist" by different rules, several times a second.
|
|
193
|
-
|
|
194
|
-
An output nobody is on gets a map with **nothing in it**, which is a statement
|
|
195
|
-
and not an absence: the walk writes one for every output a session exists for,
|
|
196
|
-
including the ones everybody has left. That is how the plan is told to stop what
|
|
197
|
-
is on it.
|
|
198
|
-
|
|
199
|
-
## Which output a person is consuming
|
|
200
|
-
|
|
201
|
-
A person holds a record on more outputs than they are consuming. The picture is
|
|
202
|
-
where their record lives — the browser addresses it, their chosen soundtrack is
|
|
203
|
-
written on it, their position is read from it — so they are never let go of it;
|
|
204
|
-
but the moment they step down to 480p, the 1080p output is producing for nobody.
|
|
205
|
-
|
|
206
|
-
That distinction is answered where the two facts meet, and neither layer is
|
|
207
|
-
handed the other:
|
|
208
|
-
|
|
209
|
-
- **which step is on their screen** is a fact about a PERSON, read off the
|
|
210
|
-
viewer as one field. The page STATES it (`playingHeight` on the viewer's
|
|
211
|
-
report, sent the moment hls.js has switched), and `Renditions.viewerPlays`
|
|
212
|
-
records it. It used to be inferred from the first segment requested of a
|
|
213
|
-
rung, which is the player fetching rather than the picture having moved;
|
|
214
|
-
- **which output a step supersedes** is a fact about the FILM'S SHAPE, answered
|
|
215
|
-
by `OutputCatalog.supersededBy(session, stepOnScreen)`, which takes a plain id
|
|
216
|
-
and has never seen a viewer.
|
|
217
|
-
|
|
218
|
-
A step, a soundtrack, and a step being warmed are consumed by whoever is
|
|
219
|
-
registered on them — everywhere but the picture, a person who stops watching is
|
|
220
|
-
let go of, so being known to an output is consuming it. Whether an output is
|
|
221
|
-
wanted at all is asked of the viewer registry and nothing else: an output a
|
|
222
|
-
picture made on its own behalf once carried a made-up "family" claim that kept
|
|
223
|
-
it alive, which was a second answer to the same question and is gone. Every
|
|
224
|
-
viewer has a name; a request that names nobody makes no viewer. Through a warm-up both
|
|
225
|
-
the step on screen and the step being made ready are genuinely produced, which
|
|
226
|
-
is the price of the switch not being visible.
|
|
227
|
-
|
|
228
|
-
## A move between two limits of one height
|
|
229
|
-
|
|
230
|
-
A bitrate limit is part of an output, so a viewer whose link cannot carry the
|
|
231
|
-
limit on their screen is given ANOTHER output of the same height at a lower
|
|
232
|
-
limit — for them alone, and without their player being told (roadmap item 97,
|
|
233
|
-
step 12). The address `v/<height>/…` does not change; what changes is which
|
|
234
|
-
output this side answers it with.
|
|
235
|
-
|
|
236
|
-
1. **Prepared, not switched.** `Renditions.prepareSameHeightSwitch` opens (or
|
|
237
|
-
finds, by its key) the output of the same size at the next limit of
|
|
238
|
-
`limitsFor(height)` that the viewer's link admits, marks it a step of the
|
|
239
|
-
picture, and registers the viewer on it — that is what buys it an encoder.
|
|
240
|
-
The choice (`Assignments.choose`) is not touched: until the move is made they
|
|
241
|
-
are given what they were given. The move being prepared is a field of the
|
|
242
|
-
viewer, `Viewer.sameHeightSwitch`, and not `warmingVariantId`, which is a
|
|
243
|
-
step their player will switch to itself.
|
|
244
|
-
2. **Made on the store's own event.** `SegmentStore.onPublished` is filtered by
|
|
245
|
-
the prepared output's key. The move is made when the segment the viewer will
|
|
246
|
-
ask for NEXT is closed on it: one past the highest they were given in the
|
|
247
|
-
viewing they are in now, or — with nothing given in it, straight after a
|
|
248
|
-
seek — the one where they stand. It is read at the moment of the event, so a
|
|
249
|
-
segment closed behind that one, or an event that arrives after a seek, moves
|
|
250
|
-
nothing.
|
|
251
|
-
3. **One synchronous stretch.** The choice under each height it was chosen
|
|
252
|
-
under, the verdict, the step on screen, the registration on the new output
|
|
253
|
-
and off the old one change with no `await` between them.
|
|
254
|
-
4. **The output left is not disposed.** The picture is never left. A step that
|
|
255
|
-
is left loses the viewer, the plan stops its encoder by its empty map, and the
|
|
256
|
-
idle expiry removes it once no assignment holds it; a repeat of an address it
|
|
257
|
-
answered is still answered by it, because `given` is read before `chosen`.
|
|
258
|
-
5. **Cancelled when not wanted.** When the link carries the output on screen
|
|
259
|
-
again, when the prepared output no longer suits, when the player moves to
|
|
260
|
-
another rung, when encoding the prepared output has failed for good, or when
|
|
261
|
-
nothing of the film is left to give from it: the viewer stops watching the
|
|
262
|
-
prepared output and nothing else changes for anybody.
|
|
263
|
-
6. **Asked again on three events, never on a timer.** A segment closed on the
|
|
264
|
-
prepared output (`SegmentStore.onPublished`); the viewer's own report
|
|
265
|
-
(`Renditions.noteViewerReported`, from the net-report route), which is what
|
|
266
|
-
carries a change of their link; and encoding the prepared output failing for
|
|
267
|
-
good (`Renditions.noteProductionFailed`, told by `EncodeRuns`). Without the
|
|
268
|
-
last two, a move onto an output that closes nothing stood for ever, and the
|
|
269
|
-
quality budget — which leaves a viewer with a move pending alone — never
|
|
270
|
-
decided for them again. Each event may also make the move, on the same one
|
|
271
|
-
condition: the segment they will ask for next is closed. A move is not
|
|
272
|
-
prepared onto an output whose encoding has failed. The fourth way a move can
|
|
273
|
-
wait — no encoder placed on the output — does not arise until the admission
|
|
274
|
-
of step 13 can refuse, and that refusal will be its event.
|
|
275
|
-
|
|
276
|
-
The quality budget uses this lever first, in both directions: down to the
|
|
277
|
-
highest lower limit the link admits, up one limit at a time, and another height
|
|
278
|
-
only where the height on screen has no limit left to move to. A copy has no
|
|
279
|
-
limit and a hardware encode is given none, so for them the lever is still the
|
|
280
|
-
height. The measured table of lower bitrate limits was dropped on 2026-09-27;
|
|
281
|
-
the product currently supplies only the nominal limit through `limitsFor`.
|
|
282
|
-
Per-machine calibration (step 14) qualifies encoder modes and does not create
|
|
283
|
-
lower bitrate limits, so same-height bitrate moves have no target in the current
|
|
284
|
-
product configuration.
|
|
285
|
-
|
|
286
|
-
7. **A move up waits for the cushion; a move down does not** (roadmap item
|
|
287
|
-
98). Once the piece asked for next is closed, a move UP is made only when
|
|
288
|
-
the viewer holds the cushion this file needs (`minimumBufferSeconds`), and
|
|
289
|
-
their next report asks again until they do. A move DOWN was started because
|
|
290
|
-
their buffer would run dry, so it is made the moment the piece is ready.
|
|
291
|
-
|
|
292
|
-
## When the quality budget moves a viewer (roadmap item 98)
|
|
293
|
-
|
|
294
|
-
The page has no quality control: the quality is always automatic. What moves a
|
|
295
|
-
viewer is decided here, per viewer, and each part of it is a measurement.
|
|
296
|
-
|
|
297
|
-
1. **Judged on the viewer's report, not on a timer.** `QualityController.noteViewerReported`
|
|
298
|
-
runs from the net-report route. The chosen fifteen-second window of a slow
|
|
299
|
-
link, the ten-second buffer threshold and the thirty-second wait after every
|
|
300
|
-
action are gone.
|
|
301
|
-
2. **Down when the buffer would run dry first.** Their link does not carry the
|
|
302
|
-
stream they are given AND, on the buffer's trend, it ends sooner than another
|
|
303
|
-
output could close the piece they need, counting the time until their next
|
|
304
|
-
report (`quality/drain-threat.js`). The trend is a least-squares fit over the
|
|
305
|
-
shortest run of their reports spanning one segment
|
|
306
|
-
(`viewer/buffer-trend.js`), because the buffer rises a segment at a time and
|
|
307
|
-
two readings catch only the rise or the fall. The time to readiness is this
|
|
308
|
-
host's measured first-segment time. The levers: a lower limit of the same
|
|
309
|
-
height, then a lower height asked of the player as URGENT — their page
|
|
310
|
-
switches as soon as the rung is ready, without waiting for a cushion. Where
|
|
311
|
-
neither can be prepared they stay on what they are given, with no message.
|
|
312
|
-
3. **The picture the viewer sees bounds a re-encode.** The page sends the
|
|
313
|
-
frame it would show without enlarging, in physical pixels; it is kept on
|
|
314
|
-
`Viewer.visiblePicture`. The bound is the smallest rung of the source's
|
|
315
|
-
ladder whose frame is not smaller (`quality/visible-rung.js`). It sets the
|
|
316
|
-
box an output is opened at (`OutputOpening`), the ceiling of every step up,
|
|
317
|
-
and the rung a step down prefers. A copy of the source is never re-encoded
|
|
318
|
-
for being taller than that.
|
|
319
|
-
4. **A smaller picture moves the viewer only onto a READY rung**
|
|
320
|
-
(`Renditions.heightReadyFor`: the piece they ask for next closed on it);
|
|
321
|
-
with none ready, the next judgement of their link or of the machine applies
|
|
322
|
-
the bound. **A larger picture** asks one rung up when the machine and their
|
|
323
|
-
link have room.
|
|
324
|
-
5. **A step up is let go when its conditions go back**: a draining buffer, a
|
|
325
|
-
link that no longer carries the stream, or no room for the output. The page
|
|
326
|
-
drops the move it was preparing when the proxy stops asking for it.
|
|
327
|
-
6. **The page switches a variant when the rung is ready**, and for a request
|
|
328
|
-
that is not urgent only once it holds `minimumBufferSeconds`.
|
|
329
|
-
|
|
330
|
-
## A place on the machine
|
|
331
|
-
|
|
332
|
-
How many encoders ONE output may run is answered per output
|
|
333
|
-
(`EncodeRuns.maxRunsForOutput`) and is always at least one. What refuses the
|
|
334
|
-
encoder that would slow everybody is `EncodeAdmission`, across every output at
|
|
335
|
-
once (roadmap item 97, step 13).
|
|
336
|
-
|
|
337
|
-
1. **The unit is seconds of work per second of film**, the one the quality
|
|
338
|
-
offer judges steps in (`EncodeCost.loadOfOutput`). Costs add, and a set of
|
|
339
|
-
encoders is affordable when the machine, corrected for the share of it
|
|
340
|
-
nobody has priced, still makes a second of film per second. A soundtrack is
|
|
341
|
-
a small fraction of a picture; counting processes would refuse a viewer
|
|
342
|
-
their sound. On the addon host the sum reproduces the measurement: two 1080p
|
|
343
|
-
encodes at 1.96x alone cost 1.02 s/s and make 0.98x, measured 0.99x and
|
|
344
|
-
0.98x.
|
|
345
|
-
2. **A place is held by an OUTPUT**: by every live encoder at its output's
|
|
346
|
-
cost, and by every output a present viewer is being prepared onto — a step
|
|
347
|
-
being warmed or a move to another limit — once however many wait for it.
|
|
348
|
-
The second is read off the viewers' own records (`Viewers.outputsBeingPrepared`),
|
|
349
|
-
so a place ends with its record on whichever path that record is cleared,
|
|
350
|
-
and there is no register of its own to keep in step.
|
|
351
|
-
3. **Asked before a preparation is recorded**, in the same synchronous stretch
|
|
352
|
-
as the record: two preparations onto two different outputs with room for
|
|
353
|
-
one cannot both be admitted, because the second is asked after the first is
|
|
354
|
-
written. Refused, a move answers `noPlace` and the budget goes to a lower
|
|
355
|
-
height (never a higher one); a warm-up answers `output-unavailable`.
|
|
356
|
-
4. **The plan is bounded by it** (`EncodeOrchestrator.#withinMachine`): an
|
|
357
|
-
output may run no more encoders than still fit, and a new one on a full
|
|
358
|
-
machine gets none until one elsewhere ends.
|
|
359
|
-
5. **An admitted encoder is never taken away**, and a place promised to a
|
|
360
|
-
preparation is kept: the machine's answer is never below what already runs
|
|
361
|
-
on an output, nor below one for an output a preparation holds.
|
|
362
|
-
|
|
363
|
-
Not solved: places go to whoever asks first, so a second encoder catching up on
|
|
364
|
-
one output can hold the place a viewer opening another film waits for.
|
|
365
|
-
|
|
366
|
-
## What each of the eight other places used to do
|
|
367
|
-
|
|
368
|
-
Before 2026-09-08 the plan was one opinion among nine. Each of these placed or
|
|
369
|
-
killed encoders by a rule of its own; each now states the fact it knows.
|
|
370
|
-
|
|
371
|
-
| it knows | it used to do | it now says |
|
|
372
|
-
|---|---|---|
|
|
373
|
-
| a session was created | start a run at the viewer's position, worked out again | the viewer is placed; the plan reads that |
|
|
374
|
-
| a viewer joined further in | start a run there if nothing was being made | as above |
|
|
375
|
-
| a step or soundtrack is being warmed | point that session at the switch and start it | this person is at N seconds on it |
|
|
376
|
-
| a step was switched to | stop the one left, point the new one | this person is on this step now |
|
|
377
|
-
| a step or track was abandoned | stop its encoder | this person has left that output |
|
|
378
|
-
| the hardware encoder failed | start a run at the dead one's start | what this host encodes with has changed |
|
|
379
|
-
| the input came back | start a run at the last requested segment | decide again |
|
|
380
|
-
| the cut table was corrected | restart the members at the measured instant | this run is producing in the wrong place; the instant is in the file's table |
|
|
381
|
-
| the bitrate cap changed | restart at where the encoder had got to | nothing: a limit is part of an output since 2.87.0, so another limit is another output and no running encoder is ever told a new one |
|
|
382
|
-
|
|
383
|
-
A **seek** is not in that list because it was already reduced to one thing: it
|
|
384
|
-
puts the viewer where they are. The settle timer behind it, its cooldown and its
|
|
385
|
-
one-segment backoff are gone — a second debounce on a signal the browser had
|
|
386
|
-
already debounced, and every millisecond of it was dead time in front of the
|
|
387
|
-
viewer.
|
|
388
|
-
|
|
389
|
-
## Whether the map is being served in its own order
|
|
390
|
-
|
|
391
|
-
The zones say what matters most. What the viewer actually waited for is measured
|
|
392
|
-
where a viewer measurably waits — the one place in the proxy that holds a
|
|
393
|
-
request for a named segment — and recorded against the rank the map gave that
|
|
394
|
-
segment **at the moment it was asked for**. It reaches the `encode:` state line
|
|
395
|
-
as `served[...]`:
|
|
396
|
-
|
|
397
|
-
```
|
|
398
|
-
served[now 42 wait(s) median 180ms worst 1900ms, soon 6 wait(s) median 90ms worst 240ms, later none]
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
Read it like this:
|
|
402
|
-
|
|
403
|
-
| what it says | what is wrong |
|
|
404
|
-
|---|---|
|
|
405
|
-
| long waits at `now` | the urgent zone is not being served first — a fault in whoever acts on the map |
|
|
406
|
-
| long waits at `soon`/`later`, none at `now` | the zones are the wrong width: the urgent one too narrow, so the viewer reaches material only ranked "soon" |
|
|
407
|
-
| `now none` while the viewer is watching | nothing was ever urgent — the map is not reaching this output |
|
|
408
|
-
| a band reading `none` | silence, not a zero, and it is printed as `none` so it cannot be read as "no waits, all good" |
|
|
409
|
-
|
|
410
|
-
Ranks are collapsed into three bands because the map's own scale is as long as
|
|
411
|
-
the film needs — a hundred ranks on a long file — and a hundred-row table says
|
|
412
|
-
nothing a reader can hold. The width of `soon` is a tenth of the top rank, which
|
|
413
|
-
is the map's own shape (its zones widen geometrically) rather than a threshold
|
|
414
|
-
chosen for the table.
|
|
415
|
-
|
|
416
|
-
The download half is measured the same way and on the same scale, so the two are
|
|
417
|
-
comparable: `download-architecture.md`.
|
|
418
|
-
|
|
419
|
-
## What the master playlist declares, and why it is not cosmetic
|
|
420
|
-
|
|
421
|
-
`BANDWIDTH` and `AVERAGE-BANDWIDTH` per variant, both measured
|
|
422
|
-
(`services/encode/output/rates.js`):
|
|
423
|
-
|
|
424
|
-
- the average is the file's own length over its duration, exact and known when
|
|
425
|
-
the session is created;
|
|
426
|
-
- the peak is the biggest piece produced over the span it covers, and equals the
|
|
427
|
-
average until a piece exists — a ratio invented meanwhile would be the
|
|
428
|
-
fabrication this replaced;
|
|
429
|
-
- a re-encoded height is declared at the cap we impose, which is exact;
|
|
430
|
-
- a smaller height at the pixel share of the source's rate, which errs high, and
|
|
431
|
-
high is the safe direction.
|
|
432
|
-
|
|
433
|
-
It used to be `height * height * 3.2`. **The browser sizes its cushion in BYTES
|
|
434
|
-
from `BANDWIDTH`**, so a figure five times low makes the cushion five times
|
|
435
|
-
shallow: field 2026-09-08, 3.73 Mbit/s declared for a file carrying 18.4, 120 s
|
|
436
|
-
asked bought 56 MB — 26 s of that film — and the deepest the browser ever held
|
|
437
|
-
was 17.1 s. Inflating it is not the answer either: hls.js compares it against
|
|
438
|
-
its own estimate of the link to decide a level is unplayable, and its recovery
|
|
439
|
-
then moves level by itself, which does not honour our pinning.
|
|
440
|
-
|
|
441
|
-
## The objective is the map's own rank order
|
|
442
|
-
|
|
443
|
-
Not three terms in seconds. One pair per rank the map states, most urgent rank
|
|
444
|
-
first, compared position by position:
|
|
445
|
-
|
|
446
|
-
```
|
|
447
|
-
[ late(100), done(100), late(99), done(99), … late(1), done(1) ], encoders, wasted
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
`late(r)` is how long anybody waits past a deadline at rank `r`; `done(r)` is
|
|
451
|
-
when the last number of that rank is made. A difference at a higher rank settles
|
|
452
|
-
it and nothing lower can reopen it — which is the stated order: nobody stares at
|
|
453
|
-
a spinner; then the film in front is encoded as fast as it can be, band by band
|
|
454
|
-
as the map ranks them; then, with what is left over and only then, the film
|
|
455
|
-
behind, in case somebody seeks back.
|
|
456
|
-
|
|
457
|
-
**No weights, and none possible.** A weight would let seconds at one rank buy
|
|
458
|
-
seconds at another, and it would be a figure nobody measured. The map is the
|
|
459
|
-
source of truth about what matters and it already says so — ten ranks on a film,
|
|
460
|
-
p100 at the number a viewer is stopped on, doubling zones down to p91 for the far
|
|
461
|
-
tail, p1 for what lies behind them.
|
|
462
|
-
|
|
463
|
-
`encoders` ranks below every rank of the map, so spare capacity cannot buy an
|
|
464
|
-
encoder where the map is indifferent. `wasted` is the swarm's bill for anything
|
|
465
|
-
fetched twice.
|
|
466
|
-
|
|
467
|
-
## Why an encoder is not moved for nothing, and it is not a rule
|
|
468
|
-
|
|
469
|
-
There is no threshold here, and there must not be one. What kept an encoder
|
|
470
|
-
moving was three quantities computed wrongly, not a policy that needed tuning.
|
|
471
|
-
|
|
472
|
-
**`delaySec` is when a body finishes the piece it STANDS ON.** For a run that has
|
|
473
|
-
produced something, one piece at the rate in force; for one still warming up, the
|
|
474
|
-
measured time to a first piece less the time it has already been alive. The
|
|
475
|
-
arrival of anything further on is that plus the pieces between, and nothing else.
|
|
476
|
-
|
|
477
|
-
It used to be `delay + (index - at + 1) / rate`, which charges every body a whole
|
|
478
|
-
piece for the one it is already making. That is right for a body that does not
|
|
479
|
-
exist yet and wrong for a run 0.8 s into a 0.9 s piece — and the difference is
|
|
480
|
-
the whole fault: from #58, reaching #59 was priced at 1.89 s against 1.88 s for a
|
|
481
|
-
kill and a cold start, a coin flip lost by ten milliseconds. Priced correctly it
|
|
482
|
-
is 1.08 s against 1.88 s.
|
|
483
|
-
|
|
484
|
-
**One rate, the one the arrangement puts in force.** Concurrent encoders slow
|
|
485
|
-
each other, measured on this host, so a piece costs what it costs at the body
|
|
486
|
-
count the arrangement has. Taken from the unpenalised rate while arrivals used
|
|
487
|
-
the penalised one, an extra body looked cheaper than it is and the plan bought a
|
|
488
|
-
second encoder where one served.
|
|
489
|
-
|
|
490
|
-
**Each body states its own debt** where it is created, as a function of what one
|
|
491
|
-
piece costs — because only there is it known what the body IS, and at the point
|
|
492
|
-
of pricing only how many there are. So there is no kind, no tag and no case
|
|
493
|
-
analysis.
|
|
494
|
-
|
|
495
|
-
**The measured start is separated from the piece it contains**, because the two
|
|
496
|
-
scale differently: a piece costs more when encoders share the machine, a spawn
|
|
497
|
-
does not.
|
|
498
|
-
|
|
499
|
-
```
|
|
500
|
-
spawn overhead = measured first output - what one piece costs alone
|
|
501
|
-
a fresh encoder = spawn overhead + the piece at the rate in force
|
|
502
|
-
a moved one = the kill, and then the same
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
**Both figures are measured before any viewer exists**, and until 2026-09-10
|
|
506
|
-
neither was. They were learned only from runs that had ENDED, so at a cold open
|
|
507
|
-
both read zero — and zero does not mean "unknown" here, it means "free".
|
|
508
|
-
|
|
509
|
-
That is not a small bias, it is the whole comparison. Subtract keeping from
|
|
510
|
-
moving and what is left is exactly the killing plus the time the run has already
|
|
511
|
-
lived:
|
|
512
|
-
|
|
513
|
-
```
|
|
514
|
-
keep a warming run = firstByteWait - elapsed
|
|
515
|
-
move it = kill + spawn overhead + one piece
|
|
516
|
-
= kill + firstByteWait (spawn overhead = firstByteWait - one piece)
|
|
517
|
-
move - keep = kill + elapsed
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
`elapsed` is the warm-up a move throws away — a process started, an input
|
|
521
|
-
opened, the first bytes fetched, a decoder filled — and it is the term that
|
|
522
|
-
makes moving cost something. Set `firstByteWait` to zero and it cancels out of
|
|
523
|
-
both sides along with the kill: keeping and moving then cost the same figure to
|
|
524
|
-
the millisecond, the tie falls to position, and any advantage however small wins.
|
|
525
|
-
|
|
526
|
-
Field 2026-09-08, the first fifteen seconds of a session: start at #68, a second
|
|
527
|
-
later kill and start at #69, half a second later kill and start at #68 again,
|
|
528
|
-
each dying having produced nothing. Over two days 153 runs were stopped by the
|
|
529
|
-
plan and 68 of them made no segment at all, median life 9.6 s.
|
|
530
|
-
|
|
531
|
-
`services/encode/start-stop-cost.js` measures both from **one ffmpeg run at
|
|
532
|
-
startup**: spawn to the first piece the encoder itself announces closed, then
|
|
533
|
-
SIGTERM to exit. 0.68 s and 0.02 s on the developer's desktop. `RunCosts` starts
|
|
534
|
-
from those and replaces them with readings from real runs as they arrive.
|
|
535
|
-
|
|
536
|
-
There was an `Infinity` here once for the cost of a move, on the reasoning that
|
|
537
|
-
an unmeasured price must not license an irreversible act. It was removed as an
|
|
538
|
-
exception in a model that needs none — correctly, but what replaced it was a
|
|
539
|
-
floor of zero, which licenses the act rather than forbidding it. A measurement
|
|
540
|
-
is what a model like this needs, not an exception and not a floor.
|
|
541
|
-
|
|
542
|
-
And a run killed before producing anything is a measurement too — a lower bound
|
|
543
|
-
on the first output, and the only reading a thrash can supply, since every run in
|
|
544
|
-
one is killed before it finishes anything. So a thrash makes its own moves
|
|
545
|
-
progressively dearer until it stops.
|
|
546
|
-
|
|
547
|
-
### What that was for
|
|
548
|
-
|
|
549
|
-
Field 2026-09-08: 39 moves in one session, 24 of them between three adjacent
|
|
550
|
-
numbers about 0.8 s apart. One viewer on a host affording three runs got three.
|
|
551
|
-
The picture stood still for 116.7 s in three interruptions, the worst 91.8 s.
|
|
552
|
-
|
|
553
|
-
Compounding it, the map states `withinSeconds: null` for the film behind the
|
|
554
|
-
viewers — nobody is waiting there — and `deadlineReaderFor` read that through
|
|
555
|
-
`Number()`, where `null` is 0. So the film a viewer had already passed was due
|
|
556
|
-
IMMEDIATELY and was the most urgent material in the file: it bought encoders, and
|
|
557
|
-
it took the run standing in front of the viewer because that run was the nearest
|
|
558
|
-
body to it.
|
|
559
|
-
|
|
560
|
-
Checked by simulation over eighty ticks against the map's real shape — ten zones
|
|
561
|
-
doubling ahead of the viewer, one behind — at both one and three runs: the
|
|
562
|
-
encoder is placed once, left alone, and moved exactly once, at the viewer's own
|
|
563
|
-
seek.
|
|
564
|
-
|
|
565
|
-
## Where a second encoder joins a stretch
|
|
566
|
-
|
|
567
|
-
Derived, not halved. A stretch of unmade film runs from `from` to `to`; whoever
|
|
568
|
-
is already on it stands at `from` and owes `w` before the piece under it exists;
|
|
569
|
-
a fresh one placed at `x` owes `d` — its start and then a whole piece — and both
|
|
570
|
-
then work at the rate two encoders leave each other, which is measured here.
|
|
571
|
-
|
|
572
|
-
```
|
|
573
|
-
the one already there closes [from, x-1]: w + (x - 1 - from) / r rises with x
|
|
574
|
-
the fresh one closes [x, to]: d + (to - x) / r falls with x
|
|
575
|
-
|
|
576
|
-
x* = (from + to + 1) / 2 + (d - w) * r / 2
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
The midpoint, shifted forward by half the difference of what the two owe, in
|
|
580
|
-
pieces. Halving is the special case `d = w`, which holds when both are fresh.
|
|
581
|
-
|
|
582
|
-
**The shift is under one segment in every measured configuration** — 0.24 of a
|
|
583
|
-
piece at the addon host's 1080p contention — so halving was very nearly right,
|
|
584
|
-
and saying otherwise would be dressing it up.
|
|
585
|
-
|
|
586
|
-
What the derivation adds is the question halving never asked: **is a second
|
|
587
|
-
encoder worth having at all?**
|
|
588
|
-
|
|
589
|
-
```
|
|
590
|
-
one: w + (to - from - 1) / rate
|
|
591
|
-
two: w + (x* - 1 - from) / r
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
Nothing is proposed where the second does not win. At 1920x1080 the measured
|
|
595
|
-
penalty for a second encoder on the addon host is 1.98 — it takes very nearly
|
|
596
|
-
all of the first's speed — and the objective keeps one; where a second is free it
|
|
597
|
-
places three.
|
|
598
|
-
|
|
599
|
-
## What the log says when anything is placed or taken away
|
|
600
|
-
|
|
601
|
-
```
|
|
602
|
-
encode-plan on <output>: start #58..#481, stop #?..#?
|
|
603
|
-
[speed=4.45x firstByte=1.26s kill=0.04s refetch=0.000s/s maxRuns=2 live=1]
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
Every action with its INTERVAL, which is what a run is, and then every term the
|
|
607
|
-
decision was made from. Printed on any pass that does something — a session
|
|
608
|
-
where nothing changes says nothing.
|
|
609
|
-
|
|
610
|
-
The terms are there because an interval says WHAT was decided and only these say
|
|
611
|
-
WHY. A decision of this plan is
|
|
612
|
-
|
|
613
|
-
delay + (index - at) / rate + madeBetween * refetch against a deadline
|
|
614
|
-
|
|
615
|
-
so a recorded decision without the rate can be re-read and not recomputed. That
|
|
616
|
-
is not hypothetical: the one-piece intervals of 2026-09-08 were diagnosed by
|
|
617
|
-
substituting the rate from the speeds the session reported elsewhere — six
|
|
618
|
-
different figures, none of which reproduced the answer the plan had given.
|
|
619
|
-
|
|
620
|
-
A zero in `firstByte`, `kill` or `refetch` is a measurement nobody has taken, not
|
|
621
|
-
a free operation. It is printed so that reading it as free is a choice.
|
|
622
|
-
|
|
623
|
-
It was missing, and its absence cost three wrong diagnoses of one field session.
|
|
624
|
-
The line printed the windows, the budget and where the live runs stood; the
|
|
625
|
-
intervals the actions carried were the one thing it did not print. What that
|
|
626
|
-
session actually did was give every encoder an interval of exactly ONE segment —
|
|
627
|
-
63 runs, each spending 1.26 s reaching its first piece, making that one piece,
|
|
628
|
-
reaching the end of its interval and exiting, twelve of them normally. The
|
|
629
|
-
reasons printed beside them read as moves back and forth, so the fault was read
|
|
630
|
-
as an oscillating placement three times over. An interval of one segment turns
|
|
631
|
-
the protection against two encoders writing one name into a mill for processes.
|
|
632
|
-
|
|
633
|
-
## What proves a segment is finished
|
|
634
|
-
|
|
635
|
-
Its NAME, and there is nothing else. A piece being written is called
|
|
636
|
-
`making-<from>-00042.mp4` — the tag is the first number of the stretch its run
|
|
637
|
-
was given — and it takes `segment-00042.mp4` when the encoder says it has closed
|
|
638
|
-
it, which it does on a channel of its own (`-segment_list pipe:3`). Making it
|
|
639
|
-
servable is therefore one rename inside one directory, performed by the store
|
|
640
|
-
because the store owns the disk. The `hls` branch needs nothing extra: its muxer
|
|
641
|
-
writes through a temporary name of its own, so its files appear under their final
|
|
642
|
-
name whole.
|
|
643
|
-
|
|
644
|
-
Three things follow, and each replaced a guess:
|
|
645
|
-
|
|
646
|
-
1. **a request can never reach a half-written piece.** Closure used to be
|
|
647
|
-
inferred from the NEXT number existing — sound for one writer walking forward,
|
|
648
|
-
false the moment two runs share an output, which is what the plan gives an
|
|
649
|
-
output whenever it places a second encoder. Field 2026-09-08:
|
|
650
|
-
`segment-00057.mp4` served at 2 268 361 bytes and then at 4 510 940, exactly
|
|
651
|
-
half; the browser appended the half and refused the whole for the rest of the
|
|
652
|
-
session, with the picture frozen at 319.66 s;
|
|
653
|
-
2. **the last piece of a run is provable.** Under the successor rule nothing
|
|
654
|
-
followed it, so it never was — the resume case that held one segment for 46 s
|
|
655
|
-
and then answered 404;
|
|
656
|
-
3. **clearing up after a dead run is a name match.** Its unfinished pieces are
|
|
657
|
-
the ones carrying its own tag: no stretch to search, no bytes to judge, and no
|
|
658
|
-
way to remove a complete piece somebody else closed. `services/encode/
|
|
659
|
-
open-piece.js` did all three of those by guessing and is gone, along with the
|
|
660
|
-
session manager's copy of it.
|
|
661
|
-
|
|
662
|
-
## How a request for a segment ends
|
|
663
|
-
|
|
664
|
-
A request steers no encoder. The file is served if the store holds it whole;
|
|
665
|
-
otherwise the request waits for the store's own announcement that the segment
|
|
666
|
-
was published (`SegmentStore.waitFor`), for the waits of that output to be
|
|
667
|
-
invalidated (a viewer left the rung), or for the requester to go. How long it
|
|
668
|
-
may wait is the requester's: the page states it (`X-Hold-Ms`, its own deadline
|
|
669
|
-
less the round trip it has measured) and the route answers "retry" by then. A
|
|
670
|
-
requester that states nothing is held until it closes the connection. There is
|
|
671
|
-
no poll and no hold time chosen by the proxy.
|
|
672
|
-
|
|
673
|
-
Which output a request is answered by is decided in the encoding component
|
|
674
|
-
(`encode/OutputOpening.js`), without knowing who asked; the server operation
|
|
675
|
-
(`server/ViewerRequests.js`) places the requester on it.
|
|
676
|
-
|
|
677
|
-
## What is checked
|
|
678
|
-
|
|
679
|
-
`test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
|
|
680
|
-
no encoder stopped for being unwatched, the settle machinery absent, each output
|
|
681
|
-
reading its own map, and the soundtrack's start instant read off the table
|
|
682
|
-
rather than handed in.
|
|
683
|
-
|
|
684
|
-
`test/priority-map-per-output.test.js` holds the two scopes, over the real
|
|
685
|
-
viewer registry, the real `OutputCatalog` and the real `PriorityOrchestrator`.
|
|
686
|
-
|
|
687
|
-
`test/encode-plan.test.js` holds the arithmetic, including that every encoder
|
|
688
|
-
stops when nobody is watching the output.
|
|
689
|
-
|
|
690
|
-
`test/segment-store.test.js` holds the naming rule: a piece under its served
|
|
691
|
-
name is finished — the last one of a run included — one under a working name is
|
|
692
|
-
not and cannot be reached, closing it is one rename, and clearing up after one
|
|
693
|
-
run leaves every other run's work alone.
|
|
694
|
-
|
|
695
|
-
## Per-machine calibration and admission (roadmap item 97, step 14)
|
|
696
|
-
|
|
697
|
-
At startup, `encode/calibration.js` qualifies every mode the proxy may select,
|
|
698
|
-
including the software encoder kept for runtime fallback. Each mode uses the
|
|
699
|
-
production arguments on a generated test clip, and every segment must decode
|
|
700
|
-
independently. `encode/fingerprint.js` keys the results to the FFmpeg and x264
|
|
701
|
-
versions, CPU model and thread count for software, or FFmpeg version, device
|
|
702
|
-
model and driver for hardware. A changed key does not reuse measurements from
|
|
703
|
-
the previous configuration.
|
|
704
|
-
|
|
705
|
-
`encode/throughput.js` measures qualified modes at five frame sizes from
|
|
706
|
-
256×144 through 3840×2160. Between measured sizes it interpolates and subtracts
|
|
707
|
-
the measured interpolation error; it does not offer a size outside the measured
|
|
708
|
-
range. Calibration stops measuring slower modes and larger sizes once a faster
|
|
709
|
-
mode cannot sustain realtime. If a detected device has no qualified mode, the
|
|
710
|
-
proxy uses software encoding.
|
|
711
|
-
|
|
712
|
-
`encode/LocalObservations.js` stores observations from admitted encodes in the
|
|
713
|
-
proxy's `local-observations.json`. It keeps speed without competing encodes,
|
|
714
|
-
average and peak segment rates, output preparation time and the viewer buffer at
|
|
715
|
-
a transition. A matching fingerprint is required. These observations refine
|
|
716
|
-
output cost, the peak estimate of an unbounded hardware encoder and transition
|
|
717
|
-
preparation timing; an empty or stale file leaves the base policy in force.
|
|
718
|
-
|
|
719
|
-
`encode/EncodeAdmission.js` counts running encoders and outputs that viewers are
|
|
720
|
-
being placed on or prepared to use. Opening an output checks capacity and
|
|
721
|
-
records the viewer in one synchronous call through `OutputOpening` and
|
|
722
|
-
`ViewerRequests`; a second simultaneous opening sees the first reservation. If
|
|
723
|
-
no measured mode or remaining capacity is available, the proxy answers 409
|
|
724
|
-
`no-capacity` before playback. Unknown costs on occupied outputs count as
|
|
725
|
-
unknown capacity, not free capacity. The health report exposes safe encoding
|
|
726
|
-
headroom, and the pool's file-specific answer uses the same occupied-cost data.
|
|
727
|
-
If the opening is refused, the page asks the pool and prepares the file again
|
|
728
|
-
through an eligible proxy before showing video. Each refused proxy is excluded
|
|
729
|
-
for that opening attempt, so stale pool answers cannot make the page cycle.
|
|
1
|
+
# Encode architecture — who decides where encoders go
|
|
2
|
+
|
|
3
|
+
One question, one answer, and the answer is arithmetic.
|
|
4
|
+
|
|
5
|
+
> Viewers are always independent and always reuse what can be reused. The
|
|
6
|
+
> number of encoders is however many are needed; how many are needed follows
|
|
7
|
+
> from which sets of output parameters are wanted and where the viewers stand
|
|
8
|
+
> inside each. Segments produced by ANY encoder are available to ANY viewer, and
|
|
9
|
+
> which viewer asked never enters the question.
|
|
10
|
+
|
|
11
|
+
## The one authority
|
|
12
|
+
|
|
13
|
+
`services/encode/EncodePlan.js` decides. Nothing else places an encoder,
|
|
14
|
+
nothing else takes one away for scheduling reasons, and there is exactly one
|
|
15
|
+
call to `#startEncodeRun` in the whole proxy — the one the plan asks through.
|
|
16
|
+
|
|
17
|
+
It decides from four things and no others:
|
|
18
|
+
|
|
19
|
+
| what | where it comes from |
|
|
20
|
+
|---|---|
|
|
21
|
+
| what is already made | the segment store, asked afresh before every decision |
|
|
22
|
+
| what is being made | the coverage map's live claims |
|
|
23
|
+
| what is wanted | the priority map of that output |
|
|
24
|
+
| how many the machine can hold | `run-budget.js`, from measurements of this host |
|
|
25
|
+
|
|
26
|
+
**No viewer reaches the decision.** `EncodePlan` and `EncodeOrchestrator` do not
|
|
27
|
+
import the viewer layer, name a consumer id or hold a person. What crosses is a
|
|
28
|
+
priority map: zones of segment numbers with a rank and a real time, and nobody's
|
|
29
|
+
name on it.
|
|
30
|
+
|
|
31
|
+
**The rest of `services/encode/` names people, and holds none.** A step and a
|
|
32
|
+
soundtrack are chosen, warmed and left per person, so `Renditions`,
|
|
33
|
+
`QualityController` and `CushionReport` say WHOSE — by consumer id, which is a
|
|
34
|
+
name and not an object. What they may ask about that name and what they may
|
|
35
|
+
tell the viewer layer about it is `services/viewer/choices.js`: the mode a size
|
|
36
|
+
was picked in, what a link measured, the soundtrack chosen, the step on screen,
|
|
37
|
+
what is being warmed, who is present, and the standing quality ask. Values in,
|
|
38
|
+
values out. No `Viewer` crosses the boundary and nothing outside the viewer
|
|
39
|
+
layer writes a viewer's record — which is the rule a position cost 77 encoder
|
|
40
|
+
starts for breaking (field 2026-09-13).
|
|
41
|
+
|
|
42
|
+
This paragraph used to claim the whole directory named nobody, which was never
|
|
43
|
+
true of `Renditions` and could not be: which soundtrack to make is a fact about
|
|
44
|
+
a person. What matters is that it is a NAME, so the viewer's record stays in
|
|
45
|
+
one place with one writer.
|
|
46
|
+
|
|
47
|
+
**What answered a request is the viewer's, per viewing** (roadmap item 97,
|
|
48
|
+
step 9). The page stamps every request with the GENERATION of its viewing — a
|
|
49
|
+
number it raises on the viewer's own seek, before it sends anything — and the
|
|
50
|
+
proxy reads it rather than counting one, so a request made before a seek and
|
|
51
|
+
delivered after it still says which viewing it belongs to. Three things follow,
|
|
52
|
+
and each is asked through `choices.js`, by name:
|
|
53
|
+
|
|
54
|
+
1. every file route asks FIRST whether that viewer still takes requests of that
|
|
55
|
+
generation, before a step or a soundtrack is resolved — resolving can create
|
|
56
|
+
an output and registers the viewer on it, which a request of a viewing
|
|
57
|
+
already left must not do;
|
|
58
|
+
2. a repeat of a request within one viewing is answered by the output that
|
|
59
|
+
answered it: the step route records the height asked for and the segment
|
|
60
|
+
(the init as −1), the picture's own route records under the height that
|
|
61
|
+
output is named after. A soundtrack records nothing of this kind;
|
|
62
|
+
3. a response holds its output from its first byte to its end, and no output is
|
|
63
|
+
disposed while an assignment holds it — except on a hardware encoder's
|
|
64
|
+
failure, where nothing can go on making what it made. The idle expiry asks
|
|
65
|
+
only the assignments, not whether anybody is registered, so a paused
|
|
66
|
+
viewer's claim on an output stays a separate decision.
|
|
67
|
+
|
|
68
|
+
**Which output serves a viewer is one rule, asked by every path, and the
|
|
69
|
+
choice is the viewer's own** (step 11). A request for a piece is answered, in
|
|
70
|
+
order, by what already answered that address in this viewing (`given`), by the
|
|
71
|
+
output chosen for this viewer at that height (`chosen`), and only then by the
|
|
72
|
+
rule — and a repeat whose output has gone is served from its stored piece or by
|
|
73
|
+
an output with a proven-compatible header, or answered `assignment-lost`. The
|
|
74
|
+
rule (`serving-output.js`, `outputSuits`) asks the picture's material, the size
|
|
75
|
+
a viewer who picked by hand requires, and what the viewer's OWN link does with
|
|
76
|
+
the output's whole load: the picture and the soundtrack they chose, each part
|
|
77
|
+
`known` (a limit), `estimated` (a stated average) or `unknown` (no bound), the
|
|
78
|
+
load as trustworthy as its worst part (`link-budget.js`). `unknown` is refused
|
|
79
|
+
against a measured link; `estimated` is admitted by estimate and reported to
|
|
80
|
+
the page as not confirmed; a bound is taken before an average. When nothing
|
|
81
|
+
suits, nothing is handed over: the answer is `output-unavailable`, with the
|
|
82
|
+
figures, and the page explains it. The shared `OutputCatalog.servedBy` record
|
|
83
|
+
and the worst link over all viewers are gone; the quality budget asks each
|
|
84
|
+
viewer's link for that viewer alone.
|
|
85
|
+
|
|
86
|
+
**A bitrate limit is part of an output, not a figure moved under it** (step
|
|
87
|
+
10). A software encode's `-maxrate`/`-bufsize` are named by its key
|
|
88
|
+
(`vbv=<maxrate>k-<bufsize>k@<level>`), so two limits are two outputs and a
|
|
89
|
+
slow viewer's limit can no longer land on every viewer of a picture — which is
|
|
90
|
+
what it did while it was a field of the output set by the worst of their links.
|
|
91
|
+
The level is pinned (`-level:v`) to the level of the NOMINAL output of that
|
|
92
|
+
size and frame rate, so every limit at one size writes the same header
|
|
93
|
+
and a viewer can be moved between them under the address they already play.
|
|
94
|
+
A limit above the nominal one is refused. Hardware encoders are given no limit
|
|
95
|
+
and state none (`vbv=-`). Whether the pinned level is the one x264 would choose
|
|
96
|
+
for the nominal output is checked before a release
|
|
97
|
+
(`stand/segment-compat/level-check.mjs`).
|
|
98
|
+
|
|
99
|
+
**Which limits a frame has is decided by its AREA** (step 14, decided with the
|
|
100
|
+
user 2026-09-24). The rows of limits are the ladder's frames at 16:9, sized by
|
|
101
|
+
the same function that sizes an encode; a frame takes the row nearest by the
|
|
102
|
+
ratio of areas, the larger row on the exact border, and a row with no nominal
|
|
103
|
+
of its own borrows the nearest row that has one (`limitRowFor`,
|
|
104
|
+
`nominalKbpsFor`). Every place that picks a row — opening an output, the move
|
|
105
|
+
between limits, the offer's price of a step and the link rule — is handed the
|
|
106
|
+
whole frame, so all of them name the same row. By height alone a 1920x800 film
|
|
107
|
+
fell in the 720 row although it is nearer the 1080 one. The proposed measured
|
|
108
|
+
table of lower limits was dropped on 2026-09-27. Step 14 now qualifies this
|
|
109
|
+
machine's encoder modes and does not set lower bitrate limits.
|
|
110
|
+
|
|
111
|
+
## Where a viewer is, and how it reaches the encoders
|
|
112
|
+
|
|
113
|
+
A viewer's position has ONE writer — the viewer — and is a function of time:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
position(now) = stated + (playing ? 1 : 0) × (now − statedAt)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A viewer whose picture moves covers a second of film every second, so between
|
|
120
|
+
the moments they speak their position is known exactly; one whose picture is
|
|
121
|
+
stopped covers nothing. Nothing else writes it, and in particular a request for
|
|
122
|
+
a segment does not: a request says the viewer is still here, and the segment
|
|
123
|
+
either exists and is served or does not and is waited for.
|
|
124
|
+
|
|
125
|
+
Held as a stored number instead it was a staircase, flat for the ten seconds
|
|
126
|
+
between reports and then a jump — and with two writers filling it in turn the
|
|
127
|
+
steps went backwards as often as forwards. Field 2026-09-13, one output over one
|
|
128
|
+
second: `p100:#9..#10`, `p100:#18..#20`, `p100:#16..#18`, `p100:#18..#20`, while
|
|
129
|
+
the second viewer sat frozen at 1673.6 s. The placement follows the map, so the
|
|
130
|
+
machine spent six minutes on 77 encoder starts and 141 stops.
|
|
131
|
+
|
|
132
|
+
PRESENCE IS A SEPARATE FACT and it is the connection: a viewer is here until
|
|
133
|
+
something says otherwise. Silence never says it — a pause, a hidden tab whose
|
|
134
|
+
timers are throttled, and a full cushion are all silent and all still watching.
|
|
135
|
+
|
|
136
|
+
## Viewers become a map, and that is the whole crossing
|
|
137
|
+
|
|
138
|
+
```mermaid
|
|
139
|
+
flowchart TB
|
|
140
|
+
subgraph V["services/viewer — where people are"]
|
|
141
|
+
VW[Viewer<br/>position as a function of time, playing, buffered, chosen track]
|
|
142
|
+
VS[Viewers<br/>the relation, indexed from both ends]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
subgraph P["services/priority — one fact, two scopes"]
|
|
146
|
+
PM[PriorityMap<br/>seconds of film to a rank]
|
|
147
|
+
PO[PriorityOrchestrator<br/>merge, publish, keep]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
subgraph E["services/encode — where encoders go"]
|
|
151
|
+
CM[CoverageMap<br/>ready / making / free]
|
|
152
|
+
EP[EncodePlan<br/>argmin over arrangements]
|
|
153
|
+
RB[run-budget<br/>what this host can hold]
|
|
154
|
+
ER[EncodeRun<br/>one process, one interval]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
subgraph D["services/torrent/demand + torrent/download — what the swarm is told"]
|
|
158
|
+
DR[DemandRegister]
|
|
159
|
+
SS[SwarmSelection]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
VW --> VS
|
|
163
|
+
VS -->|"positions, presence"| PO
|
|
164
|
+
PM --> PO
|
|
165
|
+
PO -->|"per FILE, in bytes"| DR
|
|
166
|
+
DR --> SS
|
|
167
|
+
PO -->|"per OUTPUT, in segments"| EO[EncodeOrchestrator]
|
|
168
|
+
EO --> EP
|
|
169
|
+
CM --> EP
|
|
170
|
+
RB --> EP
|
|
171
|
+
EP -->|"start / stop / move"| ER
|
|
172
|
+
ST[(SegmentStore<br/>the disk)] -->|"what is ready"| CM
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Two scopes of one map, and both are right
|
|
176
|
+
|
|
177
|
+
The map is built from where the viewers are, once, and read at two scopes.
|
|
178
|
+
|
|
179
|
+
**Per FILE, for the swarm.** The picture, a quality step and a soundtrack of one
|
|
180
|
+
film read the same bytes, so every viewer of any of them wants that file's
|
|
181
|
+
bytes. This is what `PriorityOrchestrator.mapFor(sourceKey, fileIndex)`
|
|
182
|
+
answers, and what is published to the download layer.
|
|
183
|
+
|
|
184
|
+
**Per OUTPUT, for the encoders.** A person watching 480p wants nothing of the
|
|
185
|
+
1080p output at all. This is `mapForOutput(address)`.
|
|
186
|
+
|
|
187
|
+
One map for both was the second authority over encoders. Handed the film's map,
|
|
188
|
+
the plan wanted an encoder on every output of the film; what actually stopped
|
|
189
|
+
the ones nobody was watching was the session manager killing them by its own
|
|
190
|
+
judgement — and since a viewer moving between steps announces itself, the plan
|
|
191
|
+
started them again on the very next pass. Two parties answering "should this
|
|
192
|
+
encoder exist" by different rules, several times a second.
|
|
193
|
+
|
|
194
|
+
An output nobody is on gets a map with **nothing in it**, which is a statement
|
|
195
|
+
and not an absence: the walk writes one for every output a session exists for,
|
|
196
|
+
including the ones everybody has left. That is how the plan is told to stop what
|
|
197
|
+
is on it.
|
|
198
|
+
|
|
199
|
+
## Which output a person is consuming
|
|
200
|
+
|
|
201
|
+
A person holds a record on more outputs than they are consuming. The picture is
|
|
202
|
+
where their record lives — the browser addresses it, their chosen soundtrack is
|
|
203
|
+
written on it, their position is read from it — so they are never let go of it;
|
|
204
|
+
but the moment they step down to 480p, the 1080p output is producing for nobody.
|
|
205
|
+
|
|
206
|
+
That distinction is answered where the two facts meet, and neither layer is
|
|
207
|
+
handed the other:
|
|
208
|
+
|
|
209
|
+
- **which step is on their screen** is a fact about a PERSON, read off the
|
|
210
|
+
viewer as one field. The page STATES it (`playingHeight` on the viewer's
|
|
211
|
+
report, sent the moment hls.js has switched), and `Renditions.viewerPlays`
|
|
212
|
+
records it. It used to be inferred from the first segment requested of a
|
|
213
|
+
rung, which is the player fetching rather than the picture having moved;
|
|
214
|
+
- **which output a step supersedes** is a fact about the FILM'S SHAPE, answered
|
|
215
|
+
by `OutputCatalog.supersededBy(session, stepOnScreen)`, which takes a plain id
|
|
216
|
+
and has never seen a viewer.
|
|
217
|
+
|
|
218
|
+
A step, a soundtrack, and a step being warmed are consumed by whoever is
|
|
219
|
+
registered on them — everywhere but the picture, a person who stops watching is
|
|
220
|
+
let go of, so being known to an output is consuming it. Whether an output is
|
|
221
|
+
wanted at all is asked of the viewer registry and nothing else: an output a
|
|
222
|
+
picture made on its own behalf once carried a made-up "family" claim that kept
|
|
223
|
+
it alive, which was a second answer to the same question and is gone. Every
|
|
224
|
+
viewer has a name; a request that names nobody makes no viewer. Through a warm-up both
|
|
225
|
+
the step on screen and the step being made ready are genuinely produced, which
|
|
226
|
+
is the price of the switch not being visible.
|
|
227
|
+
|
|
228
|
+
## A move between two limits of one height
|
|
229
|
+
|
|
230
|
+
A bitrate limit is part of an output, so a viewer whose link cannot carry the
|
|
231
|
+
limit on their screen is given ANOTHER output of the same height at a lower
|
|
232
|
+
limit — for them alone, and without their player being told (roadmap item 97,
|
|
233
|
+
step 12). The address `v/<height>/…` does not change; what changes is which
|
|
234
|
+
output this side answers it with.
|
|
235
|
+
|
|
236
|
+
1. **Prepared, not switched.** `Renditions.prepareSameHeightSwitch` opens (or
|
|
237
|
+
finds, by its key) the output of the same size at the next limit of
|
|
238
|
+
`limitsFor(height)` that the viewer's link admits, marks it a step of the
|
|
239
|
+
picture, and registers the viewer on it — that is what buys it an encoder.
|
|
240
|
+
The choice (`Assignments.choose`) is not touched: until the move is made they
|
|
241
|
+
are given what they were given. The move being prepared is a field of the
|
|
242
|
+
viewer, `Viewer.sameHeightSwitch`, and not `warmingVariantId`, which is a
|
|
243
|
+
step their player will switch to itself.
|
|
244
|
+
2. **Made on the store's own event.** `SegmentStore.onPublished` is filtered by
|
|
245
|
+
the prepared output's key. The move is made when the segment the viewer will
|
|
246
|
+
ask for NEXT is closed on it: one past the highest they were given in the
|
|
247
|
+
viewing they are in now, or — with nothing given in it, straight after a
|
|
248
|
+
seek — the one where they stand. It is read at the moment of the event, so a
|
|
249
|
+
segment closed behind that one, or an event that arrives after a seek, moves
|
|
250
|
+
nothing.
|
|
251
|
+
3. **One synchronous stretch.** The choice under each height it was chosen
|
|
252
|
+
under, the verdict, the step on screen, the registration on the new output
|
|
253
|
+
and off the old one change with no `await` between them.
|
|
254
|
+
4. **The output left is not disposed.** The picture is never left. A step that
|
|
255
|
+
is left loses the viewer, the plan stops its encoder by its empty map, and the
|
|
256
|
+
idle expiry removes it once no assignment holds it; a repeat of an address it
|
|
257
|
+
answered is still answered by it, because `given` is read before `chosen`.
|
|
258
|
+
5. **Cancelled when not wanted.** When the link carries the output on screen
|
|
259
|
+
again, when the prepared output no longer suits, when the player moves to
|
|
260
|
+
another rung, when encoding the prepared output has failed for good, or when
|
|
261
|
+
nothing of the film is left to give from it: the viewer stops watching the
|
|
262
|
+
prepared output and nothing else changes for anybody.
|
|
263
|
+
6. **Asked again on three events, never on a timer.** A segment closed on the
|
|
264
|
+
prepared output (`SegmentStore.onPublished`); the viewer's own report
|
|
265
|
+
(`Renditions.noteViewerReported`, from the net-report route), which is what
|
|
266
|
+
carries a change of their link; and encoding the prepared output failing for
|
|
267
|
+
good (`Renditions.noteProductionFailed`, told by `EncodeRuns`). Without the
|
|
268
|
+
last two, a move onto an output that closes nothing stood for ever, and the
|
|
269
|
+
quality budget — which leaves a viewer with a move pending alone — never
|
|
270
|
+
decided for them again. Each event may also make the move, on the same one
|
|
271
|
+
condition: the segment they will ask for next is closed. A move is not
|
|
272
|
+
prepared onto an output whose encoding has failed. The fourth way a move can
|
|
273
|
+
wait — no encoder placed on the output — does not arise until the admission
|
|
274
|
+
of step 13 can refuse, and that refusal will be its event.
|
|
275
|
+
|
|
276
|
+
The quality budget uses this lever first, in both directions: down to the
|
|
277
|
+
highest lower limit the link admits, up one limit at a time, and another height
|
|
278
|
+
only where the height on screen has no limit left to move to. A copy has no
|
|
279
|
+
limit and a hardware encode is given none, so for them the lever is still the
|
|
280
|
+
height. The measured table of lower bitrate limits was dropped on 2026-09-27;
|
|
281
|
+
the product currently supplies only the nominal limit through `limitsFor`.
|
|
282
|
+
Per-machine calibration (step 14) qualifies encoder modes and does not create
|
|
283
|
+
lower bitrate limits, so same-height bitrate moves have no target in the current
|
|
284
|
+
product configuration.
|
|
285
|
+
|
|
286
|
+
7. **A move up waits for the cushion; a move down does not** (roadmap item
|
|
287
|
+
98). Once the piece asked for next is closed, a move UP is made only when
|
|
288
|
+
the viewer holds the cushion this file needs (`minimumBufferSeconds`), and
|
|
289
|
+
their next report asks again until they do. A move DOWN was started because
|
|
290
|
+
their buffer would run dry, so it is made the moment the piece is ready.
|
|
291
|
+
|
|
292
|
+
## When the quality budget moves a viewer (roadmap item 98)
|
|
293
|
+
|
|
294
|
+
The page has no quality control: the quality is always automatic. What moves a
|
|
295
|
+
viewer is decided here, per viewer, and each part of it is a measurement.
|
|
296
|
+
|
|
297
|
+
1. **Judged on the viewer's report, not on a timer.** `QualityController.noteViewerReported`
|
|
298
|
+
runs from the net-report route. The chosen fifteen-second window of a slow
|
|
299
|
+
link, the ten-second buffer threshold and the thirty-second wait after every
|
|
300
|
+
action are gone.
|
|
301
|
+
2. **Down when the buffer would run dry first.** Their link does not carry the
|
|
302
|
+
stream they are given AND, on the buffer's trend, it ends sooner than another
|
|
303
|
+
output could close the piece they need, counting the time until their next
|
|
304
|
+
report (`quality/drain-threat.js`). The trend is a least-squares fit over the
|
|
305
|
+
shortest run of their reports spanning one segment
|
|
306
|
+
(`viewer/buffer-trend.js`), because the buffer rises a segment at a time and
|
|
307
|
+
two readings catch only the rise or the fall. The time to readiness is this
|
|
308
|
+
host's measured first-segment time. The levers: a lower limit of the same
|
|
309
|
+
height, then a lower height asked of the player as URGENT — their page
|
|
310
|
+
switches as soon as the rung is ready, without waiting for a cushion. Where
|
|
311
|
+
neither can be prepared they stay on what they are given, with no message.
|
|
312
|
+
3. **The picture the viewer sees bounds a re-encode.** The page sends the
|
|
313
|
+
frame it would show without enlarging, in physical pixels; it is kept on
|
|
314
|
+
`Viewer.visiblePicture`. The bound is the smallest rung of the source's
|
|
315
|
+
ladder whose frame is not smaller (`quality/visible-rung.js`). It sets the
|
|
316
|
+
box an output is opened at (`OutputOpening`), the ceiling of every step up,
|
|
317
|
+
and the rung a step down prefers. A copy of the source is never re-encoded
|
|
318
|
+
for being taller than that.
|
|
319
|
+
4. **A smaller picture moves the viewer only onto a READY rung**
|
|
320
|
+
(`Renditions.heightReadyFor`: the piece they ask for next closed on it);
|
|
321
|
+
with none ready, the next judgement of their link or of the machine applies
|
|
322
|
+
the bound. **A larger picture** asks one rung up when the machine and their
|
|
323
|
+
link have room.
|
|
324
|
+
5. **A step up is let go when its conditions go back**: a draining buffer, a
|
|
325
|
+
link that no longer carries the stream, or no room for the output. The page
|
|
326
|
+
drops the move it was preparing when the proxy stops asking for it.
|
|
327
|
+
6. **The page switches a variant when the rung is ready**, and for a request
|
|
328
|
+
that is not urgent only once it holds `minimumBufferSeconds`.
|
|
329
|
+
|
|
330
|
+
## A place on the machine
|
|
331
|
+
|
|
332
|
+
How many encoders ONE output may run is answered per output
|
|
333
|
+
(`EncodeRuns.maxRunsForOutput`) and is always at least one. What refuses the
|
|
334
|
+
encoder that would slow everybody is `EncodeAdmission`, across every output at
|
|
335
|
+
once (roadmap item 97, step 13).
|
|
336
|
+
|
|
337
|
+
1. **The unit is seconds of work per second of film**, the one the quality
|
|
338
|
+
offer judges steps in (`EncodeCost.loadOfOutput`). Costs add, and a set of
|
|
339
|
+
encoders is affordable when the machine, corrected for the share of it
|
|
340
|
+
nobody has priced, still makes a second of film per second. A soundtrack is
|
|
341
|
+
a small fraction of a picture; counting processes would refuse a viewer
|
|
342
|
+
their sound. On the addon host the sum reproduces the measurement: two 1080p
|
|
343
|
+
encodes at 1.96x alone cost 1.02 s/s and make 0.98x, measured 0.99x and
|
|
344
|
+
0.98x.
|
|
345
|
+
2. **A place is held by an OUTPUT**: by every live encoder at its output's
|
|
346
|
+
cost, and by every output a present viewer is being prepared onto — a step
|
|
347
|
+
being warmed or a move to another limit — once however many wait for it.
|
|
348
|
+
The second is read off the viewers' own records (`Viewers.outputsBeingPrepared`),
|
|
349
|
+
so a place ends with its record on whichever path that record is cleared,
|
|
350
|
+
and there is no register of its own to keep in step.
|
|
351
|
+
3. **Asked before a preparation is recorded**, in the same synchronous stretch
|
|
352
|
+
as the record: two preparations onto two different outputs with room for
|
|
353
|
+
one cannot both be admitted, because the second is asked after the first is
|
|
354
|
+
written. Refused, a move answers `noPlace` and the budget goes to a lower
|
|
355
|
+
height (never a higher one); a warm-up answers `output-unavailable`.
|
|
356
|
+
4. **The plan is bounded by it** (`EncodeOrchestrator.#withinMachine`): an
|
|
357
|
+
output may run no more encoders than still fit, and a new one on a full
|
|
358
|
+
machine gets none until one elsewhere ends.
|
|
359
|
+
5. **An admitted encoder is never taken away**, and a place promised to a
|
|
360
|
+
preparation is kept: the machine's answer is never below what already runs
|
|
361
|
+
on an output, nor below one for an output a preparation holds.
|
|
362
|
+
|
|
363
|
+
Not solved: places go to whoever asks first, so a second encoder catching up on
|
|
364
|
+
one output can hold the place a viewer opening another film waits for.
|
|
365
|
+
|
|
366
|
+
## What each of the eight other places used to do
|
|
367
|
+
|
|
368
|
+
Before 2026-09-08 the plan was one opinion among nine. Each of these placed or
|
|
369
|
+
killed encoders by a rule of its own; each now states the fact it knows.
|
|
370
|
+
|
|
371
|
+
| it knows | it used to do | it now says |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| a session was created | start a run at the viewer's position, worked out again | the viewer is placed; the plan reads that |
|
|
374
|
+
| a viewer joined further in | start a run there if nothing was being made | as above |
|
|
375
|
+
| a step or soundtrack is being warmed | point that session at the switch and start it | this person is at N seconds on it |
|
|
376
|
+
| a step was switched to | stop the one left, point the new one | this person is on this step now |
|
|
377
|
+
| a step or track was abandoned | stop its encoder | this person has left that output |
|
|
378
|
+
| the hardware encoder failed | start a run at the dead one's start | what this host encodes with has changed |
|
|
379
|
+
| the input came back | start a run at the last requested segment | decide again |
|
|
380
|
+
| the cut table was corrected | restart the members at the measured instant | this run is producing in the wrong place; the instant is in the file's table |
|
|
381
|
+
| the bitrate cap changed | restart at where the encoder had got to | nothing: a limit is part of an output since 2.87.0, so another limit is another output and no running encoder is ever told a new one |
|
|
382
|
+
|
|
383
|
+
A **seek** is not in that list because it was already reduced to one thing: it
|
|
384
|
+
puts the viewer where they are. The settle timer behind it, its cooldown and its
|
|
385
|
+
one-segment backoff are gone — a second debounce on a signal the browser had
|
|
386
|
+
already debounced, and every millisecond of it was dead time in front of the
|
|
387
|
+
viewer.
|
|
388
|
+
|
|
389
|
+
## Whether the map is being served in its own order
|
|
390
|
+
|
|
391
|
+
The zones say what matters most. What the viewer actually waited for is measured
|
|
392
|
+
where a viewer measurably waits — the one place in the proxy that holds a
|
|
393
|
+
request for a named segment — and recorded against the rank the map gave that
|
|
394
|
+
segment **at the moment it was asked for**. It reaches the `encode:` state line
|
|
395
|
+
as `served[...]`:
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
served[now 42 wait(s) median 180ms worst 1900ms, soon 6 wait(s) median 90ms worst 240ms, later none]
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Read it like this:
|
|
402
|
+
|
|
403
|
+
| what it says | what is wrong |
|
|
404
|
+
|---|---|
|
|
405
|
+
| long waits at `now` | the urgent zone is not being served first — a fault in whoever acts on the map |
|
|
406
|
+
| long waits at `soon`/`later`, none at `now` | the zones are the wrong width: the urgent one too narrow, so the viewer reaches material only ranked "soon" |
|
|
407
|
+
| `now none` while the viewer is watching | nothing was ever urgent — the map is not reaching this output |
|
|
408
|
+
| a band reading `none` | silence, not a zero, and it is printed as `none` so it cannot be read as "no waits, all good" |
|
|
409
|
+
|
|
410
|
+
Ranks are collapsed into three bands because the map's own scale is as long as
|
|
411
|
+
the film needs — a hundred ranks on a long file — and a hundred-row table says
|
|
412
|
+
nothing a reader can hold. The width of `soon` is a tenth of the top rank, which
|
|
413
|
+
is the map's own shape (its zones widen geometrically) rather than a threshold
|
|
414
|
+
chosen for the table.
|
|
415
|
+
|
|
416
|
+
The download half is measured the same way and on the same scale, so the two are
|
|
417
|
+
comparable: `download-architecture.md`.
|
|
418
|
+
|
|
419
|
+
## What the master playlist declares, and why it is not cosmetic
|
|
420
|
+
|
|
421
|
+
`BANDWIDTH` and `AVERAGE-BANDWIDTH` per variant, both measured
|
|
422
|
+
(`services/encode/output/rates.js`):
|
|
423
|
+
|
|
424
|
+
- the average is the file's own length over its duration, exact and known when
|
|
425
|
+
the session is created;
|
|
426
|
+
- the peak is the biggest piece produced over the span it covers, and equals the
|
|
427
|
+
average until a piece exists — a ratio invented meanwhile would be the
|
|
428
|
+
fabrication this replaced;
|
|
429
|
+
- a re-encoded height is declared at the cap we impose, which is exact;
|
|
430
|
+
- a smaller height at the pixel share of the source's rate, which errs high, and
|
|
431
|
+
high is the safe direction.
|
|
432
|
+
|
|
433
|
+
It used to be `height * height * 3.2`. **The browser sizes its cushion in BYTES
|
|
434
|
+
from `BANDWIDTH`**, so a figure five times low makes the cushion five times
|
|
435
|
+
shallow: field 2026-09-08, 3.73 Mbit/s declared for a file carrying 18.4, 120 s
|
|
436
|
+
asked bought 56 MB — 26 s of that film — and the deepest the browser ever held
|
|
437
|
+
was 17.1 s. Inflating it is not the answer either: hls.js compares it against
|
|
438
|
+
its own estimate of the link to decide a level is unplayable, and its recovery
|
|
439
|
+
then moves level by itself, which does not honour our pinning.
|
|
440
|
+
|
|
441
|
+
## The objective is the map's own rank order
|
|
442
|
+
|
|
443
|
+
Not three terms in seconds. One pair per rank the map states, most urgent rank
|
|
444
|
+
first, compared position by position:
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
[ late(100), done(100), late(99), done(99), … late(1), done(1) ], encoders, wasted
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`late(r)` is how long anybody waits past a deadline at rank `r`; `done(r)` is
|
|
451
|
+
when the last number of that rank is made. A difference at a higher rank settles
|
|
452
|
+
it and nothing lower can reopen it — which is the stated order: nobody stares at
|
|
453
|
+
a spinner; then the film in front is encoded as fast as it can be, band by band
|
|
454
|
+
as the map ranks them; then, with what is left over and only then, the film
|
|
455
|
+
behind, in case somebody seeks back.
|
|
456
|
+
|
|
457
|
+
**No weights, and none possible.** A weight would let seconds at one rank buy
|
|
458
|
+
seconds at another, and it would be a figure nobody measured. The map is the
|
|
459
|
+
source of truth about what matters and it already says so — ten ranks on a film,
|
|
460
|
+
p100 at the number a viewer is stopped on, doubling zones down to p91 for the far
|
|
461
|
+
tail, p1 for what lies behind them.
|
|
462
|
+
|
|
463
|
+
`encoders` ranks below every rank of the map, so spare capacity cannot buy an
|
|
464
|
+
encoder where the map is indifferent. `wasted` is the swarm's bill for anything
|
|
465
|
+
fetched twice.
|
|
466
|
+
|
|
467
|
+
## Why an encoder is not moved for nothing, and it is not a rule
|
|
468
|
+
|
|
469
|
+
There is no threshold here, and there must not be one. What kept an encoder
|
|
470
|
+
moving was three quantities computed wrongly, not a policy that needed tuning.
|
|
471
|
+
|
|
472
|
+
**`delaySec` is when a body finishes the piece it STANDS ON.** For a run that has
|
|
473
|
+
produced something, one piece at the rate in force; for one still warming up, the
|
|
474
|
+
measured time to a first piece less the time it has already been alive. The
|
|
475
|
+
arrival of anything further on is that plus the pieces between, and nothing else.
|
|
476
|
+
|
|
477
|
+
It used to be `delay + (index - at + 1) / rate`, which charges every body a whole
|
|
478
|
+
piece for the one it is already making. That is right for a body that does not
|
|
479
|
+
exist yet and wrong for a run 0.8 s into a 0.9 s piece — and the difference is
|
|
480
|
+
the whole fault: from #58, reaching #59 was priced at 1.89 s against 1.88 s for a
|
|
481
|
+
kill and a cold start, a coin flip lost by ten milliseconds. Priced correctly it
|
|
482
|
+
is 1.08 s against 1.88 s.
|
|
483
|
+
|
|
484
|
+
**One rate, the one the arrangement puts in force.** Concurrent encoders slow
|
|
485
|
+
each other, measured on this host, so a piece costs what it costs at the body
|
|
486
|
+
count the arrangement has. Taken from the unpenalised rate while arrivals used
|
|
487
|
+
the penalised one, an extra body looked cheaper than it is and the plan bought a
|
|
488
|
+
second encoder where one served.
|
|
489
|
+
|
|
490
|
+
**Each body states its own debt** where it is created, as a function of what one
|
|
491
|
+
piece costs — because only there is it known what the body IS, and at the point
|
|
492
|
+
of pricing only how many there are. So there is no kind, no tag and no case
|
|
493
|
+
analysis.
|
|
494
|
+
|
|
495
|
+
**The measured start is separated from the piece it contains**, because the two
|
|
496
|
+
scale differently: a piece costs more when encoders share the machine, a spawn
|
|
497
|
+
does not.
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
spawn overhead = measured first output - what one piece costs alone
|
|
501
|
+
a fresh encoder = spawn overhead + the piece at the rate in force
|
|
502
|
+
a moved one = the kill, and then the same
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Both figures are measured before any viewer exists**, and until 2026-09-10
|
|
506
|
+
neither was. They were learned only from runs that had ENDED, so at a cold open
|
|
507
|
+
both read zero — and zero does not mean "unknown" here, it means "free".
|
|
508
|
+
|
|
509
|
+
That is not a small bias, it is the whole comparison. Subtract keeping from
|
|
510
|
+
moving and what is left is exactly the killing plus the time the run has already
|
|
511
|
+
lived:
|
|
512
|
+
|
|
513
|
+
```
|
|
514
|
+
keep a warming run = firstByteWait - elapsed
|
|
515
|
+
move it = kill + spawn overhead + one piece
|
|
516
|
+
= kill + firstByteWait (spawn overhead = firstByteWait - one piece)
|
|
517
|
+
move - keep = kill + elapsed
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
`elapsed` is the warm-up a move throws away — a process started, an input
|
|
521
|
+
opened, the first bytes fetched, a decoder filled — and it is the term that
|
|
522
|
+
makes moving cost something. Set `firstByteWait` to zero and it cancels out of
|
|
523
|
+
both sides along with the kill: keeping and moving then cost the same figure to
|
|
524
|
+
the millisecond, the tie falls to position, and any advantage however small wins.
|
|
525
|
+
|
|
526
|
+
Field 2026-09-08, the first fifteen seconds of a session: start at #68, a second
|
|
527
|
+
later kill and start at #69, half a second later kill and start at #68 again,
|
|
528
|
+
each dying having produced nothing. Over two days 153 runs were stopped by the
|
|
529
|
+
plan and 68 of them made no segment at all, median life 9.6 s.
|
|
530
|
+
|
|
531
|
+
`services/encode/start-stop-cost.js` measures both from **one ffmpeg run at
|
|
532
|
+
startup**: spawn to the first piece the encoder itself announces closed, then
|
|
533
|
+
SIGTERM to exit. 0.68 s and 0.02 s on the developer's desktop. `RunCosts` starts
|
|
534
|
+
from those and replaces them with readings from real runs as they arrive.
|
|
535
|
+
|
|
536
|
+
There was an `Infinity` here once for the cost of a move, on the reasoning that
|
|
537
|
+
an unmeasured price must not license an irreversible act. It was removed as an
|
|
538
|
+
exception in a model that needs none — correctly, but what replaced it was a
|
|
539
|
+
floor of zero, which licenses the act rather than forbidding it. A measurement
|
|
540
|
+
is what a model like this needs, not an exception and not a floor.
|
|
541
|
+
|
|
542
|
+
And a run killed before producing anything is a measurement too — a lower bound
|
|
543
|
+
on the first output, and the only reading a thrash can supply, since every run in
|
|
544
|
+
one is killed before it finishes anything. So a thrash makes its own moves
|
|
545
|
+
progressively dearer until it stops.
|
|
546
|
+
|
|
547
|
+
### What that was for
|
|
548
|
+
|
|
549
|
+
Field 2026-09-08: 39 moves in one session, 24 of them between three adjacent
|
|
550
|
+
numbers about 0.8 s apart. One viewer on a host affording three runs got three.
|
|
551
|
+
The picture stood still for 116.7 s in three interruptions, the worst 91.8 s.
|
|
552
|
+
|
|
553
|
+
Compounding it, the map states `withinSeconds: null` for the film behind the
|
|
554
|
+
viewers — nobody is waiting there — and `deadlineReaderFor` read that through
|
|
555
|
+
`Number()`, where `null` is 0. So the film a viewer had already passed was due
|
|
556
|
+
IMMEDIATELY and was the most urgent material in the file: it bought encoders, and
|
|
557
|
+
it took the run standing in front of the viewer because that run was the nearest
|
|
558
|
+
body to it.
|
|
559
|
+
|
|
560
|
+
Checked by simulation over eighty ticks against the map's real shape — ten zones
|
|
561
|
+
doubling ahead of the viewer, one behind — at both one and three runs: the
|
|
562
|
+
encoder is placed once, left alone, and moved exactly once, at the viewer's own
|
|
563
|
+
seek.
|
|
564
|
+
|
|
565
|
+
## Where a second encoder joins a stretch
|
|
566
|
+
|
|
567
|
+
Derived, not halved. A stretch of unmade film runs from `from` to `to`; whoever
|
|
568
|
+
is already on it stands at `from` and owes `w` before the piece under it exists;
|
|
569
|
+
a fresh one placed at `x` owes `d` — its start and then a whole piece — and both
|
|
570
|
+
then work at the rate two encoders leave each other, which is measured here.
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
the one already there closes [from, x-1]: w + (x - 1 - from) / r rises with x
|
|
574
|
+
the fresh one closes [x, to]: d + (to - x) / r falls with x
|
|
575
|
+
|
|
576
|
+
x* = (from + to + 1) / 2 + (d - w) * r / 2
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
The midpoint, shifted forward by half the difference of what the two owe, in
|
|
580
|
+
pieces. Halving is the special case `d = w`, which holds when both are fresh.
|
|
581
|
+
|
|
582
|
+
**The shift is under one segment in every measured configuration** — 0.24 of a
|
|
583
|
+
piece at the addon host's 1080p contention — so halving was very nearly right,
|
|
584
|
+
and saying otherwise would be dressing it up.
|
|
585
|
+
|
|
586
|
+
What the derivation adds is the question halving never asked: **is a second
|
|
587
|
+
encoder worth having at all?**
|
|
588
|
+
|
|
589
|
+
```
|
|
590
|
+
one: w + (to - from - 1) / rate
|
|
591
|
+
two: w + (x* - 1 - from) / r
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
Nothing is proposed where the second does not win. At 1920x1080 the measured
|
|
595
|
+
penalty for a second encoder on the addon host is 1.98 — it takes very nearly
|
|
596
|
+
all of the first's speed — and the objective keeps one; where a second is free it
|
|
597
|
+
places three.
|
|
598
|
+
|
|
599
|
+
## What the log says when anything is placed or taken away
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
encode-plan on <output>: start #58..#481, stop #?..#?
|
|
603
|
+
[speed=4.45x firstByte=1.26s kill=0.04s refetch=0.000s/s maxRuns=2 live=1]
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Every action with its INTERVAL, which is what a run is, and then every term the
|
|
607
|
+
decision was made from. Printed on any pass that does something — a session
|
|
608
|
+
where nothing changes says nothing.
|
|
609
|
+
|
|
610
|
+
The terms are there because an interval says WHAT was decided and only these say
|
|
611
|
+
WHY. A decision of this plan is
|
|
612
|
+
|
|
613
|
+
delay + (index - at) / rate + madeBetween * refetch against a deadline
|
|
614
|
+
|
|
615
|
+
so a recorded decision without the rate can be re-read and not recomputed. That
|
|
616
|
+
is not hypothetical: the one-piece intervals of 2026-09-08 were diagnosed by
|
|
617
|
+
substituting the rate from the speeds the session reported elsewhere — six
|
|
618
|
+
different figures, none of which reproduced the answer the plan had given.
|
|
619
|
+
|
|
620
|
+
A zero in `firstByte`, `kill` or `refetch` is a measurement nobody has taken, not
|
|
621
|
+
a free operation. It is printed so that reading it as free is a choice.
|
|
622
|
+
|
|
623
|
+
It was missing, and its absence cost three wrong diagnoses of one field session.
|
|
624
|
+
The line printed the windows, the budget and where the live runs stood; the
|
|
625
|
+
intervals the actions carried were the one thing it did not print. What that
|
|
626
|
+
session actually did was give every encoder an interval of exactly ONE segment —
|
|
627
|
+
63 runs, each spending 1.26 s reaching its first piece, making that one piece,
|
|
628
|
+
reaching the end of its interval and exiting, twelve of them normally. The
|
|
629
|
+
reasons printed beside them read as moves back and forth, so the fault was read
|
|
630
|
+
as an oscillating placement three times over. An interval of one segment turns
|
|
631
|
+
the protection against two encoders writing one name into a mill for processes.
|
|
632
|
+
|
|
633
|
+
## What proves a segment is finished
|
|
634
|
+
|
|
635
|
+
Its NAME, and there is nothing else. A piece being written is called
|
|
636
|
+
`making-<from>-00042.mp4` — the tag is the first number of the stretch its run
|
|
637
|
+
was given — and it takes `segment-00042.mp4` when the encoder says it has closed
|
|
638
|
+
it, which it does on a channel of its own (`-segment_list pipe:3`). Making it
|
|
639
|
+
servable is therefore one rename inside one directory, performed by the store
|
|
640
|
+
because the store owns the disk. The `hls` branch needs nothing extra: its muxer
|
|
641
|
+
writes through a temporary name of its own, so its files appear under their final
|
|
642
|
+
name whole.
|
|
643
|
+
|
|
644
|
+
Three things follow, and each replaced a guess:
|
|
645
|
+
|
|
646
|
+
1. **a request can never reach a half-written piece.** Closure used to be
|
|
647
|
+
inferred from the NEXT number existing — sound for one writer walking forward,
|
|
648
|
+
false the moment two runs share an output, which is what the plan gives an
|
|
649
|
+
output whenever it places a second encoder. Field 2026-09-08:
|
|
650
|
+
`segment-00057.mp4` served at 2 268 361 bytes and then at 4 510 940, exactly
|
|
651
|
+
half; the browser appended the half and refused the whole for the rest of the
|
|
652
|
+
session, with the picture frozen at 319.66 s;
|
|
653
|
+
2. **the last piece of a run is provable.** Under the successor rule nothing
|
|
654
|
+
followed it, so it never was — the resume case that held one segment for 46 s
|
|
655
|
+
and then answered 404;
|
|
656
|
+
3. **clearing up after a dead run is a name match.** Its unfinished pieces are
|
|
657
|
+
the ones carrying its own tag: no stretch to search, no bytes to judge, and no
|
|
658
|
+
way to remove a complete piece somebody else closed. `services/encode/
|
|
659
|
+
open-piece.js` did all three of those by guessing and is gone, along with the
|
|
660
|
+
session manager's copy of it.
|
|
661
|
+
|
|
662
|
+
## How a request for a segment ends
|
|
663
|
+
|
|
664
|
+
A request steers no encoder. The file is served if the store holds it whole;
|
|
665
|
+
otherwise the request waits for the store's own announcement that the segment
|
|
666
|
+
was published (`SegmentStore.waitFor`), for the waits of that output to be
|
|
667
|
+
invalidated (a viewer left the rung), or for the requester to go. How long it
|
|
668
|
+
may wait is the requester's: the page states it (`X-Hold-Ms`, its own deadline
|
|
669
|
+
less the round trip it has measured) and the route answers "retry" by then. A
|
|
670
|
+
requester that states nothing is held until it closes the connection. There is
|
|
671
|
+
no poll and no hold time chosen by the proxy.
|
|
672
|
+
|
|
673
|
+
Which output a request is answered by is decided in the encoding component
|
|
674
|
+
(`encode/OutputOpening.js`), without knowing who asked; the server operation
|
|
675
|
+
(`server/ViewerRequests.js`) places the requester on it.
|
|
676
|
+
|
|
677
|
+
## What is checked
|
|
678
|
+
|
|
679
|
+
`test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
|
|
680
|
+
no encoder stopped for being unwatched, the settle machinery absent, each output
|
|
681
|
+
reading its own map, and the soundtrack's start instant read off the table
|
|
682
|
+
rather than handed in.
|
|
683
|
+
|
|
684
|
+
`test/priority-map-per-output.test.js` holds the two scopes, over the real
|
|
685
|
+
viewer registry, the real `OutputCatalog` and the real `PriorityOrchestrator`.
|
|
686
|
+
|
|
687
|
+
`test/encode-plan.test.js` holds the arithmetic, including that every encoder
|
|
688
|
+
stops when nobody is watching the output.
|
|
689
|
+
|
|
690
|
+
`test/segment-store.test.js` holds the naming rule: a piece under its served
|
|
691
|
+
name is finished — the last one of a run included — one under a working name is
|
|
692
|
+
not and cannot be reached, closing it is one rename, and clearing up after one
|
|
693
|
+
run leaves every other run's work alone.
|
|
694
|
+
|
|
695
|
+
## Per-machine calibration and admission (roadmap item 97, step 14)
|
|
696
|
+
|
|
697
|
+
At startup, `encode/calibration.js` qualifies every mode the proxy may select,
|
|
698
|
+
including the software encoder kept for runtime fallback. Each mode uses the
|
|
699
|
+
production arguments on a generated test clip, and every segment must decode
|
|
700
|
+
independently. `encode/fingerprint.js` keys the results to the FFmpeg and x264
|
|
701
|
+
versions, CPU model and thread count for software, or FFmpeg version, device
|
|
702
|
+
model and driver for hardware. A changed key does not reuse measurements from
|
|
703
|
+
the previous configuration.
|
|
704
|
+
|
|
705
|
+
`encode/throughput.js` measures qualified modes at five frame sizes from
|
|
706
|
+
256×144 through 3840×2160. Between measured sizes it interpolates and subtracts
|
|
707
|
+
the measured interpolation error; it does not offer a size outside the measured
|
|
708
|
+
range. Calibration stops measuring slower modes and larger sizes once a faster
|
|
709
|
+
mode cannot sustain realtime. If a detected device has no qualified mode, the
|
|
710
|
+
proxy uses software encoding.
|
|
711
|
+
|
|
712
|
+
`encode/LocalObservations.js` stores observations from admitted encodes in the
|
|
713
|
+
proxy's `local-observations.json`. It keeps speed without competing encodes,
|
|
714
|
+
average and peak segment rates, output preparation time and the viewer buffer at
|
|
715
|
+
a transition. A matching fingerprint is required. These observations refine
|
|
716
|
+
output cost, the peak estimate of an unbounded hardware encoder and transition
|
|
717
|
+
preparation timing; an empty or stale file leaves the base policy in force.
|
|
718
|
+
|
|
719
|
+
`encode/EncodeAdmission.js` counts running encoders and outputs that viewers are
|
|
720
|
+
being placed on or prepared to use. Opening an output checks capacity and
|
|
721
|
+
records the viewer in one synchronous call through `OutputOpening` and
|
|
722
|
+
`ViewerRequests`; a second simultaneous opening sees the first reservation. If
|
|
723
|
+
no measured mode or remaining capacity is available, the proxy answers 409
|
|
724
|
+
`no-capacity` before playback. Unknown costs on occupied outputs count as
|
|
725
|
+
unknown capacity, not free capacity. The health report exposes safe encoding
|
|
726
|
+
headroom, and the pool's file-specific answer uses the same occupied-cost data.
|
|
727
|
+
If the opening is refused, the page asks the pool and prepares the file again
|
|
728
|
+
through an eligible proxy before showing video. Each refused proxy is excluded
|
|
729
|
+
for that opening attempt, so stale pool answers cannot make the page cycle.
|