@torrent-tv/proxy 2.87.4 → 2.87.6
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 +8 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/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 +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/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/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/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 +2 -2
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +1 -1
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/data-channel-handler.js +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 +71 -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 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -0
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/{services/torrent → utils}/torrent-source-key.js +38 -38
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Whether a viewer's buffer will run dry before another output could have
|
|
3
|
-
* the piece they need (roadmap item 98).
|
|
4
|
-
*
|
|
5
|
-
* THE RULE, stated by the user 2026-09-27: a move to a smaller output is
|
|
6
|
-
* prepared when, on the buffer's present trend, the viewer's buffer ends
|
|
7
|
-
* sooner than the piece they need would be closed on the new output. A buffer
|
|
8
|
-
* that falls without that threat moves nothing — a trend down is ordinary
|
|
9
|
-
* between two segments and while a cushion is being spent.
|
|
10
|
-
*
|
|
11
|
-
* WHEN IT IS JUDGED. On each report of the viewer, and the next judgement comes
|
|
12
|
-
* with their next report. So the question asked is whether the buffer lasts
|
|
13
|
-
* until the new piece would be ready IF THE MOVE WAITED FOR THAT NEXT REPORT:
|
|
14
|
-
*
|
|
15
|
-
* secondsToEmpty <= secondsToReady + reportGapSec
|
|
16
|
-
*
|
|
17
|
-
* where `secondsToEmpty` is what they hold divided by how fast it drains, and
|
|
18
|
-
* `reportGapSec` is the interval between their last two reports. Every term is
|
|
19
|
-
* measured; nothing here is chosen.
|
|
20
|
-
*
|
|
21
|
-
* A time to ready that is not known is not assumed to be short: with nothing to
|
|
22
|
-
* show there is time left, a draining buffer is a threat.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {object} params
|
|
27
|
-
* @param {number} params.bufferedSec - What the viewer holds now.
|
|
28
|
-
* @param {number | null} params.slope - The buffer's trend, seconds of film per
|
|
29
|
-
* second; negative while it drains. Null when not yet known.
|
|
30
|
-
* @param {number} params.reportGapSec - The interval between the viewer's last
|
|
31
|
-
* two reports.
|
|
32
|
-
* @param {number | null} params.secondsToReady - How long the new output would
|
|
33
|
-
* take to close the piece the viewer needs; null when not known.
|
|
34
|
-
* @returns {{ threat: boolean, secondsToEmpty: number | null }}
|
|
35
|
-
*/
|
|
36
|
-
export function drainThreat({ bufferedSec, slope, reportGapSec, secondsToReady }) {
|
|
37
|
-
if (!Number.isFinite(slope) || slope >= 0) {
|
|
38
|
-
return { threat: false, secondsToEmpty: null };
|
|
39
|
-
}
|
|
40
|
-
const held = Number.isFinite(bufferedSec) && bufferedSec > 0 ? bufferedSec : 0;
|
|
41
|
-
const secondsToEmpty = held / -slope;
|
|
42
|
-
if (!Number.isFinite(secondsToReady) || secondsToReady < 0) {
|
|
43
|
-
return { threat: true, secondsToEmpty };
|
|
44
|
-
}
|
|
45
|
-
const gap = Number.isFinite(reportGapSec) && reportGapSec > 0 ? reportGapSec : 0;
|
|
46
|
-
return { threat: secondsToEmpty <= secondsToReady + gap, secondsToEmpty };
|
|
47
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether a viewer's buffer will run dry before another output could have
|
|
3
|
+
* the piece they need (roadmap item 98).
|
|
4
|
+
*
|
|
5
|
+
* THE RULE, stated by the user 2026-09-27: a move to a smaller output is
|
|
6
|
+
* prepared when, on the buffer's present trend, the viewer's buffer ends
|
|
7
|
+
* sooner than the piece they need would be closed on the new output. A buffer
|
|
8
|
+
* that falls without that threat moves nothing — a trend down is ordinary
|
|
9
|
+
* between two segments and while a cushion is being spent.
|
|
10
|
+
*
|
|
11
|
+
* WHEN IT IS JUDGED. On each report of the viewer, and the next judgement comes
|
|
12
|
+
* with their next report. So the question asked is whether the buffer lasts
|
|
13
|
+
* until the new piece would be ready IF THE MOVE WAITED FOR THAT NEXT REPORT:
|
|
14
|
+
*
|
|
15
|
+
* secondsToEmpty <= secondsToReady + reportGapSec
|
|
16
|
+
*
|
|
17
|
+
* where `secondsToEmpty` is what they hold divided by how fast it drains, and
|
|
18
|
+
* `reportGapSec` is the interval between their last two reports. Every term is
|
|
19
|
+
* measured; nothing here is chosen.
|
|
20
|
+
*
|
|
21
|
+
* A time to ready that is not known is not assumed to be short: with nothing to
|
|
22
|
+
* show there is time left, a draining buffer is a threat.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} params
|
|
27
|
+
* @param {number} params.bufferedSec - What the viewer holds now.
|
|
28
|
+
* @param {number | null} params.slope - The buffer's trend, seconds of film per
|
|
29
|
+
* second; negative while it drains. Null when not yet known.
|
|
30
|
+
* @param {number} params.reportGapSec - The interval between the viewer's last
|
|
31
|
+
* two reports.
|
|
32
|
+
* @param {number | null} params.secondsToReady - How long the new output would
|
|
33
|
+
* take to close the piece the viewer needs; null when not known.
|
|
34
|
+
* @returns {{ threat: boolean, secondsToEmpty: number | null }}
|
|
35
|
+
*/
|
|
36
|
+
export function drainThreat({ bufferedSec, slope, reportGapSec, secondsToReady }) {
|
|
37
|
+
if (!Number.isFinite(slope) || slope >= 0) {
|
|
38
|
+
return { threat: false, secondsToEmpty: null };
|
|
39
|
+
}
|
|
40
|
+
const held = Number.isFinite(bufferedSec) && bufferedSec > 0 ? bufferedSec : 0;
|
|
41
|
+
const secondsToEmpty = held / -slope;
|
|
42
|
+
if (!Number.isFinite(secondsToReady) || secondsToReady < 0) {
|
|
43
|
+
return { threat: true, secondsToEmpty };
|
|
44
|
+
}
|
|
45
|
+
const gap = Number.isFinite(reportGapSec) && reportGapSec > 0 ? reportGapSec : 0;
|
|
46
|
+
return { threat: secondsToEmpty <= secondsToReady + gap, secondsToEmpty };
|
|
47
|
+
}
|
|
@@ -1,254 +1,254 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a viewer's link can carry, and what an output asks of it.
|
|
3
|
-
*
|
|
4
|
-
* PURE FUNCTIONS: the viewer's connection enters as a NUMBER — how many megabits
|
|
5
|
-
* the last report showed — and never as a reference to a viewer. Nothing here
|
|
6
|
-
* holds anybody, reads anything or can be asked at the wrong moment.
|
|
7
|
-
*
|
|
8
|
-
* WHAT A LINK CARRIES IS A LOAD, NOT A PICTURE (roadmap item 97, step 11). The
|
|
9
|
-
* viewer receives the picture AND the soundtrack they chose — inside the
|
|
10
|
-
* picture's own stream, or as a separate one — so both are added before the
|
|
11
|
-
* link is asked. And each part says how much its figure can be trusted:
|
|
12
|
-
*
|
|
13
|
-
* 1. `known` — a bound the encoder is held to: a re-encode's `maxrate`, or the
|
|
14
|
-
* constant rate a re-encoded soundtrack is produced at;
|
|
15
|
-
* 2. `estimated` — a figure the FILE states: a copied picture's average rate,
|
|
16
|
-
* a copied soundtrack's stated rate. An average is not a bound, so a load
|
|
17
|
-
* built on it is admitted only by estimate and is never confirmed;
|
|
18
|
-
* 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
|
|
19
|
-
* soundtrack whose rate nothing states.
|
|
20
|
-
*
|
|
21
|
-
* A load is as trustworthy as its least trustworthy part.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { maxrateKbpsFor, nominalKbpsFor } from "../args.js";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* How much of a measured link may be spent on the stream.
|
|
28
|
-
*
|
|
29
|
-
* The rest is what the link does when it is not being perfect: transport
|
|
30
|
-
* framing, retransmission, the other tabs, the moment somebody else in the
|
|
31
|
-
* house starts something. A load sized to the whole reading stalls on the
|
|
32
|
-
* first of those.
|
|
33
|
-
*
|
|
34
|
-
* Chosen, not measured, and written as a constant rather than dressed up as a
|
|
35
|
-
* measurement. What would replace it is a reading of the transport's own
|
|
36
|
-
* overhead and of how far a link's throughput varies over a session, which
|
|
37
|
-
* nothing takes.
|
|
38
|
-
*/
|
|
39
|
-
export const LINK_SAFETY = 0.8;
|
|
40
|
-
|
|
41
|
-
/** How far a figure in a load can be trusted. */
|
|
42
|
-
export const PEAK_CLASS = Object.freeze({
|
|
43
|
-
KNOWN: "known",
|
|
44
|
-
ESTIMATED: "estimated",
|
|
45
|
-
UNKNOWN: "unknown"
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/** What a link was found to do with a load. */
|
|
49
|
-
export const LINK_VERDICT = Object.freeze({
|
|
50
|
-
FITS: "fits",
|
|
51
|
-
ESTIMATED_TO_FIT: "estimated to fit",
|
|
52
|
-
DOES_NOT_FIT: "does not fit",
|
|
53
|
-
NO_SAFE_BOUND: "no safe bound",
|
|
54
|
-
NO_MEASUREMENT: "no measurement"
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN];
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @typedef {{ mbps: number | null, peakClass: string }} LoadPart
|
|
61
|
-
* @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @param {number | null} mbps
|
|
66
|
-
* @param {string} peakClass
|
|
67
|
-
* @returns {LoadPart}
|
|
68
|
-
*/
|
|
69
|
-
function part(mbps, peakClass) {
|
|
70
|
-
const figure = Number.isFinite(mbps) && mbps > 0 ? mbps : null;
|
|
71
|
-
// A figure that is missing is no figure, whatever the part claimed to be.
|
|
72
|
-
return { mbps: figure, peakClass: figure === null ? PEAK_CLASS.UNKNOWN : peakClass };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The picture part of a load, read from an output's identity.
|
|
77
|
-
*
|
|
78
|
-
* An encoder given no limit states no bound, and its part is `unknown` — unless
|
|
79
|
-
* this host has seen outputs of exactly this mode carry a peak (roadmap item
|
|
80
|
-
* 97, step 14), and then that peak is its figure, as an ESTIMATE: it is what
|
|
81
|
-
* was seen, not a bound anything holds the encoder to.
|
|
82
|
-
*
|
|
83
|
-
* @param {import("../output/OutputSpec.js").OutputSpec} spec
|
|
84
|
-
* @param {number | null} sourceMbps - The average rate the source file states.
|
|
85
|
-
* @param {number | null} [observedPeakMbps] - The largest peak outputs of this
|
|
86
|
-
* mode were seen carrying on this configuration, or null.
|
|
87
|
-
* @returns {LoadPart | null} Null for an output that carries no picture.
|
|
88
|
-
*/
|
|
89
|
-
export function videoLoadOfSpec(spec, sourceMbps, observedPeakMbps = null) {
|
|
90
|
-
if (!spec?.video) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const encode = spec.video.encode;
|
|
94
|
-
if (!encode) {
|
|
95
|
-
return part(sourceMbps, PEAK_CLASS.ESTIMATED);
|
|
96
|
-
}
|
|
97
|
-
const rateControl = encode.rateControl;
|
|
98
|
-
if (rateControl) {
|
|
99
|
-
return part(rateControl.maxrateKbps / 1000, PEAK_CLASS.KNOWN);
|
|
100
|
-
}
|
|
101
|
-
return Number.isFinite(observedPeakMbps) && observedPeakMbps > 0
|
|
102
|
-
? part(observedPeakMbps, PEAK_CLASS.ESTIMATED)
|
|
103
|
-
: part(null, PEAK_CLASS.UNKNOWN);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The picture part of a load for a height nothing has produced yet, the way
|
|
108
|
-
* it WOULD be produced here: at the nominal limit on the software encoder,
|
|
109
|
-
* with no limit on a hardware one, and as the source's own stream when the
|
|
110
|
-
* picture is copied at the source's height.
|
|
111
|
-
*
|
|
112
|
-
* The nominal limit is the one of the FRAME the height would be encoded at,
|
|
113
|
-
* chosen by its area (`nominalKbpsFor`) — the same row the output is then
|
|
114
|
-
* opened in, so the offer and the output cannot price one step differently.
|
|
115
|
-
*
|
|
116
|
-
* @param {{ sourceHeight: number, copiesAtSource: boolean, sourceMbps: number | null, encoderKind: string }} picture
|
|
117
|
-
* @param {{ width: number, height: number }} frame - The frame this height is
|
|
118
|
-
* encoded at for this source.
|
|
119
|
-
* @returns {LoadPart}
|
|
120
|
-
*/
|
|
121
|
-
export function videoLoadForFrame(picture, frame) {
|
|
122
|
-
const sourceHeight = Math.round(Number(picture?.sourceHeight) || 0);
|
|
123
|
-
if (picture?.copiesAtSource === true && frame.height === sourceHeight) {
|
|
124
|
-
return part(picture?.sourceMbps ?? null, PEAK_CLASS.ESTIMATED);
|
|
125
|
-
}
|
|
126
|
-
if (picture?.encoderKind !== "software") {
|
|
127
|
-
return part(null, PEAK_CLASS.UNKNOWN);
|
|
128
|
-
}
|
|
129
|
-
return part(maxrateKbpsFor(nominalKbpsFor(frame)) / 1000, PEAK_CLASS.KNOWN);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* The picture part of a load for a software encode at a nominal limit nothing
|
|
134
|
-
* has produced yet. The bound is the one `softwareRateControlFor` would give
|
|
135
|
-
* it, so an output opened at that limit carries exactly this load.
|
|
136
|
-
*
|
|
137
|
-
* @param {number} nominalKbps
|
|
138
|
-
* @returns {LoadPart}
|
|
139
|
-
*/
|
|
140
|
-
export function videoLoadOfLimit(nominalKbps) {
|
|
141
|
-
return part(maxrateKbpsFor(nominalKbps) / 1000, PEAK_CLASS.KNOWN);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* The soundtrack part of a load: the track THIS viewer chose.
|
|
146
|
-
*
|
|
147
|
-
* @param {{ transcode: boolean, bitrateKbps: number | null } | null} track -
|
|
148
|
-
* Null when the viewer receives no sound from this output or beside it.
|
|
149
|
-
* @param {number} transcodeKbps - What a re-encoded track is produced at.
|
|
150
|
-
* @returns {LoadPart | null}
|
|
151
|
-
*/
|
|
152
|
-
export function audioLoadOf(track, transcodeKbps) {
|
|
153
|
-
if (!track) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
if (track.transcode === true) {
|
|
157
|
-
return part(transcodeKbps / 1000, PEAK_CLASS.KNOWN);
|
|
158
|
-
}
|
|
159
|
-
return part((Number(track.bitrateKbps) || 0) / 1000, PEAK_CLASS.ESTIMATED);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The whole load: the parts added, and the class of the least trustworthy.
|
|
164
|
-
*
|
|
165
|
-
* @param {LoadPart | null} video
|
|
166
|
-
* @param {LoadPart | null} audio
|
|
167
|
-
* @returns {Load}
|
|
168
|
-
*/
|
|
169
|
-
export function loadOf(video, audio) {
|
|
170
|
-
const parts = [video, audio].filter(Boolean);
|
|
171
|
-
const peakClass = parts.reduce(
|
|
172
|
-
(worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
|
|
173
|
-
PEAK_CLASS.KNOWN
|
|
174
|
-
);
|
|
175
|
-
const totalMbps = peakClass === PEAK_CLASS.UNKNOWN
|
|
176
|
-
? null
|
|
177
|
-
: parts.reduce((sum, one) => sum + one.mbps, 0);
|
|
178
|
-
return { video, audio, totalMbps, peakClass };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* What a link that measured `linkMbps` does with a load.
|
|
183
|
-
*
|
|
184
|
-
* 1. nothing measured the link — every class is admitted, since the link gave
|
|
185
|
-
* no ground to refuse, and nothing is confirmed;
|
|
186
|
-
* 2. an `unknown` load against a measured link — `no safe bound`, refused:
|
|
187
|
-
* there is no number to compare;
|
|
188
|
-
* 3. otherwise the load is compared with the usable share of the link: a
|
|
189
|
-
* `known` load that passes FITS and is confirmed, an `estimated` one that
|
|
190
|
-
* passes is admitted by estimate and is NOT confirmed.
|
|
191
|
-
*
|
|
192
|
-
* @param {number | null} linkMbps
|
|
193
|
-
* @param {Load} load
|
|
194
|
-
* @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
|
|
195
|
-
*/
|
|
196
|
-
export function linkCouldCarry(linkMbps, load) {
|
|
197
|
-
const measured = Number.isFinite(linkMbps) && linkMbps > 0;
|
|
198
|
-
const answer = (verdict, admitted, confirmed) => ({
|
|
199
|
-
verdict,
|
|
200
|
-
admitted,
|
|
201
|
-
confirmed,
|
|
202
|
-
linkMbps: measured ? linkMbps : null,
|
|
203
|
-
load
|
|
204
|
-
});
|
|
205
|
-
if (!measured) {
|
|
206
|
-
return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
|
|
207
|
-
}
|
|
208
|
-
if (load.peakClass === PEAK_CLASS.UNKNOWN) {
|
|
209
|
-
return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
|
|
210
|
-
}
|
|
211
|
-
if (linkMbps * LINK_SAFETY < load.totalMbps) {
|
|
212
|
-
return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
|
|
213
|
-
}
|
|
214
|
-
return load.peakClass === PEAK_CLASS.KNOWN
|
|
215
|
-
? answer(LINK_VERDICT.FITS, true, true)
|
|
216
|
-
: answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* How strongly an answer is admitted, for ordering: a load whose figure is a
|
|
221
|
-
* bound before one whose figure is an average, and both before one with no
|
|
222
|
-
* figure (which is only ever admitted with nothing measured). Zero for a
|
|
223
|
-
* refusal.
|
|
224
|
-
*
|
|
225
|
-
* @param {{ admitted: boolean, load: Load }} answer
|
|
226
|
-
* @returns {number}
|
|
227
|
-
*/
|
|
228
|
-
export function admissionRank(answer) {
|
|
229
|
-
if (!answer.admitted) {
|
|
230
|
-
return 0;
|
|
231
|
-
}
|
|
232
|
-
return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* The figures behind an answer, as plain values a log line or a report can
|
|
237
|
-
* carry.
|
|
238
|
-
*
|
|
239
|
-
* @param {{ verdict: string, linkMbps: number | null, load: Load }} answer
|
|
240
|
-
* @returns {object}
|
|
241
|
-
*/
|
|
242
|
-
export function linkAnswerFigures(answer) {
|
|
243
|
-
return {
|
|
244
|
-
verdict: answer.verdict,
|
|
245
|
-
linkMbps: answer.linkMbps,
|
|
246
|
-
linkSafety: LINK_SAFETY,
|
|
247
|
-
videoMbps: answer.load.video?.mbps ?? null,
|
|
248
|
-
videoClass: answer.load.video?.peakClass ?? null,
|
|
249
|
-
audioMbps: answer.load.audio?.mbps ?? null,
|
|
250
|
-
audioClass: answer.load.audio?.peakClass ?? null,
|
|
251
|
-
totalMbps: answer.load.totalMbps,
|
|
252
|
-
peakClass: answer.load.peakClass
|
|
253
|
-
};
|
|
254
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file What a viewer's link can carry, and what an output asks of it.
|
|
3
|
+
*
|
|
4
|
+
* PURE FUNCTIONS: the viewer's connection enters as a NUMBER — how many megabits
|
|
5
|
+
* the last report showed — and never as a reference to a viewer. Nothing here
|
|
6
|
+
* holds anybody, reads anything or can be asked at the wrong moment.
|
|
7
|
+
*
|
|
8
|
+
* WHAT A LINK CARRIES IS A LOAD, NOT A PICTURE (roadmap item 97, step 11). The
|
|
9
|
+
* viewer receives the picture AND the soundtrack they chose — inside the
|
|
10
|
+
* picture's own stream, or as a separate one — so both are added before the
|
|
11
|
+
* link is asked. And each part says how much its figure can be trusted:
|
|
12
|
+
*
|
|
13
|
+
* 1. `known` — a bound the encoder is held to: a re-encode's `maxrate`, or the
|
|
14
|
+
* constant rate a re-encoded soundtrack is produced at;
|
|
15
|
+
* 2. `estimated` — a figure the FILE states: a copied picture's average rate,
|
|
16
|
+
* a copied soundtrack's stated rate. An average is not a bound, so a load
|
|
17
|
+
* built on it is admitted only by estimate and is never confirmed;
|
|
18
|
+
* 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
|
|
19
|
+
* soundtrack whose rate nothing states.
|
|
20
|
+
*
|
|
21
|
+
* A load is as trustworthy as its least trustworthy part.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { maxrateKbpsFor, nominalKbpsFor } from "../args.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How much of a measured link may be spent on the stream.
|
|
28
|
+
*
|
|
29
|
+
* The rest is what the link does when it is not being perfect: transport
|
|
30
|
+
* framing, retransmission, the other tabs, the moment somebody else in the
|
|
31
|
+
* house starts something. A load sized to the whole reading stalls on the
|
|
32
|
+
* first of those.
|
|
33
|
+
*
|
|
34
|
+
* Chosen, not measured, and written as a constant rather than dressed up as a
|
|
35
|
+
* measurement. What would replace it is a reading of the transport's own
|
|
36
|
+
* overhead and of how far a link's throughput varies over a session, which
|
|
37
|
+
* nothing takes.
|
|
38
|
+
*/
|
|
39
|
+
export const LINK_SAFETY = 0.8;
|
|
40
|
+
|
|
41
|
+
/** How far a figure in a load can be trusted. */
|
|
42
|
+
export const PEAK_CLASS = Object.freeze({
|
|
43
|
+
KNOWN: "known",
|
|
44
|
+
ESTIMATED: "estimated",
|
|
45
|
+
UNKNOWN: "unknown"
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** What a link was found to do with a load. */
|
|
49
|
+
export const LINK_VERDICT = Object.freeze({
|
|
50
|
+
FITS: "fits",
|
|
51
|
+
ESTIMATED_TO_FIT: "estimated to fit",
|
|
52
|
+
DOES_NOT_FIT: "does not fit",
|
|
53
|
+
NO_SAFE_BOUND: "no safe bound",
|
|
54
|
+
NO_MEASUREMENT: "no measurement"
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{ mbps: number | null, peakClass: string }} LoadPart
|
|
61
|
+
* @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {number | null} mbps
|
|
66
|
+
* @param {string} peakClass
|
|
67
|
+
* @returns {LoadPart}
|
|
68
|
+
*/
|
|
69
|
+
function part(mbps, peakClass) {
|
|
70
|
+
const figure = Number.isFinite(mbps) && mbps > 0 ? mbps : null;
|
|
71
|
+
// A figure that is missing is no figure, whatever the part claimed to be.
|
|
72
|
+
return { mbps: figure, peakClass: figure === null ? PEAK_CLASS.UNKNOWN : peakClass };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The picture part of a load, read from an output's identity.
|
|
77
|
+
*
|
|
78
|
+
* An encoder given no limit states no bound, and its part is `unknown` — unless
|
|
79
|
+
* this host has seen outputs of exactly this mode carry a peak (roadmap item
|
|
80
|
+
* 97, step 14), and then that peak is its figure, as an ESTIMATE: it is what
|
|
81
|
+
* was seen, not a bound anything holds the encoder to.
|
|
82
|
+
*
|
|
83
|
+
* @param {import("../output/OutputSpec.js").OutputSpec} spec
|
|
84
|
+
* @param {number | null} sourceMbps - The average rate the source file states.
|
|
85
|
+
* @param {number | null} [observedPeakMbps] - The largest peak outputs of this
|
|
86
|
+
* mode were seen carrying on this configuration, or null.
|
|
87
|
+
* @returns {LoadPart | null} Null for an output that carries no picture.
|
|
88
|
+
*/
|
|
89
|
+
export function videoLoadOfSpec(spec, sourceMbps, observedPeakMbps = null) {
|
|
90
|
+
if (!spec?.video) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const encode = spec.video.encode;
|
|
94
|
+
if (!encode) {
|
|
95
|
+
return part(sourceMbps, PEAK_CLASS.ESTIMATED);
|
|
96
|
+
}
|
|
97
|
+
const rateControl = encode.rateControl;
|
|
98
|
+
if (rateControl) {
|
|
99
|
+
return part(rateControl.maxrateKbps / 1000, PEAK_CLASS.KNOWN);
|
|
100
|
+
}
|
|
101
|
+
return Number.isFinite(observedPeakMbps) && observedPeakMbps > 0
|
|
102
|
+
? part(observedPeakMbps, PEAK_CLASS.ESTIMATED)
|
|
103
|
+
: part(null, PEAK_CLASS.UNKNOWN);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The picture part of a load for a height nothing has produced yet, the way
|
|
108
|
+
* it WOULD be produced here: at the nominal limit on the software encoder,
|
|
109
|
+
* with no limit on a hardware one, and as the source's own stream when the
|
|
110
|
+
* picture is copied at the source's height.
|
|
111
|
+
*
|
|
112
|
+
* The nominal limit is the one of the FRAME the height would be encoded at,
|
|
113
|
+
* chosen by its area (`nominalKbpsFor`) — the same row the output is then
|
|
114
|
+
* opened in, so the offer and the output cannot price one step differently.
|
|
115
|
+
*
|
|
116
|
+
* @param {{ sourceHeight: number, copiesAtSource: boolean, sourceMbps: number | null, encoderKind: string }} picture
|
|
117
|
+
* @param {{ width: number, height: number }} frame - The frame this height is
|
|
118
|
+
* encoded at for this source.
|
|
119
|
+
* @returns {LoadPart}
|
|
120
|
+
*/
|
|
121
|
+
export function videoLoadForFrame(picture, frame) {
|
|
122
|
+
const sourceHeight = Math.round(Number(picture?.sourceHeight) || 0);
|
|
123
|
+
if (picture?.copiesAtSource === true && frame.height === sourceHeight) {
|
|
124
|
+
return part(picture?.sourceMbps ?? null, PEAK_CLASS.ESTIMATED);
|
|
125
|
+
}
|
|
126
|
+
if (picture?.encoderKind !== "software") {
|
|
127
|
+
return part(null, PEAK_CLASS.UNKNOWN);
|
|
128
|
+
}
|
|
129
|
+
return part(maxrateKbpsFor(nominalKbpsFor(frame)) / 1000, PEAK_CLASS.KNOWN);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The picture part of a load for a software encode at a nominal limit nothing
|
|
134
|
+
* has produced yet. The bound is the one `softwareRateControlFor` would give
|
|
135
|
+
* it, so an output opened at that limit carries exactly this load.
|
|
136
|
+
*
|
|
137
|
+
* @param {number} nominalKbps
|
|
138
|
+
* @returns {LoadPart}
|
|
139
|
+
*/
|
|
140
|
+
export function videoLoadOfLimit(nominalKbps) {
|
|
141
|
+
return part(maxrateKbpsFor(nominalKbps) / 1000, PEAK_CLASS.KNOWN);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The soundtrack part of a load: the track THIS viewer chose.
|
|
146
|
+
*
|
|
147
|
+
* @param {{ transcode: boolean, bitrateKbps: number | null } | null} track -
|
|
148
|
+
* Null when the viewer receives no sound from this output or beside it.
|
|
149
|
+
* @param {number} transcodeKbps - What a re-encoded track is produced at.
|
|
150
|
+
* @returns {LoadPart | null}
|
|
151
|
+
*/
|
|
152
|
+
export function audioLoadOf(track, transcodeKbps) {
|
|
153
|
+
if (!track) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (track.transcode === true) {
|
|
157
|
+
return part(transcodeKbps / 1000, PEAK_CLASS.KNOWN);
|
|
158
|
+
}
|
|
159
|
+
return part((Number(track.bitrateKbps) || 0) / 1000, PEAK_CLASS.ESTIMATED);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The whole load: the parts added, and the class of the least trustworthy.
|
|
164
|
+
*
|
|
165
|
+
* @param {LoadPart | null} video
|
|
166
|
+
* @param {LoadPart | null} audio
|
|
167
|
+
* @returns {Load}
|
|
168
|
+
*/
|
|
169
|
+
export function loadOf(video, audio) {
|
|
170
|
+
const parts = [video, audio].filter(Boolean);
|
|
171
|
+
const peakClass = parts.reduce(
|
|
172
|
+
(worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
|
|
173
|
+
PEAK_CLASS.KNOWN
|
|
174
|
+
);
|
|
175
|
+
const totalMbps = peakClass === PEAK_CLASS.UNKNOWN
|
|
176
|
+
? null
|
|
177
|
+
: parts.reduce((sum, one) => sum + one.mbps, 0);
|
|
178
|
+
return { video, audio, totalMbps, peakClass };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* What a link that measured `linkMbps` does with a load.
|
|
183
|
+
*
|
|
184
|
+
* 1. nothing measured the link — every class is admitted, since the link gave
|
|
185
|
+
* no ground to refuse, and nothing is confirmed;
|
|
186
|
+
* 2. an `unknown` load against a measured link — `no safe bound`, refused:
|
|
187
|
+
* there is no number to compare;
|
|
188
|
+
* 3. otherwise the load is compared with the usable share of the link: a
|
|
189
|
+
* `known` load that passes FITS and is confirmed, an `estimated` one that
|
|
190
|
+
* passes is admitted by estimate and is NOT confirmed.
|
|
191
|
+
*
|
|
192
|
+
* @param {number | null} linkMbps
|
|
193
|
+
* @param {Load} load
|
|
194
|
+
* @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
|
|
195
|
+
*/
|
|
196
|
+
export function linkCouldCarry(linkMbps, load) {
|
|
197
|
+
const measured = Number.isFinite(linkMbps) && linkMbps > 0;
|
|
198
|
+
const answer = (verdict, admitted, confirmed) => ({
|
|
199
|
+
verdict,
|
|
200
|
+
admitted,
|
|
201
|
+
confirmed,
|
|
202
|
+
linkMbps: measured ? linkMbps : null,
|
|
203
|
+
load
|
|
204
|
+
});
|
|
205
|
+
if (!measured) {
|
|
206
|
+
return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
|
|
207
|
+
}
|
|
208
|
+
if (load.peakClass === PEAK_CLASS.UNKNOWN) {
|
|
209
|
+
return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
|
|
210
|
+
}
|
|
211
|
+
if (linkMbps * LINK_SAFETY < load.totalMbps) {
|
|
212
|
+
return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
|
|
213
|
+
}
|
|
214
|
+
return load.peakClass === PEAK_CLASS.KNOWN
|
|
215
|
+
? answer(LINK_VERDICT.FITS, true, true)
|
|
216
|
+
: answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* How strongly an answer is admitted, for ordering: a load whose figure is a
|
|
221
|
+
* bound before one whose figure is an average, and both before one with no
|
|
222
|
+
* figure (which is only ever admitted with nothing measured). Zero for a
|
|
223
|
+
* refusal.
|
|
224
|
+
*
|
|
225
|
+
* @param {{ admitted: boolean, load: Load }} answer
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
export function admissionRank(answer) {
|
|
229
|
+
if (!answer.admitted) {
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The figures behind an answer, as plain values a log line or a report can
|
|
237
|
+
* carry.
|
|
238
|
+
*
|
|
239
|
+
* @param {{ verdict: string, linkMbps: number | null, load: Load }} answer
|
|
240
|
+
* @returns {object}
|
|
241
|
+
*/
|
|
242
|
+
export function linkAnswerFigures(answer) {
|
|
243
|
+
return {
|
|
244
|
+
verdict: answer.verdict,
|
|
245
|
+
linkMbps: answer.linkMbps,
|
|
246
|
+
linkSafety: LINK_SAFETY,
|
|
247
|
+
videoMbps: answer.load.video?.mbps ?? null,
|
|
248
|
+
videoClass: answer.load.video?.peakClass ?? null,
|
|
249
|
+
audioMbps: answer.load.audio?.mbps ?? null,
|
|
250
|
+
audioClass: answer.load.audio?.peakClass ?? null,
|
|
251
|
+
totalMbps: answer.load.totalMbps,
|
|
252
|
+
peakClass: answer.load.peakClass
|
|
253
|
+
};
|
|
254
|
+
}
|