@torrent-tv/proxy 2.89.1 → 2.89.3
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 +6 -1
- package/CHANGELOG.md +15 -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/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/playback-start-forecast.md +42 -8
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- 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 +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/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/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2282
- 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 +418 -418
- 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/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/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/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 +349 -349
- package/services/encode/quality/output-format.js +319 -319
- 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-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +476 -458
- package/services/encode/segment-formats/index.js +103 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +604 -541
- 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/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +790 -786
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1024 -1020
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1255 -1255
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +836 -836
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/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/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -0
- package/services/{server → viewer}/playback-readiness.js +635 -516
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -70
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-presentation-clock.test.js +146 -162
- 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/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/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 +112 -112
- 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/media-time.test.js +62 -0
- 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-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -483
- package/test/playback-start-regressions.test.js +101 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1257
- 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-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- 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 +105 -105
- 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/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- 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 +504 -504
- 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/whole-file-held-read.test.js +30 -30
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,349 +1,349 @@
|
|
|
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 stream is held to: a re-encode's `maxrate`, or the
|
|
14
|
-
* most a copied soundtrack's codec allows it to carry;
|
|
15
|
-
* 2. `estimated` — an average: a copied picture's or soundtrack's rate as the
|
|
16
|
-
* FILE states it, or the target a re-encoded soundtrack is asked for. An
|
|
17
|
-
* average is not a bound, so a load built on it is admitted only by
|
|
18
|
-
* estimate and is never confirmed;
|
|
19
|
-
* 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
|
|
20
|
-
* soundtrack whose rate nothing states.
|
|
21
|
-
*
|
|
22
|
-
* A load is as trustworthy as its least trustworthy part.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { AUDIO_TRANSCODE_KBPS, maxrateKbpsFor, nominalKbpsFor } from "../args.js";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* How much of a measured link may be spent on the stream.
|
|
29
|
-
*
|
|
30
|
-
* The rest is what the link does when it is not being perfect: transport
|
|
31
|
-
* framing, retransmission, the other tabs, the moment somebody else in the
|
|
32
|
-
* house starts something. A load sized to the whole reading stalls on the
|
|
33
|
-
* first of those.
|
|
34
|
-
*
|
|
35
|
-
* Chosen, not measured, and written as a constant rather than dressed up as a
|
|
36
|
-
* measurement. What would replace it is a reading of the transport's own
|
|
37
|
-
* overhead and of how far a link's throughput varies over a session, which
|
|
38
|
-
* nothing takes.
|
|
39
|
-
*/
|
|
40
|
-
export const LINK_SAFETY = 0.8;
|
|
41
|
-
|
|
42
|
-
/** How far a figure in a load can be trusted. */
|
|
43
|
-
export const PEAK_CLASS = Object.freeze({
|
|
44
|
-
KNOWN: "known",
|
|
45
|
-
ESTIMATED: "estimated",
|
|
46
|
-
UNKNOWN: "unknown"
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
/** What a link was found to do with a load. */
|
|
50
|
-
export const LINK_VERDICT = Object.freeze({
|
|
51
|
-
FITS: "fits",
|
|
52
|
-
ESTIMATED_TO_FIT: "estimated to fit",
|
|
53
|
-
DOES_NOT_FIT: "does not fit",
|
|
54
|
-
NO_SAFE_BOUND: "no safe bound",
|
|
55
|
-
NO_MEASUREMENT: "no measurement"
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN];
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {{ mbps: number | null, peakClass: string }} LoadPart
|
|
62
|
-
* @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @param {number | null} mbps
|
|
67
|
-
* @param {string} peakClass
|
|
68
|
-
* @returns {LoadPart}
|
|
69
|
-
*/
|
|
70
|
-
function part(mbps, peakClass) {
|
|
71
|
-
const figure = Number.isFinite(mbps) && mbps > 0 ? mbps : null;
|
|
72
|
-
// A figure that is missing is no figure, whatever the part claimed to be.
|
|
73
|
-
return { mbps: figure, peakClass: figure === null ? PEAK_CLASS.UNKNOWN : peakClass };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The picture part of a load, read from an output's identity.
|
|
78
|
-
*
|
|
79
|
-
* An encoder given no limit states no bound, and its part is `unknown` — unless
|
|
80
|
-
* this host has seen outputs of exactly this mode carry a peak (roadmap item
|
|
81
|
-
* 97, step 14), and then that peak is its figure, as an ESTIMATE: it is what
|
|
82
|
-
* was seen, not a bound anything holds the encoder to.
|
|
83
|
-
*
|
|
84
|
-
* @param {import("../output/OutputSpec.js").OutputSpec} spec
|
|
85
|
-
* @param {number | null} sourceMbps - The average rate the source file states.
|
|
86
|
-
* @param {number | null} [observedPeakMbps] - The largest peak outputs of this
|
|
87
|
-
* mode were seen carrying on this configuration, or null.
|
|
88
|
-
* @returns {LoadPart | null} Null for an output that carries no picture.
|
|
89
|
-
*/
|
|
90
|
-
export function videoLoadOfSpec(spec, sourceMbps, observedPeakMbps = null) {
|
|
91
|
-
if (!spec?.video) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
const encode = spec.video.encode;
|
|
95
|
-
if (!encode) {
|
|
96
|
-
return part(sourceMbps, PEAK_CLASS.ESTIMATED);
|
|
97
|
-
}
|
|
98
|
-
const rateControl = encode.rateControl;
|
|
99
|
-
if (rateControl) {
|
|
100
|
-
return part(rateControl.maxrateKbps / 1000, PEAK_CLASS.KNOWN);
|
|
101
|
-
}
|
|
102
|
-
return Number.isFinite(observedPeakMbps) && observedPeakMbps > 0
|
|
103
|
-
? part(observedPeakMbps, PEAK_CLASS.ESTIMATED)
|
|
104
|
-
: part(null, PEAK_CLASS.UNKNOWN);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The picture part of a load for a height nothing has produced yet, the way
|
|
109
|
-
* it WOULD be produced here: at the nominal limit on the software encoder,
|
|
110
|
-
* with no limit on a hardware one, and as the source's own stream when the
|
|
111
|
-
* picture is copied at the source's height.
|
|
112
|
-
*
|
|
113
|
-
* The nominal limit is the one of the FRAME the height would be encoded at,
|
|
114
|
-
* chosen by its area (`nominalKbpsFor`) — the same row the output is then
|
|
115
|
-
* opened in, so the offer and the output cannot price one step differently.
|
|
116
|
-
*
|
|
117
|
-
* @param {{ sourceHeight: number, copiesAtSource: boolean, sourceMbps: number | null, encoderKind: string }} picture
|
|
118
|
-
* @param {{ width: number, height: number }} frame - The frame this height is
|
|
119
|
-
* encoded at for this source.
|
|
120
|
-
* @returns {LoadPart}
|
|
121
|
-
*/
|
|
122
|
-
export function videoLoadForFrame(picture, frame) {
|
|
123
|
-
const sourceHeight = Math.round(Number(picture?.sourceHeight) || 0);
|
|
124
|
-
if (picture?.copiesAtSource === true && frame.height === sourceHeight) {
|
|
125
|
-
return part(picture?.sourceMbps ?? null, PEAK_CLASS.ESTIMATED);
|
|
126
|
-
}
|
|
127
|
-
if (picture?.encoderKind !== "software") {
|
|
128
|
-
return part(null, PEAK_CLASS.UNKNOWN);
|
|
129
|
-
}
|
|
130
|
-
return part(maxrateKbpsFor(nominalKbpsFor(frame)) / 1000, PEAK_CLASS.KNOWN);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* The picture part of a load for a software encode at a nominal limit nothing
|
|
135
|
-
* has produced yet. The bound is the one `softwareRateControlFor` would give
|
|
136
|
-
* it, so an output opened at that limit carries exactly this load.
|
|
137
|
-
*
|
|
138
|
-
* @param {number} nominalKbps
|
|
139
|
-
* @returns {LoadPart}
|
|
140
|
-
*/
|
|
141
|
-
export function videoLoadOfLimit(nominalKbps) {
|
|
142
|
-
return part(maxrateKbpsFor(nominalKbps) / 1000, PEAK_CLASS.KNOWN);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* The soundtrack part of a load: the track THIS viewer receives, in the mode
|
|
147
|
-
* it is actually produced in.
|
|
148
|
-
*
|
|
149
|
-
* Copied, the track weighs what its codec allows it to — the bound its
|
|
150
|
-
* configuration is held to, `known` — and where no such bound is confirmed, the
|
|
151
|
-
* average the file states for it, `estimated`. Re-encoded, it weighs the rate
|
|
152
|
-
* the encoder is asked for, and that is `estimated` too: ffmpeg's AAC encoder
|
|
153
|
-
* treats `-b:a` as a target its rate control drifts towards, not a limit
|
|
154
|
-
* (`libavcodec/aacenc.c`, 8.1.2), and the 6144-bit frame limit it does hold
|
|
155
|
-
* cannot be turned into a rate without the output's sampling frequency, which
|
|
156
|
-
* the command does not fix. `research/soundtrack-rate-bound-2026-10-01.md`.
|
|
157
|
-
*
|
|
158
|
-
* @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} entry -
|
|
159
|
-
* The inventory entry of the track; null when nothing describes it.
|
|
160
|
-
* @param {boolean | null} transcode - How it reaches the viewer; null when the
|
|
161
|
-
* viewer receives no sound from this output or beside it.
|
|
162
|
-
* @returns {LoadPart | null}
|
|
163
|
-
*/
|
|
164
|
-
export function soundtrackLoadOf(entry, transcode) {
|
|
165
|
-
if (transcode === null || transcode === undefined) {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
if (transcode === true) {
|
|
169
|
-
return part(AUDIO_TRANSCODE_KBPS / 1000, PEAK_CLASS.ESTIMATED);
|
|
170
|
-
}
|
|
171
|
-
const peakKbps = Number(entry?.peakKbps);
|
|
172
|
-
if (Number.isFinite(peakKbps) && peakKbps > 0) {
|
|
173
|
-
return part(peakKbps / 1000, PEAK_CLASS.KNOWN);
|
|
174
|
-
}
|
|
175
|
-
return part((Number(entry?.bitrateKbps) || 0) / 1000, PEAK_CLASS.ESTIMATED);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Whether a copied track has a figure for the link to be asked about: a
|
|
180
|
-
* confirmed bound, or a rate the file states.
|
|
181
|
-
*
|
|
182
|
-
* @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} entry
|
|
183
|
-
* @returns {boolean}
|
|
184
|
-
*/
|
|
185
|
-
export function soundtrackHasFigure(entry) {
|
|
186
|
-
const peakKbps = Number(entry?.peakKbps);
|
|
187
|
-
const bitrateKbps = Number(entry?.bitrateKbps);
|
|
188
|
-
return (Number.isFinite(peakKbps) && peakKbps > 0) || (Number.isFinite(bitrateKbps) && bitrateKbps > 0);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* How a soundtrack is produced for a viewer, decided once, when they choose it.
|
|
193
|
-
*
|
|
194
|
-
* 1. the browser plays the track as it is and the track has a figure — copied;
|
|
195
|
-
* 2. the browser cannot play it, or it has no figure at all — re-encoded to AAC,
|
|
196
|
-
* where re-encoding is allowed. Without a figure the link cannot be asked
|
|
197
|
-
* whether a copy fits it, and the re-encode is the soundtrack that does have
|
|
198
|
-
* one;
|
|
199
|
-
* 3. re-encoding not allowed: a track the browser plays is copied, figure or
|
|
200
|
-
* not, and the link's own answer about it stands; a track it cannot play
|
|
201
|
-
* cannot be served at all.
|
|
202
|
-
*
|
|
203
|
-
* @param {object} params
|
|
204
|
-
* @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} params.entry
|
|
205
|
-
* @param {boolean} params.browserPlays - Whether the viewer's browser can play
|
|
206
|
-
* this track's codec as it stands.
|
|
207
|
-
* @param {boolean} params.transcodeAllowed
|
|
208
|
-
* @returns {{ transcode: boolean | null, cause: string }} `transcode: null`
|
|
209
|
-
* when the track cannot be served. `cause` is one of {@link SOUNDTRACK_MODE_CAUSE}.
|
|
210
|
-
*/
|
|
211
|
-
export function chooseSoundtrackMode({ entry, browserPlays, transcodeAllowed }) {
|
|
212
|
-
const hasFigure = soundtrackHasFigure(entry);
|
|
213
|
-
if (browserPlays && hasFigure) {
|
|
214
|
-
return { transcode: false, cause: SOUNDTRACK_MODE_CAUSE.COPY };
|
|
215
|
-
}
|
|
216
|
-
if (transcodeAllowed) {
|
|
217
|
-
return { transcode: true, cause: browserPlays ? SOUNDTRACK_MODE_CAUSE.NO_FIGURE : SOUNDTRACK_MODE_CAUSE.UNPLAYABLE };
|
|
218
|
-
}
|
|
219
|
-
return browserPlays
|
|
220
|
-
? { transcode: false, cause: SOUNDTRACK_MODE_CAUSE.COPY_WITHOUT_FIGURE }
|
|
221
|
-
: { transcode: null, cause: SOUNDTRACK_MODE_CAUSE.CANNOT_SERVE };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** Why a soundtrack is produced the way {@link chooseSoundtrackMode} decided. */
|
|
225
|
-
export const SOUNDTRACK_MODE_CAUSE = Object.freeze({
|
|
226
|
-
COPY: "copied: the browser plays it and its rate is stated",
|
|
227
|
-
NO_FIGURE: "re-encoded: nothing states how much a copy of it would carry",
|
|
228
|
-
UNPLAYABLE: "re-encoded: the browser cannot play it as it is",
|
|
229
|
-
COPY_WITHOUT_FIGURE: "copied without a stated rate: re-encoding is not allowed here",
|
|
230
|
-
CANNOT_SERVE: "the browser cannot play it and re-encoding is not allowed here"
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* The whole load: the parts added, and the class of the least trustworthy.
|
|
235
|
-
*
|
|
236
|
-
* @param {LoadPart | null} video
|
|
237
|
-
* @param {LoadPart | null} audio
|
|
238
|
-
* @returns {Load}
|
|
239
|
-
*/
|
|
240
|
-
export function loadOf(video, audio) {
|
|
241
|
-
const parts = [video, audio].filter(Boolean);
|
|
242
|
-
const peakClass = parts.reduce(
|
|
243
|
-
(worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
|
|
244
|
-
PEAK_CLASS.KNOWN
|
|
245
|
-
);
|
|
246
|
-
const totalMbps = peakClass === PEAK_CLASS.UNKNOWN
|
|
247
|
-
? null
|
|
248
|
-
: parts.reduce((sum, one) => sum + one.mbps, 0);
|
|
249
|
-
return { video, audio, totalMbps, peakClass };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* What a link that measured `linkMbps` does with a load.
|
|
254
|
-
*
|
|
255
|
-
* 1. nothing measured the link — every class is admitted, since the link gave
|
|
256
|
-
* no ground to refuse, and nothing is confirmed;
|
|
257
|
-
* 2. an `unknown` load against a measured link — `no safe bound`, refused:
|
|
258
|
-
* there is no number to compare;
|
|
259
|
-
* 3. otherwise the load is compared with the usable share of the link: a
|
|
260
|
-
* `known` load that passes FITS and is confirmed, an `estimated` one that
|
|
261
|
-
* passes is admitted by estimate and is NOT confirmed.
|
|
262
|
-
*
|
|
263
|
-
* @param {number | null} linkMbps
|
|
264
|
-
* @param {Load} load
|
|
265
|
-
* @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
|
|
266
|
-
*/
|
|
267
|
-
export function linkCouldCarry(linkMbps, load) {
|
|
268
|
-
const measured = Number.isFinite(linkMbps) && linkMbps > 0;
|
|
269
|
-
const answer = (verdict, admitted, confirmed) => ({
|
|
270
|
-
verdict,
|
|
271
|
-
admitted,
|
|
272
|
-
confirmed,
|
|
273
|
-
linkMbps: measured ? linkMbps : null,
|
|
274
|
-
load
|
|
275
|
-
});
|
|
276
|
-
if (!measured) {
|
|
277
|
-
return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
|
|
278
|
-
}
|
|
279
|
-
if (load.peakClass === PEAK_CLASS.UNKNOWN) {
|
|
280
|
-
return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
|
|
281
|
-
}
|
|
282
|
-
if (linkMbps * LINK_SAFETY < load.totalMbps) {
|
|
283
|
-
return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
|
|
284
|
-
}
|
|
285
|
-
return load.peakClass === PEAK_CLASS.KNOWN
|
|
286
|
-
? answer(LINK_VERDICT.FITS, true, true)
|
|
287
|
-
: answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Why a link refused a load, in words naming what was missing or too much.
|
|
292
|
-
*
|
|
293
|
-
* `no safe bound` names every part that has no figure — the picture, the
|
|
294
|
-
* soundtrack or both — because that, and not the link, is what the refusal is
|
|
295
|
-
* about. Any other refusal is about the size of the load and is described by
|
|
296
|
-
* the caller, who knows which outputs were weighed.
|
|
297
|
-
*
|
|
298
|
-
* @param {{ verdict: string, load: Load }} answer
|
|
299
|
-
* @param {string} otherwise - The words for a load that is too large.
|
|
300
|
-
* @returns {string}
|
|
301
|
-
*/
|
|
302
|
-
export function linkRefusalReason(answer, otherwise) {
|
|
303
|
-
if (answer?.verdict !== LINK_VERDICT.NO_SAFE_BOUND) {
|
|
304
|
-
return otherwise;
|
|
305
|
-
}
|
|
306
|
-
const missing = [
|
|
307
|
-
answer.load?.video?.peakClass === PEAK_CLASS.UNKNOWN ? "the picture" : "",
|
|
308
|
-
answer.load?.audio?.peakClass === PEAK_CLASS.UNKNOWN ? "the soundtrack" : ""
|
|
309
|
-
].filter(Boolean);
|
|
310
|
-
return `nothing states how much ${missing.length > 0 ? missing.join(" or ") : "this output"} would send, ` +
|
|
311
|
-
"so this viewer's link cannot be asked whether it fits";
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* How strongly an answer is admitted, for ordering: a load whose figure is a
|
|
316
|
-
* bound before one whose figure is an average, and both before one with no
|
|
317
|
-
* figure (which is only ever admitted with nothing measured). Zero for a
|
|
318
|
-
* refusal.
|
|
319
|
-
*
|
|
320
|
-
* @param {{ admitted: boolean, load: Load }} answer
|
|
321
|
-
* @returns {number}
|
|
322
|
-
*/
|
|
323
|
-
export function admissionRank(answer) {
|
|
324
|
-
if (!answer.admitted) {
|
|
325
|
-
return 0;
|
|
326
|
-
}
|
|
327
|
-
return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* The figures behind an answer, as plain values a log line or a report can
|
|
332
|
-
* carry.
|
|
333
|
-
*
|
|
334
|
-
* @param {{ verdict: string, linkMbps: number | null, load: Load }} answer
|
|
335
|
-
* @returns {object}
|
|
336
|
-
*/
|
|
337
|
-
export function linkAnswerFigures(answer) {
|
|
338
|
-
return {
|
|
339
|
-
verdict: answer.verdict,
|
|
340
|
-
linkMbps: answer.linkMbps,
|
|
341
|
-
linkSafety: LINK_SAFETY,
|
|
342
|
-
videoMbps: answer.load.video?.mbps ?? null,
|
|
343
|
-
videoClass: answer.load.video?.peakClass ?? null,
|
|
344
|
-
audioMbps: answer.load.audio?.mbps ?? null,
|
|
345
|
-
audioClass: answer.load.audio?.peakClass ?? null,
|
|
346
|
-
totalMbps: answer.load.totalMbps,
|
|
347
|
-
peakClass: answer.load.peakClass
|
|
348
|
-
};
|
|
349
|
-
}
|
|
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 stream is held to: a re-encode's `maxrate`, or the
|
|
14
|
+
* most a copied soundtrack's codec allows it to carry;
|
|
15
|
+
* 2. `estimated` — an average: a copied picture's or soundtrack's rate as the
|
|
16
|
+
* FILE states it, or the target a re-encoded soundtrack is asked for. An
|
|
17
|
+
* average is not a bound, so a load built on it is admitted only by
|
|
18
|
+
* estimate and is never confirmed;
|
|
19
|
+
* 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
|
|
20
|
+
* soundtrack whose rate nothing states.
|
|
21
|
+
*
|
|
22
|
+
* A load is as trustworthy as its least trustworthy part.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { AUDIO_TRANSCODE_KBPS, maxrateKbpsFor, nominalKbpsFor } from "../args.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How much of a measured link may be spent on the stream.
|
|
29
|
+
*
|
|
30
|
+
* The rest is what the link does when it is not being perfect: transport
|
|
31
|
+
* framing, retransmission, the other tabs, the moment somebody else in the
|
|
32
|
+
* house starts something. A load sized to the whole reading stalls on the
|
|
33
|
+
* first of those.
|
|
34
|
+
*
|
|
35
|
+
* Chosen, not measured, and written as a constant rather than dressed up as a
|
|
36
|
+
* measurement. What would replace it is a reading of the transport's own
|
|
37
|
+
* overhead and of how far a link's throughput varies over a session, which
|
|
38
|
+
* nothing takes.
|
|
39
|
+
*/
|
|
40
|
+
export const LINK_SAFETY = 0.8;
|
|
41
|
+
|
|
42
|
+
/** How far a figure in a load can be trusted. */
|
|
43
|
+
export const PEAK_CLASS = Object.freeze({
|
|
44
|
+
KNOWN: "known",
|
|
45
|
+
ESTIMATED: "estimated",
|
|
46
|
+
UNKNOWN: "unknown"
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** What a link was found to do with a load. */
|
|
50
|
+
export const LINK_VERDICT = Object.freeze({
|
|
51
|
+
FITS: "fits",
|
|
52
|
+
ESTIMATED_TO_FIT: "estimated to fit",
|
|
53
|
+
DOES_NOT_FIT: "does not fit",
|
|
54
|
+
NO_SAFE_BOUND: "no safe bound",
|
|
55
|
+
NO_MEASUREMENT: "no measurement"
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {{ mbps: number | null, peakClass: string }} LoadPart
|
|
62
|
+
* @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {number | null} mbps
|
|
67
|
+
* @param {string} peakClass
|
|
68
|
+
* @returns {LoadPart}
|
|
69
|
+
*/
|
|
70
|
+
function part(mbps, peakClass) {
|
|
71
|
+
const figure = Number.isFinite(mbps) && mbps > 0 ? mbps : null;
|
|
72
|
+
// A figure that is missing is no figure, whatever the part claimed to be.
|
|
73
|
+
return { mbps: figure, peakClass: figure === null ? PEAK_CLASS.UNKNOWN : peakClass };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The picture part of a load, read from an output's identity.
|
|
78
|
+
*
|
|
79
|
+
* An encoder given no limit states no bound, and its part is `unknown` — unless
|
|
80
|
+
* this host has seen outputs of exactly this mode carry a peak (roadmap item
|
|
81
|
+
* 97, step 14), and then that peak is its figure, as an ESTIMATE: it is what
|
|
82
|
+
* was seen, not a bound anything holds the encoder to.
|
|
83
|
+
*
|
|
84
|
+
* @param {import("../output/OutputSpec.js").OutputSpec} spec
|
|
85
|
+
* @param {number | null} sourceMbps - The average rate the source file states.
|
|
86
|
+
* @param {number | null} [observedPeakMbps] - The largest peak outputs of this
|
|
87
|
+
* mode were seen carrying on this configuration, or null.
|
|
88
|
+
* @returns {LoadPart | null} Null for an output that carries no picture.
|
|
89
|
+
*/
|
|
90
|
+
export function videoLoadOfSpec(spec, sourceMbps, observedPeakMbps = null) {
|
|
91
|
+
if (!spec?.video) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const encode = spec.video.encode;
|
|
95
|
+
if (!encode) {
|
|
96
|
+
return part(sourceMbps, PEAK_CLASS.ESTIMATED);
|
|
97
|
+
}
|
|
98
|
+
const rateControl = encode.rateControl;
|
|
99
|
+
if (rateControl) {
|
|
100
|
+
return part(rateControl.maxrateKbps / 1000, PEAK_CLASS.KNOWN);
|
|
101
|
+
}
|
|
102
|
+
return Number.isFinite(observedPeakMbps) && observedPeakMbps > 0
|
|
103
|
+
? part(observedPeakMbps, PEAK_CLASS.ESTIMATED)
|
|
104
|
+
: part(null, PEAK_CLASS.UNKNOWN);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The picture part of a load for a height nothing has produced yet, the way
|
|
109
|
+
* it WOULD be produced here: at the nominal limit on the software encoder,
|
|
110
|
+
* with no limit on a hardware one, and as the source's own stream when the
|
|
111
|
+
* picture is copied at the source's height.
|
|
112
|
+
*
|
|
113
|
+
* The nominal limit is the one of the FRAME the height would be encoded at,
|
|
114
|
+
* chosen by its area (`nominalKbpsFor`) — the same row the output is then
|
|
115
|
+
* opened in, so the offer and the output cannot price one step differently.
|
|
116
|
+
*
|
|
117
|
+
* @param {{ sourceHeight: number, copiesAtSource: boolean, sourceMbps: number | null, encoderKind: string }} picture
|
|
118
|
+
* @param {{ width: number, height: number }} frame - The frame this height is
|
|
119
|
+
* encoded at for this source.
|
|
120
|
+
* @returns {LoadPart}
|
|
121
|
+
*/
|
|
122
|
+
export function videoLoadForFrame(picture, frame) {
|
|
123
|
+
const sourceHeight = Math.round(Number(picture?.sourceHeight) || 0);
|
|
124
|
+
if (picture?.copiesAtSource === true && frame.height === sourceHeight) {
|
|
125
|
+
return part(picture?.sourceMbps ?? null, PEAK_CLASS.ESTIMATED);
|
|
126
|
+
}
|
|
127
|
+
if (picture?.encoderKind !== "software") {
|
|
128
|
+
return part(null, PEAK_CLASS.UNKNOWN);
|
|
129
|
+
}
|
|
130
|
+
return part(maxrateKbpsFor(nominalKbpsFor(frame)) / 1000, PEAK_CLASS.KNOWN);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The picture part of a load for a software encode at a nominal limit nothing
|
|
135
|
+
* has produced yet. The bound is the one `softwareRateControlFor` would give
|
|
136
|
+
* it, so an output opened at that limit carries exactly this load.
|
|
137
|
+
*
|
|
138
|
+
* @param {number} nominalKbps
|
|
139
|
+
* @returns {LoadPart}
|
|
140
|
+
*/
|
|
141
|
+
export function videoLoadOfLimit(nominalKbps) {
|
|
142
|
+
return part(maxrateKbpsFor(nominalKbps) / 1000, PEAK_CLASS.KNOWN);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The soundtrack part of a load: the track THIS viewer receives, in the mode
|
|
147
|
+
* it is actually produced in.
|
|
148
|
+
*
|
|
149
|
+
* Copied, the track weighs what its codec allows it to — the bound its
|
|
150
|
+
* configuration is held to, `known` — and where no such bound is confirmed, the
|
|
151
|
+
* average the file states for it, `estimated`. Re-encoded, it weighs the rate
|
|
152
|
+
* the encoder is asked for, and that is `estimated` too: ffmpeg's AAC encoder
|
|
153
|
+
* treats `-b:a` as a target its rate control drifts towards, not a limit
|
|
154
|
+
* (`libavcodec/aacenc.c`, 8.1.2), and the 6144-bit frame limit it does hold
|
|
155
|
+
* cannot be turned into a rate without the output's sampling frequency, which
|
|
156
|
+
* the command does not fix. `research/soundtrack-rate-bound-2026-10-01.md`.
|
|
157
|
+
*
|
|
158
|
+
* @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} entry -
|
|
159
|
+
* The inventory entry of the track; null when nothing describes it.
|
|
160
|
+
* @param {boolean | null} transcode - How it reaches the viewer; null when the
|
|
161
|
+
* viewer receives no sound from this output or beside it.
|
|
162
|
+
* @returns {LoadPart | null}
|
|
163
|
+
*/
|
|
164
|
+
export function soundtrackLoadOf(entry, transcode) {
|
|
165
|
+
if (transcode === null || transcode === undefined) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (transcode === true) {
|
|
169
|
+
return part(AUDIO_TRANSCODE_KBPS / 1000, PEAK_CLASS.ESTIMATED);
|
|
170
|
+
}
|
|
171
|
+
const peakKbps = Number(entry?.peakKbps);
|
|
172
|
+
if (Number.isFinite(peakKbps) && peakKbps > 0) {
|
|
173
|
+
return part(peakKbps / 1000, PEAK_CLASS.KNOWN);
|
|
174
|
+
}
|
|
175
|
+
return part((Number(entry?.bitrateKbps) || 0) / 1000, PEAK_CLASS.ESTIMATED);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Whether a copied track has a figure for the link to be asked about: a
|
|
180
|
+
* confirmed bound, or a rate the file states.
|
|
181
|
+
*
|
|
182
|
+
* @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} entry
|
|
183
|
+
* @returns {boolean}
|
|
184
|
+
*/
|
|
185
|
+
export function soundtrackHasFigure(entry) {
|
|
186
|
+
const peakKbps = Number(entry?.peakKbps);
|
|
187
|
+
const bitrateKbps = Number(entry?.bitrateKbps);
|
|
188
|
+
return (Number.isFinite(peakKbps) && peakKbps > 0) || (Number.isFinite(bitrateKbps) && bitrateKbps > 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* How a soundtrack is produced for a viewer, decided once, when they choose it.
|
|
193
|
+
*
|
|
194
|
+
* 1. the browser plays the track as it is and the track has a figure — copied;
|
|
195
|
+
* 2. the browser cannot play it, or it has no figure at all — re-encoded to AAC,
|
|
196
|
+
* where re-encoding is allowed. Without a figure the link cannot be asked
|
|
197
|
+
* whether a copy fits it, and the re-encode is the soundtrack that does have
|
|
198
|
+
* one;
|
|
199
|
+
* 3. re-encoding not allowed: a track the browser plays is copied, figure or
|
|
200
|
+
* not, and the link's own answer about it stands; a track it cannot play
|
|
201
|
+
* cannot be served at all.
|
|
202
|
+
*
|
|
203
|
+
* @param {object} params
|
|
204
|
+
* @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} params.entry
|
|
205
|
+
* @param {boolean} params.browserPlays - Whether the viewer's browser can play
|
|
206
|
+
* this track's codec as it stands.
|
|
207
|
+
* @param {boolean} params.transcodeAllowed
|
|
208
|
+
* @returns {{ transcode: boolean | null, cause: string }} `transcode: null`
|
|
209
|
+
* when the track cannot be served. `cause` is one of {@link SOUNDTRACK_MODE_CAUSE}.
|
|
210
|
+
*/
|
|
211
|
+
export function chooseSoundtrackMode({ entry, browserPlays, transcodeAllowed }) {
|
|
212
|
+
const hasFigure = soundtrackHasFigure(entry);
|
|
213
|
+
if (browserPlays && hasFigure) {
|
|
214
|
+
return { transcode: false, cause: SOUNDTRACK_MODE_CAUSE.COPY };
|
|
215
|
+
}
|
|
216
|
+
if (transcodeAllowed) {
|
|
217
|
+
return { transcode: true, cause: browserPlays ? SOUNDTRACK_MODE_CAUSE.NO_FIGURE : SOUNDTRACK_MODE_CAUSE.UNPLAYABLE };
|
|
218
|
+
}
|
|
219
|
+
return browserPlays
|
|
220
|
+
? { transcode: false, cause: SOUNDTRACK_MODE_CAUSE.COPY_WITHOUT_FIGURE }
|
|
221
|
+
: { transcode: null, cause: SOUNDTRACK_MODE_CAUSE.CANNOT_SERVE };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Why a soundtrack is produced the way {@link chooseSoundtrackMode} decided. */
|
|
225
|
+
export const SOUNDTRACK_MODE_CAUSE = Object.freeze({
|
|
226
|
+
COPY: "copied: the browser plays it and its rate is stated",
|
|
227
|
+
NO_FIGURE: "re-encoded: nothing states how much a copy of it would carry",
|
|
228
|
+
UNPLAYABLE: "re-encoded: the browser cannot play it as it is",
|
|
229
|
+
COPY_WITHOUT_FIGURE: "copied without a stated rate: re-encoding is not allowed here",
|
|
230
|
+
CANNOT_SERVE: "the browser cannot play it and re-encoding is not allowed here"
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The whole load: the parts added, and the class of the least trustworthy.
|
|
235
|
+
*
|
|
236
|
+
* @param {LoadPart | null} video
|
|
237
|
+
* @param {LoadPart | null} audio
|
|
238
|
+
* @returns {Load}
|
|
239
|
+
*/
|
|
240
|
+
export function loadOf(video, audio) {
|
|
241
|
+
const parts = [video, audio].filter(Boolean);
|
|
242
|
+
const peakClass = parts.reduce(
|
|
243
|
+
(worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
|
|
244
|
+
PEAK_CLASS.KNOWN
|
|
245
|
+
);
|
|
246
|
+
const totalMbps = peakClass === PEAK_CLASS.UNKNOWN
|
|
247
|
+
? null
|
|
248
|
+
: parts.reduce((sum, one) => sum + one.mbps, 0);
|
|
249
|
+
return { video, audio, totalMbps, peakClass };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* What a link that measured `linkMbps` does with a load.
|
|
254
|
+
*
|
|
255
|
+
* 1. nothing measured the link — every class is admitted, since the link gave
|
|
256
|
+
* no ground to refuse, and nothing is confirmed;
|
|
257
|
+
* 2. an `unknown` load against a measured link — `no safe bound`, refused:
|
|
258
|
+
* there is no number to compare;
|
|
259
|
+
* 3. otherwise the load is compared with the usable share of the link: a
|
|
260
|
+
* `known` load that passes FITS and is confirmed, an `estimated` one that
|
|
261
|
+
* passes is admitted by estimate and is NOT confirmed.
|
|
262
|
+
*
|
|
263
|
+
* @param {number | null} linkMbps
|
|
264
|
+
* @param {Load} load
|
|
265
|
+
* @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
|
|
266
|
+
*/
|
|
267
|
+
export function linkCouldCarry(linkMbps, load) {
|
|
268
|
+
const measured = Number.isFinite(linkMbps) && linkMbps > 0;
|
|
269
|
+
const answer = (verdict, admitted, confirmed) => ({
|
|
270
|
+
verdict,
|
|
271
|
+
admitted,
|
|
272
|
+
confirmed,
|
|
273
|
+
linkMbps: measured ? linkMbps : null,
|
|
274
|
+
load
|
|
275
|
+
});
|
|
276
|
+
if (!measured) {
|
|
277
|
+
return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
|
|
278
|
+
}
|
|
279
|
+
if (load.peakClass === PEAK_CLASS.UNKNOWN) {
|
|
280
|
+
return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
|
|
281
|
+
}
|
|
282
|
+
if (linkMbps * LINK_SAFETY < load.totalMbps) {
|
|
283
|
+
return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
|
|
284
|
+
}
|
|
285
|
+
return load.peakClass === PEAK_CLASS.KNOWN
|
|
286
|
+
? answer(LINK_VERDICT.FITS, true, true)
|
|
287
|
+
: answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Why a link refused a load, in words naming what was missing or too much.
|
|
292
|
+
*
|
|
293
|
+
* `no safe bound` names every part that has no figure — the picture, the
|
|
294
|
+
* soundtrack or both — because that, and not the link, is what the refusal is
|
|
295
|
+
* about. Any other refusal is about the size of the load and is described by
|
|
296
|
+
* the caller, who knows which outputs were weighed.
|
|
297
|
+
*
|
|
298
|
+
* @param {{ verdict: string, load: Load }} answer
|
|
299
|
+
* @param {string} otherwise - The words for a load that is too large.
|
|
300
|
+
* @returns {string}
|
|
301
|
+
*/
|
|
302
|
+
export function linkRefusalReason(answer, otherwise) {
|
|
303
|
+
if (answer?.verdict !== LINK_VERDICT.NO_SAFE_BOUND) {
|
|
304
|
+
return otherwise;
|
|
305
|
+
}
|
|
306
|
+
const missing = [
|
|
307
|
+
answer.load?.video?.peakClass === PEAK_CLASS.UNKNOWN ? "the picture" : "",
|
|
308
|
+
answer.load?.audio?.peakClass === PEAK_CLASS.UNKNOWN ? "the soundtrack" : ""
|
|
309
|
+
].filter(Boolean);
|
|
310
|
+
return `nothing states how much ${missing.length > 0 ? missing.join(" or ") : "this output"} would send, ` +
|
|
311
|
+
"so this viewer's link cannot be asked whether it fits";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* How strongly an answer is admitted, for ordering: a load whose figure is a
|
|
316
|
+
* bound before one whose figure is an average, and both before one with no
|
|
317
|
+
* figure (which is only ever admitted with nothing measured). Zero for a
|
|
318
|
+
* refusal.
|
|
319
|
+
*
|
|
320
|
+
* @param {{ admitted: boolean, load: Load }} answer
|
|
321
|
+
* @returns {number}
|
|
322
|
+
*/
|
|
323
|
+
export function admissionRank(answer) {
|
|
324
|
+
if (!answer.admitted) {
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The figures behind an answer, as plain values a log line or a report can
|
|
332
|
+
* carry.
|
|
333
|
+
*
|
|
334
|
+
* @param {{ verdict: string, linkMbps: number | null, load: Load }} answer
|
|
335
|
+
* @returns {object}
|
|
336
|
+
*/
|
|
337
|
+
export function linkAnswerFigures(answer) {
|
|
338
|
+
return {
|
|
339
|
+
verdict: answer.verdict,
|
|
340
|
+
linkMbps: answer.linkMbps,
|
|
341
|
+
linkSafety: LINK_SAFETY,
|
|
342
|
+
videoMbps: answer.load.video?.mbps ?? null,
|
|
343
|
+
videoClass: answer.load.video?.peakClass ?? null,
|
|
344
|
+
audioMbps: answer.load.audio?.mbps ?? null,
|
|
345
|
+
audioClass: answer.load.audio?.peakClass ?? null,
|
|
346
|
+
totalMbps: answer.load.totalMbps,
|
|
347
|
+
peakClass: answer.load.peakClass
|
|
348
|
+
};
|
|
349
|
+
}
|