@torrent-tv/proxy 2.87.3 → 2.87.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2157 -2153
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +94 -94
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -181
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +721 -720
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +212 -212
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +856 -848
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +36 -36
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2635
- package/services/torrent/torrent-source-key.js +38 -38
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -760
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +365 -350
- package/services/torrent/worker/protocol.js +184 -182
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1116
- package/services/transport/data-channel-handler.js +1356 -1356
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +73 -73
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +176 -176
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -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/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { bodySender } from "../services/transport/data-channel-handler.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Drives the sender over a body arriving in the given read sizes, and answers
|
|
7
|
-
* both what reached the far end and what was written — so "the body is
|
|
8
|
-
* unchanged" is asserted against the body rather than against a length.
|
|
9
|
-
*
|
|
10
|
-
* @param {number} sizeBytes
|
|
11
|
-
* @param {number[]} readSizes - The sizes the body read hands over, in order.
|
|
12
|
-
* @returns {{ sent: Buffer[], counted: number, written: Buffer }}
|
|
13
|
-
*/
|
|
14
|
-
function sendBody(sizeBytes, readSizes) {
|
|
15
|
-
/** @type {Buffer[]} */
|
|
16
|
-
const sent = [];
|
|
17
|
-
const sender = bodySender((bytes) => sent.push(Buffer.from(bytes)), sizeBytes);
|
|
18
|
-
/** @type {Buffer[]} */
|
|
19
|
-
const written = [];
|
|
20
|
-
let counted = 0;
|
|
21
|
-
let next = 0;
|
|
22
|
-
for (const size of readSizes) {
|
|
23
|
-
const block = Buffer.alloc(size);
|
|
24
|
-
for (let index = 0; index < size; index += 1) {
|
|
25
|
-
block[index] = next & 0xff;
|
|
26
|
-
next += 1;
|
|
27
|
-
}
|
|
28
|
-
written.push(block);
|
|
29
|
-
counted += sender.push(block);
|
|
30
|
-
}
|
|
31
|
-
counted += sender.flush();
|
|
32
|
-
return { sent, counted, written: Buffer.concat(written) };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test("with no size chosen, one body read is one message", () => {
|
|
36
|
-
const { sent, counted } = sendBody(0, [1000, 2000, 3]);
|
|
37
|
-
assert.deepEqual(
|
|
38
|
-
sent.map((message) => message.length),
|
|
39
|
-
[1000, 2000, 3]
|
|
40
|
-
);
|
|
41
|
-
assert.equal(counted, 3);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("a smaller size cuts a body read into messages of exactly that size", () => {
|
|
45
|
-
const { sent, counted } = sendBody(400, [1000]);
|
|
46
|
-
assert.deepEqual(
|
|
47
|
-
sent.map((message) => message.length),
|
|
48
|
-
[400, 400, 200]
|
|
49
|
-
);
|
|
50
|
-
assert.equal(counted, 3);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("a larger size joins body reads until it is filled", () => {
|
|
54
|
-
const { sent } = sendBody(2500, [1000, 1000, 1000, 1000]);
|
|
55
|
-
assert.deepEqual(
|
|
56
|
-
sent.map((message) => message.length),
|
|
57
|
-
[2500, 1500]
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("every byte arrives once, in order, whatever the size", () => {
|
|
62
|
-
for (const size of [0, 1, 7, 400, 2500, 999_999]) {
|
|
63
|
-
const { sent, written } = sendBody(size, [1000, 3, 5000, 17]);
|
|
64
|
-
assert.deepEqual(Buffer.concat(sent), written, `size ${size} changed the body`);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("a size larger than the whole body still sends it, once, on flush", () => {
|
|
69
|
-
const { sent, written, counted } = sendBody(10_000, [100, 200]);
|
|
70
|
-
assert.equal(sent.length, 1);
|
|
71
|
-
assert.equal(counted, 1);
|
|
72
|
-
assert.deepEqual(sent[0], written);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("the count is of messages, which is what a reading is attributed to", () => {
|
|
76
|
-
// 163 messages for 10 657 210 bytes is the field reading of 2026-09-12; at
|
|
77
|
-
// 16 KB the same body is 651 of them, and that difference is the measurement.
|
|
78
|
-
const { counted } = sendBody(16 * 1024, [10_657_210]);
|
|
79
|
-
assert.equal(counted, Math.ceil(10_657_210 / (16 * 1024)));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("nothing is sent for an empty body, and flush has nothing to release", () => {
|
|
83
|
-
const { sent, counted } = sendBody(400, []);
|
|
84
|
-
assert.deepEqual(sent, []);
|
|
85
|
-
assert.equal(counted, 0);
|
|
86
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { bodySender } from "../services/transport/data-channel-handler.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Drives the sender over a body arriving in the given read sizes, and answers
|
|
7
|
+
* both what reached the far end and what was written — so "the body is
|
|
8
|
+
* unchanged" is asserted against the body rather than against a length.
|
|
9
|
+
*
|
|
10
|
+
* @param {number} sizeBytes
|
|
11
|
+
* @param {number[]} readSizes - The sizes the body read hands over, in order.
|
|
12
|
+
* @returns {{ sent: Buffer[], counted: number, written: Buffer }}
|
|
13
|
+
*/
|
|
14
|
+
function sendBody(sizeBytes, readSizes) {
|
|
15
|
+
/** @type {Buffer[]} */
|
|
16
|
+
const sent = [];
|
|
17
|
+
const sender = bodySender((bytes) => sent.push(Buffer.from(bytes)), sizeBytes);
|
|
18
|
+
/** @type {Buffer[]} */
|
|
19
|
+
const written = [];
|
|
20
|
+
let counted = 0;
|
|
21
|
+
let next = 0;
|
|
22
|
+
for (const size of readSizes) {
|
|
23
|
+
const block = Buffer.alloc(size);
|
|
24
|
+
for (let index = 0; index < size; index += 1) {
|
|
25
|
+
block[index] = next & 0xff;
|
|
26
|
+
next += 1;
|
|
27
|
+
}
|
|
28
|
+
written.push(block);
|
|
29
|
+
counted += sender.push(block);
|
|
30
|
+
}
|
|
31
|
+
counted += sender.flush();
|
|
32
|
+
return { sent, counted, written: Buffer.concat(written) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("with no size chosen, one body read is one message", () => {
|
|
36
|
+
const { sent, counted } = sendBody(0, [1000, 2000, 3]);
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
sent.map((message) => message.length),
|
|
39
|
+
[1000, 2000, 3]
|
|
40
|
+
);
|
|
41
|
+
assert.equal(counted, 3);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a smaller size cuts a body read into messages of exactly that size", () => {
|
|
45
|
+
const { sent, counted } = sendBody(400, [1000]);
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
sent.map((message) => message.length),
|
|
48
|
+
[400, 400, 200]
|
|
49
|
+
);
|
|
50
|
+
assert.equal(counted, 3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a larger size joins body reads until it is filled", () => {
|
|
54
|
+
const { sent } = sendBody(2500, [1000, 1000, 1000, 1000]);
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
sent.map((message) => message.length),
|
|
57
|
+
[2500, 1500]
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("every byte arrives once, in order, whatever the size", () => {
|
|
62
|
+
for (const size of [0, 1, 7, 400, 2500, 999_999]) {
|
|
63
|
+
const { sent, written } = sendBody(size, [1000, 3, 5000, 17]);
|
|
64
|
+
assert.deepEqual(Buffer.concat(sent), written, `size ${size} changed the body`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a size larger than the whole body still sends it, once, on flush", () => {
|
|
69
|
+
const { sent, written, counted } = sendBody(10_000, [100, 200]);
|
|
70
|
+
assert.equal(sent.length, 1);
|
|
71
|
+
assert.equal(counted, 1);
|
|
72
|
+
assert.deepEqual(sent[0], written);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("the count is of messages, which is what a reading is attributed to", () => {
|
|
76
|
+
// 163 messages for 10 657 210 bytes is the field reading of 2026-09-12; at
|
|
77
|
+
// 16 KB the same body is 651 of them, and that difference is the measurement.
|
|
78
|
+
const { counted } = sendBody(16 * 1024, [10_657_210]);
|
|
79
|
+
assert.equal(counted, Math.ceil(10_657_210 / (16 * 1024)));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("nothing is sent for an empty body, and flush has nothing to release", () => {
|
|
83
|
+
const { sent, counted } = sendBody(400, []);
|
|
84
|
+
assert.deepEqual(sent, []);
|
|
85
|
+
assert.equal(counted, 0);
|
|
86
|
+
});
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file An output already made serves a viewer when the rules decided with the
|
|
3
|
-
* user 2026-09-16 allow it, through the paths a real request takes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { writeFileSync } from "node:fs";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
11
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
12
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
13
|
-
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
14
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
15
|
-
import { Output } from "../services/encode/output/Output.js";
|
|
16
|
-
|
|
17
|
-
const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
|
|
18
|
-
|
|
19
|
-
function hostWithA1080pOutput(t) {
|
|
20
|
-
const { manager, store, cleanup } = managerWithOwnStore({
|
|
21
|
-
startupWaitMs: 0,
|
|
22
|
-
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
23
|
-
});
|
|
24
|
-
t.after(async () => {
|
|
25
|
-
await manager.lifecycle.disposeAll();
|
|
26
|
-
cleanup();
|
|
27
|
-
});
|
|
28
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
29
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
30
|
-
manager.getCachedMediaInfo = () => ({ durationSeconds: 400, width: 1920, height: 1080, fps: 24 });
|
|
31
|
-
const spec = outputSpec({
|
|
32
|
-
sourceKey: TORRENT,
|
|
33
|
-
transcodeVideo: true,
|
|
34
|
-
transcodeAudio: false,
|
|
35
|
-
encoder: "h264_vaapi",
|
|
36
|
-
width: 1920,
|
|
37
|
-
height: 1080,
|
|
38
|
-
fps: 24,
|
|
39
|
-
cutGrid: "uniform"
|
|
40
|
-
});
|
|
41
|
-
const key = spec.toKey();
|
|
42
|
-
store.useFormat(key, fmp4Format);
|
|
43
|
-
// The piece at the start of the film is made.
|
|
44
|
-
writeFileSync(path.join(store.directoryFor(key), fmp4Format.segmentFileName(0)), Buffer.alloc(100));
|
|
45
|
-
const file = new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }).learn({ width: 1920, height: 1080, durationSeconds: 400 });
|
|
46
|
-
const made = {
|
|
47
|
-
id: spec.toName(),
|
|
48
|
-
spec,
|
|
49
|
-
get outputKey() { return this.spec.toKey(); },
|
|
50
|
-
file,
|
|
51
|
-
timeline: new Timeline({ boundaries: Array.from({ length: 101 }, (_, index) => index * 4), cutGrid: "uniform" }),
|
|
52
|
-
output: new Output({ encodeWidth: 1920, encodeHeight: 1080, outputFps: 24 }),
|
|
53
|
-
segmentFormat: fmp4Format,
|
|
54
|
-
useSyntheticPlaylist: true
|
|
55
|
-
};
|
|
56
|
-
manager.outputs.set(made.id, made);
|
|
57
|
-
return { manager, made };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function request(over) {
|
|
61
|
-
return {
|
|
62
|
-
sourceKey: TORRENT,
|
|
63
|
-
fileIndex: 0,
|
|
64
|
-
fileName: "film.mkv",
|
|
65
|
-
transcodeVideo: true,
|
|
66
|
-
transcodeAudio: false,
|
|
67
|
-
segmentFormatId: "fmp4",
|
|
68
|
-
...over
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
test("the automatic choice is served by a better output whose piece at the viewer's position is made", async (t) => {
|
|
73
|
-
const { manager, made } = hostWithA1080pOutput(t);
|
|
74
|
-
const answered = await manager.viewerRequests.createOrGetSession(
|
|
75
|
-
request({ consumerId: "auto-viewer", targetWidth: 1280, targetHeight: 720 })
|
|
76
|
-
);
|
|
77
|
-
assert.equal(answered, made);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("a size picked by hand is not served by another size", async (t) => {
|
|
81
|
-
const { manager, made } = hostWithA1080pOutput(t);
|
|
82
|
-
let answered = null;
|
|
83
|
-
try {
|
|
84
|
-
answered = await manager.viewerRequests.createOrGetSession(
|
|
85
|
-
request({ consumerId: "picker", targetWidth: 1280, targetHeight: 720, exactSize: true })
|
|
86
|
-
);
|
|
87
|
-
} catch {
|
|
88
|
-
answered = null;
|
|
89
|
-
}
|
|
90
|
-
assert.notEqual(answered, made);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("a step asked for by an automatic viewer and served by the picture leaves the picture a picture", async (t) => {
|
|
94
|
-
const { manager, made } = hostWithA1080pOutput(t);
|
|
95
|
-
manager.viewers.of(made, "auto-viewer").qualityMode = "auto";
|
|
96
|
-
|
|
97
|
-
const variant = await manager.renditions.resolveVariantSession(made.id, 720, 0, "auto-viewer");
|
|
98
|
-
|
|
99
|
-
assert.equal(variant, made, "the picture already made at 1080p serves the 720p request");
|
|
100
|
-
assert.notEqual(made.isStep, true, "and is not turned into a step of itself");
|
|
101
|
-
assert.ok(manager.viewers.forOutput(made).has("auto-viewer"));
|
|
102
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file An output already made serves a viewer when the rules decided with the
|
|
3
|
+
* user 2026-09-16 allow it, through the paths a real request takes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { writeFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
11
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
12
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
13
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
14
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
15
|
+
import { Output } from "../services/encode/output/Output.js";
|
|
16
|
+
|
|
17
|
+
const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
|
|
18
|
+
|
|
19
|
+
function hostWithA1080pOutput(t) {
|
|
20
|
+
const { manager, store, cleanup } = managerWithOwnStore({
|
|
21
|
+
startupWaitMs: 0,
|
|
22
|
+
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
23
|
+
});
|
|
24
|
+
t.after(async () => {
|
|
25
|
+
await manager.lifecycle.disposeAll();
|
|
26
|
+
cleanup();
|
|
27
|
+
});
|
|
28
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
29
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
30
|
+
manager.getCachedMediaInfo = () => ({ durationSeconds: 400, width: 1920, height: 1080, fps: 24 });
|
|
31
|
+
const spec = outputSpec({
|
|
32
|
+
sourceKey: TORRENT,
|
|
33
|
+
transcodeVideo: true,
|
|
34
|
+
transcodeAudio: false,
|
|
35
|
+
encoder: "h264_vaapi",
|
|
36
|
+
width: 1920,
|
|
37
|
+
height: 1080,
|
|
38
|
+
fps: 24,
|
|
39
|
+
cutGrid: "uniform"
|
|
40
|
+
});
|
|
41
|
+
const key = spec.toKey();
|
|
42
|
+
store.useFormat(key, fmp4Format);
|
|
43
|
+
// The piece at the start of the film is made.
|
|
44
|
+
writeFileSync(path.join(store.directoryFor(key), fmp4Format.segmentFileName(0)), Buffer.alloc(100));
|
|
45
|
+
const file = new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }).learn({ width: 1920, height: 1080, durationSeconds: 400 });
|
|
46
|
+
const made = {
|
|
47
|
+
id: spec.toName(),
|
|
48
|
+
spec,
|
|
49
|
+
get outputKey() { return this.spec.toKey(); },
|
|
50
|
+
file,
|
|
51
|
+
timeline: new Timeline({ boundaries: Array.from({ length: 101 }, (_, index) => index * 4), cutGrid: "uniform" }),
|
|
52
|
+
output: new Output({ encodeWidth: 1920, encodeHeight: 1080, outputFps: 24 }),
|
|
53
|
+
segmentFormat: fmp4Format,
|
|
54
|
+
useSyntheticPlaylist: true
|
|
55
|
+
};
|
|
56
|
+
manager.outputs.set(made.id, made);
|
|
57
|
+
return { manager, made };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function request(over) {
|
|
61
|
+
return {
|
|
62
|
+
sourceKey: TORRENT,
|
|
63
|
+
fileIndex: 0,
|
|
64
|
+
fileName: "film.mkv",
|
|
65
|
+
transcodeVideo: true,
|
|
66
|
+
transcodeAudio: false,
|
|
67
|
+
segmentFormatId: "fmp4",
|
|
68
|
+
...over
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("the automatic choice is served by a better output whose piece at the viewer's position is made", async (t) => {
|
|
73
|
+
const { manager, made } = hostWithA1080pOutput(t);
|
|
74
|
+
const answered = await manager.viewerRequests.createOrGetSession(
|
|
75
|
+
request({ consumerId: "auto-viewer", targetWidth: 1280, targetHeight: 720 })
|
|
76
|
+
);
|
|
77
|
+
assert.equal(answered, made);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a size picked by hand is not served by another size", async (t) => {
|
|
81
|
+
const { manager, made } = hostWithA1080pOutput(t);
|
|
82
|
+
let answered = null;
|
|
83
|
+
try {
|
|
84
|
+
answered = await manager.viewerRequests.createOrGetSession(
|
|
85
|
+
request({ consumerId: "picker", targetWidth: 1280, targetHeight: 720, exactSize: true })
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
answered = null;
|
|
89
|
+
}
|
|
90
|
+
assert.notEqual(answered, made);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a step asked for by an automatic viewer and served by the picture leaves the picture a picture", async (t) => {
|
|
94
|
+
const { manager, made } = hostWithA1080pOutput(t);
|
|
95
|
+
manager.viewers.of(made, "auto-viewer").qualityMode = "auto";
|
|
96
|
+
|
|
97
|
+
const variant = await manager.renditions.resolveVariantSession(made.id, 720, 0, "auto-viewer");
|
|
98
|
+
|
|
99
|
+
assert.equal(variant, made, "the picture already made at 1080p serves the 720p request");
|
|
100
|
+
assert.notEqual(made.isStep, true, "and is not turned into a step of itself");
|
|
101
|
+
assert.ok(manager.viewers.forOutput(made).has("auto-viewer"));
|
|
102
|
+
});
|
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which existing output serves a viewer: the rules decided with the user
|
|
3
|
-
* 2026-09-16, with the viewer's own link asked in every mode and on every
|
|
4
|
-
* branch (roadmap item 97, step 11).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { chooseServingOutput } from "../services/encode/quality/serving-output.js";
|
|
10
|
-
import { linkCouldCarry, loadOf, PEAK_CLASS } from "../services/encode/quality/link-budget.js";
|
|
11
|
-
|
|
12
|
-
const WANTED = { width: 1824, height: 1026 };
|
|
13
|
-
const NEXT_LOWER = 1280 * 720;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} key
|
|
17
|
-
* @param {number} width
|
|
18
|
-
* @param {number} height
|
|
19
|
-
* @param {{ mbps?: number | null, peakClass?: string, readyHere?: boolean }} [over]
|
|
20
|
-
*/
|
|
21
|
-
function candidate(key, width, height, { mbps = 5, peakClass = PEAK_CLASS.KNOWN, readyHere = true } = {}) {
|
|
22
|
-
return { key, width, height, readyHere, load: loadOf({ mbps, peakClass: mbps === null ? PEAK_CLASS.UNKNOWN : peakClass }, null) };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {object} over
|
|
27
|
-
* @param {number | null} [linkMbps] - What this viewer's link measured.
|
|
28
|
-
*/
|
|
29
|
-
function choose(over, linkMbps = null) {
|
|
30
|
-
return chooseServingOutput({
|
|
31
|
-
mode: "auto",
|
|
32
|
-
wanted: WANTED,
|
|
33
|
-
nextLowerArea: NEXT_LOWER,
|
|
34
|
-
atRisk: false,
|
|
35
|
-
judge: (one) => linkCouldCarry(linkMbps, one.load),
|
|
36
|
-
candidates: [],
|
|
37
|
-
...over
|
|
38
|
-
})?.key ?? null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test("the same quality or higher is served, the nearest of them", () => {
|
|
42
|
-
assert.equal(choose({ candidates: [candidate("2160", 3840, 2160), candidate("1080", 1920, 1080)] }), "1080");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("higher quality is not served over a link it does not fit", () => {
|
|
46
|
-
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { mbps: 12 })] }, 10), null);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("a little lower — above the next rung down — is served, and only if the link admits it", () => {
|
|
50
|
-
assert.equal(choose({ candidates: [candidate("1000", 1776, 1000)] }), "1000");
|
|
51
|
-
assert.equal(
|
|
52
|
-
choose({ candidates: [candidate("1000", 1776, 1000, { mbps: 12 })] }, 10),
|
|
53
|
-
null,
|
|
54
|
-
"a smaller picture proves nothing by itself: its own load is asked"
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("the next rung down or lower is served only when making the wanted format risks a wait, and only if it fits", () => {
|
|
59
|
-
const lower = [candidate("720", 1280, 720), candidate("480", 854, 480)];
|
|
60
|
-
assert.equal(choose({ candidates: lower }), null);
|
|
61
|
-
assert.equal(choose({ candidates: lower, atRisk: true }), "720");
|
|
62
|
-
assert.equal(
|
|
63
|
-
choose({ candidates: [candidate("720", 1280, 720, { mbps: 12 }), candidate("480", 854, 480, { mbps: 3 })], atRisk: true }, 10),
|
|
64
|
-
"480",
|
|
65
|
-
"the larger one does not fit this link, so it is not taken"
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("an output without the piece at the viewer's position spares them nothing", () => {
|
|
70
|
-
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { readyHere: false })] }), null);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("a size chosen by hand is served only exactly, and only if the link admits it", () => {
|
|
74
|
-
const candidates = [candidate("1080", 1920, 1080), candidate("exact", 1824, 1026)];
|
|
75
|
-
assert.equal(choose({ mode: "manual", candidates }), "exact");
|
|
76
|
-
assert.equal(choose({ mode: "manual", candidates: [candidate("1080", 1920, 1080)], atRisk: true }), null);
|
|
77
|
-
assert.equal(
|
|
78
|
-
choose({ mode: "manual", candidates: [candidate("exact", 1824, 1026, { mbps: 12 })] }, 10),
|
|
79
|
-
null,
|
|
80
|
-
"the size they picked, but more than their link carries: not handed over"
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("of two outputs of the size picked by hand, the higher limit the link admits is served", () => {
|
|
85
|
-
const candidates = [candidate("2800", 1824, 1026, { mbps: 3.64 }), candidate("1400", 1824, 1026, { mbps: 1.82 })];
|
|
86
|
-
assert.equal(choose({ mode: "manual", candidates }, 10), "2800");
|
|
87
|
-
assert.equal(choose({ mode: "manual", candidates }, 3), "1400", "the link admits only the lower limit");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("a bound is taken before an average, even when the average's limit is higher", () => {
|
|
91
|
-
const candidates = [
|
|
92
|
-
candidate("estimated", 1824, 1026, { mbps: 6, peakClass: PEAK_CLASS.ESTIMATED }),
|
|
93
|
-
candidate("known", 1824, 1026, { mbps: 4, peakClass: PEAK_CLASS.KNOWN })
|
|
94
|
-
];
|
|
95
|
-
assert.equal(choose({ mode: "manual", candidates }, 10), "known");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("an output with no bound is not served against a measured link, and is while nothing measured it", () => {
|
|
99
|
-
const candidates = [candidate("hardware", 1824, 1026, { mbps: null })];
|
|
100
|
-
assert.equal(choose({ mode: "manual", candidates }, 50), null);
|
|
101
|
-
assert.equal(choose({ mode: "manual", candidates }, null), "hardware");
|
|
102
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Which existing output serves a viewer: the rules decided with the user
|
|
3
|
+
* 2026-09-16, with the viewer's own link asked in every mode and on every
|
|
4
|
+
* branch (roadmap item 97, step 11).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { chooseServingOutput } from "../services/encode/quality/serving-output.js";
|
|
10
|
+
import { linkCouldCarry, loadOf, PEAK_CLASS } from "../services/encode/quality/link-budget.js";
|
|
11
|
+
|
|
12
|
+
const WANTED = { width: 1824, height: 1026 };
|
|
13
|
+
const NEXT_LOWER = 1280 * 720;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} key
|
|
17
|
+
* @param {number} width
|
|
18
|
+
* @param {number} height
|
|
19
|
+
* @param {{ mbps?: number | null, peakClass?: string, readyHere?: boolean }} [over]
|
|
20
|
+
*/
|
|
21
|
+
function candidate(key, width, height, { mbps = 5, peakClass = PEAK_CLASS.KNOWN, readyHere = true } = {}) {
|
|
22
|
+
return { key, width, height, readyHere, load: loadOf({ mbps, peakClass: mbps === null ? PEAK_CLASS.UNKNOWN : peakClass }, null) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} over
|
|
27
|
+
* @param {number | null} [linkMbps] - What this viewer's link measured.
|
|
28
|
+
*/
|
|
29
|
+
function choose(over, linkMbps = null) {
|
|
30
|
+
return chooseServingOutput({
|
|
31
|
+
mode: "auto",
|
|
32
|
+
wanted: WANTED,
|
|
33
|
+
nextLowerArea: NEXT_LOWER,
|
|
34
|
+
atRisk: false,
|
|
35
|
+
judge: (one) => linkCouldCarry(linkMbps, one.load),
|
|
36
|
+
candidates: [],
|
|
37
|
+
...over
|
|
38
|
+
})?.key ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("the same quality or higher is served, the nearest of them", () => {
|
|
42
|
+
assert.equal(choose({ candidates: [candidate("2160", 3840, 2160), candidate("1080", 1920, 1080)] }), "1080");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("higher quality is not served over a link it does not fit", () => {
|
|
46
|
+
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { mbps: 12 })] }, 10), null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a little lower — above the next rung down — is served, and only if the link admits it", () => {
|
|
50
|
+
assert.equal(choose({ candidates: [candidate("1000", 1776, 1000)] }), "1000");
|
|
51
|
+
assert.equal(
|
|
52
|
+
choose({ candidates: [candidate("1000", 1776, 1000, { mbps: 12 })] }, 10),
|
|
53
|
+
null,
|
|
54
|
+
"a smaller picture proves nothing by itself: its own load is asked"
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("the next rung down or lower is served only when making the wanted format risks a wait, and only if it fits", () => {
|
|
59
|
+
const lower = [candidate("720", 1280, 720), candidate("480", 854, 480)];
|
|
60
|
+
assert.equal(choose({ candidates: lower }), null);
|
|
61
|
+
assert.equal(choose({ candidates: lower, atRisk: true }), "720");
|
|
62
|
+
assert.equal(
|
|
63
|
+
choose({ candidates: [candidate("720", 1280, 720, { mbps: 12 }), candidate("480", 854, 480, { mbps: 3 })], atRisk: true }, 10),
|
|
64
|
+
"480",
|
|
65
|
+
"the larger one does not fit this link, so it is not taken"
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an output without the piece at the viewer's position spares them nothing", () => {
|
|
70
|
+
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { readyHere: false })] }), null);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a size chosen by hand is served only exactly, and only if the link admits it", () => {
|
|
74
|
+
const candidates = [candidate("1080", 1920, 1080), candidate("exact", 1824, 1026)];
|
|
75
|
+
assert.equal(choose({ mode: "manual", candidates }), "exact");
|
|
76
|
+
assert.equal(choose({ mode: "manual", candidates: [candidate("1080", 1920, 1080)], atRisk: true }), null);
|
|
77
|
+
assert.equal(
|
|
78
|
+
choose({ mode: "manual", candidates: [candidate("exact", 1824, 1026, { mbps: 12 })] }, 10),
|
|
79
|
+
null,
|
|
80
|
+
"the size they picked, but more than their link carries: not handed over"
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("of two outputs of the size picked by hand, the higher limit the link admits is served", () => {
|
|
85
|
+
const candidates = [candidate("2800", 1824, 1026, { mbps: 3.64 }), candidate("1400", 1824, 1026, { mbps: 1.82 })];
|
|
86
|
+
assert.equal(choose({ mode: "manual", candidates }, 10), "2800");
|
|
87
|
+
assert.equal(choose({ mode: "manual", candidates }, 3), "1400", "the link admits only the lower limit");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a bound is taken before an average, even when the average's limit is higher", () => {
|
|
91
|
+
const candidates = [
|
|
92
|
+
candidate("estimated", 1824, 1026, { mbps: 6, peakClass: PEAK_CLASS.ESTIMATED }),
|
|
93
|
+
candidate("known", 1824, 1026, { mbps: 4, peakClass: PEAK_CLASS.KNOWN })
|
|
94
|
+
];
|
|
95
|
+
assert.equal(choose({ mode: "manual", candidates }, 10), "known");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("an output with no bound is not served against a measured link, and is while nothing measured it", () => {
|
|
99
|
+
const candidates = [candidate("hardware", 1824, 1026, { mbps: null })];
|
|
100
|
+
assert.equal(choose({ mode: "manual", candidates }, 50), null);
|
|
101
|
+
assert.equal(choose({ mode: "manual", candidates }, null), "hardware");
|
|
102
|
+
});
|