@torrent-tv/proxy 2.87.10 → 2.87.11
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 +2186 -2181
- 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 +124 -101
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +136 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -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 +1110 -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/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 +931 -931
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +703 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +620 -0
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +989 -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/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/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/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +675 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- 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 +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 +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/playback-readiness.test.js +323 -0
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -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 +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { predictPlaybackReadiness, RateTrend, forecastRate } from "../services/server/playback-readiness.js";
|
|
4
|
+
|
|
5
|
+
function input(overrides = {}) {
|
|
6
|
+
const now = 10_000;
|
|
7
|
+
return {
|
|
8
|
+
now,
|
|
9
|
+
positionSeconds: 0,
|
|
10
|
+
durationSeconds: 8,
|
|
11
|
+
bufferedAheadSeconds: 0,
|
|
12
|
+
bufferLimitSeconds: 8,
|
|
13
|
+
reserveSeconds: 4,
|
|
14
|
+
lookaheadSeconds: 8,
|
|
15
|
+
sources: [{ id: "source", complete: true, bytesPerMediaSecond: 1, readings: [] }],
|
|
16
|
+
tracks: [{
|
|
17
|
+
id: "video",
|
|
18
|
+
sourceIds: ["source"],
|
|
19
|
+
processedSeconds: 4,
|
|
20
|
+
bitsPerMediaSecond: 80,
|
|
21
|
+
readings: [{ at: now - 1_000, value: 2 }, { at: now, value: 2 }],
|
|
22
|
+
segments: [
|
|
23
|
+
{ index: 0, startSeconds: 0, endSeconds: 4 },
|
|
24
|
+
{ index: 1, startSeconds: 4, endSeconds: 8 }
|
|
25
|
+
],
|
|
26
|
+
readySegmentIndices: [0],
|
|
27
|
+
segmentSizesBytes: new Map([[0, 40]])
|
|
28
|
+
}],
|
|
29
|
+
linkReadings: [{ at: now - 1_000, value: 320 }, { at: now, value: 320 }],
|
|
30
|
+
...overrides
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("finds the first safe start from segments delivered before playback", () => {
|
|
35
|
+
const forecast = predictPlaybackReadiness(input());
|
|
36
|
+
|
|
37
|
+
assert.equal(forecast.ready, false);
|
|
38
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
39
|
+
assert.ok(Math.abs(forecast.delaySeconds - 3) < 1e-8);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("overlaps source reads and encoding, then waits for the slower stage and delivery", () => {
|
|
43
|
+
const source = {
|
|
44
|
+
id: "source",
|
|
45
|
+
complete: false,
|
|
46
|
+
bytesPerMediaSecond: 1,
|
|
47
|
+
readings: [{ at: 9_000, value: 2 }, { at: 10_000, value: 2 }]
|
|
48
|
+
};
|
|
49
|
+
const track = {
|
|
50
|
+
...input().tracks[0],
|
|
51
|
+
readings: [{ at: 9_000, value: 10 }, { at: 10_000, value: 10 }]
|
|
52
|
+
};
|
|
53
|
+
const forecast = predictPlaybackReadiness(input({ sources: [source], tracks: [track] }));
|
|
54
|
+
|
|
55
|
+
assert.equal(forecast.ready, false);
|
|
56
|
+
assert.ok(Math.abs(forecast.delaySeconds - 3) < 1e-8);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("does not produce an ETA without a client-link measurement", () => {
|
|
60
|
+
const forecast = predictPlaybackReadiness(input({ linkReadings: [] }));
|
|
61
|
+
|
|
62
|
+
assert.equal(forecast.ready, false);
|
|
63
|
+
assert.equal(forecast.delaySeconds, null);
|
|
64
|
+
assert.equal(forecast.reason, "link-rate-unavailable");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("rate trend projects the measured linear trend without a fitted weight", () => {
|
|
68
|
+
const trend = new RateTrend();
|
|
69
|
+
trend.add(9_000, 2);
|
|
70
|
+
trend.add(10_000, 4);
|
|
71
|
+
|
|
72
|
+
assert.equal(forecastRate(trend.snapshot(), 10_000, 2), 6);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("uses each track's own segment boundaries and requires continuous coverage on both tracks", () => {
|
|
76
|
+
const common = input();
|
|
77
|
+
const video = common.tracks[0];
|
|
78
|
+
const audio = {
|
|
79
|
+
id: "audio",
|
|
80
|
+
sourceIds: ["source"],
|
|
81
|
+
processedSeconds: 2,
|
|
82
|
+
bitsPerMediaSecond: 80,
|
|
83
|
+
readings: [{ at: 9_000, value: 2 }, { at: 10_000, value: 2 }],
|
|
84
|
+
segments: [
|
|
85
|
+
{ index: 7, startSeconds: 0, endSeconds: 2 },
|
|
86
|
+
{ index: 8, startSeconds: 2, endSeconds: 4 },
|
|
87
|
+
{ index: 9, startSeconds: 4, endSeconds: 8 }
|
|
88
|
+
],
|
|
89
|
+
readySegmentIndices: [7],
|
|
90
|
+
segmentSizesBytes: new Map([[7, 20], [8, 20], [9, 40]])
|
|
91
|
+
};
|
|
92
|
+
video.segmentSizesBytes.set(1, 40);
|
|
93
|
+
const forecast = predictPlaybackReadiness({
|
|
94
|
+
...common,
|
|
95
|
+
requiredAudio: true,
|
|
96
|
+
tracks: [video, audio]
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assert.equal(forecast.ready, false);
|
|
100
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
101
|
+
assert.ok(forecast.delaySeconds > 0);
|
|
102
|
+
assert.equal(forecast.preparedSegments, 2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("does not start when only one required track has continuous coverage", () => {
|
|
106
|
+
const common = input();
|
|
107
|
+
const audio = {
|
|
108
|
+
id: "audio",
|
|
109
|
+
sourceIds: ["source"],
|
|
110
|
+
processedSeconds: 0,
|
|
111
|
+
bitsPerMediaSecond: 80,
|
|
112
|
+
readings: [{ at: 9_000, value: 2 }, { at: 10_000, value: 2 }],
|
|
113
|
+
segments: [
|
|
114
|
+
{ index: 4, startSeconds: 0, endSeconds: 2 },
|
|
115
|
+
{ index: 5, startSeconds: 3, endSeconds: 8 }
|
|
116
|
+
],
|
|
117
|
+
readySegmentIndices: [4, 5],
|
|
118
|
+
segmentSizesBytes: new Map([[4, 20], [5, 50]])
|
|
119
|
+
};
|
|
120
|
+
const forecast = predictPlaybackReadiness({
|
|
121
|
+
...common,
|
|
122
|
+
requiredAudio: true,
|
|
123
|
+
tracks: [common.tracks[0], audio]
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
assert.equal(forecast.ready, false);
|
|
127
|
+
assert.equal(forecast.delaySeconds, null);
|
|
128
|
+
assert.equal(forecast.reason, "no-safe-start-found");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("downloads an overlapping source interval once when two tracks use the same file", () => {
|
|
132
|
+
const now = 10_000;
|
|
133
|
+
const readings = [{ at: now - 1_000, value: 10 }, { at: now, value: 10 }];
|
|
134
|
+
const tracks = [
|
|
135
|
+
{
|
|
136
|
+
id: "video",
|
|
137
|
+
sourceIds: ["shared"],
|
|
138
|
+
processedSeconds: 2,
|
|
139
|
+
bitsPerMediaSecond: 80,
|
|
140
|
+
readings,
|
|
141
|
+
segments: [
|
|
142
|
+
{ index: 0, startSeconds: 0, endSeconds: 2 },
|
|
143
|
+
{ index: 1, startSeconds: 2, endSeconds: 4 }
|
|
144
|
+
],
|
|
145
|
+
readySegmentIndices: [0],
|
|
146
|
+
segmentSizesBytes: new Map([[0, 20], [1, 20]])
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "audio",
|
|
150
|
+
sourceIds: ["shared"],
|
|
151
|
+
processedSeconds: 0,
|
|
152
|
+
bitsPerMediaSecond: 80,
|
|
153
|
+
readings,
|
|
154
|
+
segments: [{ index: 11, startSeconds: 0, endSeconds: 4 }],
|
|
155
|
+
readySegmentIndices: [],
|
|
156
|
+
segmentSizesBytes: new Map([[11, 40]])
|
|
157
|
+
}
|
|
158
|
+
];
|
|
159
|
+
const forecast = predictPlaybackReadiness({
|
|
160
|
+
now,
|
|
161
|
+
positionSeconds: 0,
|
|
162
|
+
durationSeconds: 4,
|
|
163
|
+
bufferedAheadSeconds: 0,
|
|
164
|
+
bufferLimitSeconds: 4,
|
|
165
|
+
reserveSeconds: 4,
|
|
166
|
+
lookaheadSeconds: 4,
|
|
167
|
+
requiredAudio: true,
|
|
168
|
+
sources: [{
|
|
169
|
+
id: "shared",
|
|
170
|
+
complete: false,
|
|
171
|
+
bytesPerMediaSecond: 1,
|
|
172
|
+
readings: [{ at: now - 1_000, value: 2 }, { at: now, value: 2 }]
|
|
173
|
+
}],
|
|
174
|
+
tracks,
|
|
175
|
+
linkReadings: [{ at: now - 1_000, value: 10_000 }, { at: now, value: 10_000 }]
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.equal(forecast.ready, false);
|
|
179
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
180
|
+
assert.ok(Math.abs(forecast.delaySeconds - 2.048) < 1e-8);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("downloads sources on separate torrents concurrently", () => {
|
|
184
|
+
const now = 10_000;
|
|
185
|
+
const readings = [{ at: now - 1_000, value: 10 }, { at: now, value: 10 }];
|
|
186
|
+
const tracks = [
|
|
187
|
+
{
|
|
188
|
+
id: "video",
|
|
189
|
+
sourceIds: ["video-source"],
|
|
190
|
+
processedSeconds: 0,
|
|
191
|
+
bitsPerMediaSecond: 80,
|
|
192
|
+
readings,
|
|
193
|
+
segments: [{ index: 0, startSeconds: 0, endSeconds: 4 }],
|
|
194
|
+
readySegmentIndices: [],
|
|
195
|
+
segmentSizesBytes: new Map([[0, 40]])
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "audio",
|
|
199
|
+
sourceIds: ["audio-source"],
|
|
200
|
+
processedSeconds: 0,
|
|
201
|
+
bitsPerMediaSecond: 80,
|
|
202
|
+
readings,
|
|
203
|
+
segments: [{ index: 7, startSeconds: 0, endSeconds: 4 }],
|
|
204
|
+
readySegmentIndices: [],
|
|
205
|
+
segmentSizesBytes: new Map([[7, 40]])
|
|
206
|
+
}
|
|
207
|
+
];
|
|
208
|
+
const sourceReadings = [{ at: now - 1_000, value: 2 }, { at: now, value: 2 }];
|
|
209
|
+
const forecast = predictPlaybackReadiness({
|
|
210
|
+
now,
|
|
211
|
+
positionSeconds: 0,
|
|
212
|
+
durationSeconds: 4,
|
|
213
|
+
bufferedAheadSeconds: 0,
|
|
214
|
+
bufferLimitSeconds: 4,
|
|
215
|
+
reserveSeconds: 4,
|
|
216
|
+
lookaheadSeconds: 4,
|
|
217
|
+
requiredAudio: true,
|
|
218
|
+
sources: [
|
|
219
|
+
{ id: "video-source", complete: false, bytesPerMediaSecond: 1, readings: sourceReadings },
|
|
220
|
+
{ id: "audio-source", complete: false, bytesPerMediaSecond: 1, readings: sourceReadings }
|
|
221
|
+
],
|
|
222
|
+
tracks,
|
|
223
|
+
linkReadings: [{ at: now - 1_000, value: 10_000 }, { at: now, value: 10_000 }]
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
assert.equal(forecast.ready, false);
|
|
227
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
228
|
+
assert.ok(Math.abs(forecast.delaySeconds - 2.064) < 1e-8);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("shares one torrent download rate across distinct video and audio files", () => {
|
|
232
|
+
const now = 10_000;
|
|
233
|
+
const readings = [{ at: now - 1_000, value: 10 }, { at: now, value: 10 }];
|
|
234
|
+
const sourceReadings = [{ at: now - 1_000, value: 2 }, { at: now, value: 2 }];
|
|
235
|
+
const tracks = [
|
|
236
|
+
{
|
|
237
|
+
id: "video",
|
|
238
|
+
sourceIds: ["torrent:video-file"],
|
|
239
|
+
processedSeconds: 0,
|
|
240
|
+
bitsPerMediaSecond: 80,
|
|
241
|
+
readings,
|
|
242
|
+
segments: [{ index: 0, startSeconds: 0, endSeconds: 4 }],
|
|
243
|
+
readySegmentIndices: [],
|
|
244
|
+
segmentSizesBytes: new Map([[0, 40]])
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "audio",
|
|
248
|
+
sourceIds: ["torrent:audio-file"],
|
|
249
|
+
processedSeconds: 0,
|
|
250
|
+
bitsPerMediaSecond: 80,
|
|
251
|
+
readings,
|
|
252
|
+
segments: [{ index: 7, startSeconds: 0, endSeconds: 4 }],
|
|
253
|
+
readySegmentIndices: [],
|
|
254
|
+
segmentSizesBytes: new Map([[7, 40]])
|
|
255
|
+
}
|
|
256
|
+
];
|
|
257
|
+
const forecast = predictPlaybackReadiness({
|
|
258
|
+
now,
|
|
259
|
+
positionSeconds: 0,
|
|
260
|
+
durationSeconds: 4,
|
|
261
|
+
bufferedAheadSeconds: 0,
|
|
262
|
+
bufferLimitSeconds: 4,
|
|
263
|
+
reserveSeconds: 4,
|
|
264
|
+
lookaheadSeconds: 4,
|
|
265
|
+
requiredAudio: true,
|
|
266
|
+
sources: [
|
|
267
|
+
{
|
|
268
|
+
id: "torrent:video-file",
|
|
269
|
+
serviceId: "torrent",
|
|
270
|
+
complete: false,
|
|
271
|
+
bytesPerMediaSecond: 1,
|
|
272
|
+
readings: sourceReadings
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "torrent:audio-file",
|
|
276
|
+
serviceId: "torrent",
|
|
277
|
+
complete: false,
|
|
278
|
+
bytesPerMediaSecond: 1,
|
|
279
|
+
readings: sourceReadings
|
|
280
|
+
}
|
|
281
|
+
],
|
|
282
|
+
tracks,
|
|
283
|
+
linkReadings: [{ at: now - 1_000, value: 10_000 }, { at: now, value: 10_000 }]
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
assert.equal(forecast.ready, false);
|
|
287
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
288
|
+
assert.ok(Math.abs(forecast.delaySeconds - 4.032) < 1e-8);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("finds a safe interval when rising encode speed and falling link speed make the forecast non-monotonic", () => {
|
|
292
|
+
const now = 1_000;
|
|
293
|
+
const forecast = predictPlaybackReadiness({
|
|
294
|
+
now,
|
|
295
|
+
positionSeconds: 0,
|
|
296
|
+
durationSeconds: 8,
|
|
297
|
+
bufferedAheadSeconds: 4,
|
|
298
|
+
bufferLimitSeconds: 8,
|
|
299
|
+
reserveSeconds: 1,
|
|
300
|
+
lookaheadSeconds: 4,
|
|
301
|
+
sources: [{ id: "source", complete: true, bytesPerMediaSecond: 1, readings: [] }],
|
|
302
|
+
tracks: [{
|
|
303
|
+
id: "video",
|
|
304
|
+
sourceIds: ["source"],
|
|
305
|
+
processedSeconds: 4,
|
|
306
|
+
bitsPerMediaSecond: 2,
|
|
307
|
+
readings: [{ at: 0, value: 0.1 }, { at: now, value: 0.15 }],
|
|
308
|
+
segments: [
|
|
309
|
+
{ index: 0, startSeconds: 0, endSeconds: 4 },
|
|
310
|
+
{ index: 1, startSeconds: 4, endSeconds: 6 },
|
|
311
|
+
{ index: 2, startSeconds: 6, endSeconds: 8 }
|
|
312
|
+
],
|
|
313
|
+
readySegmentIndices: [0],
|
|
314
|
+
segmentSizesBytes: new Map([[0, 1], [1, 1], [2, 1]])
|
|
315
|
+
}],
|
|
316
|
+
linkReadings: [{ at: 0, value: 105.3 }, { at: now, value: 100 }]
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
assert.equal(forecast.ready, false);
|
|
320
|
+
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
321
|
+
assert.ok(Number.isFinite(forecast.delaySeconds));
|
|
322
|
+
assert.ok(forecast.delaySeconds > 7 && forecast.delaySeconds < 10);
|
|
323
|
+
});
|
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the download is told when the priority map changes.
|
|
3
|
-
*
|
|
4
|
-
* The map states seconds of film against a number. This is the one place those
|
|
5
|
-
* seconds become bytes, and the one place the map's own scale — as many bands
|
|
6
|
-
* as the film needs — is fitted onto the five levels the register has. Both
|
|
7
|
-
* were written on 2026-09-05 to replace the windows the reads themselves used
|
|
8
|
-
* to declare: fifteen reads on one file were fifteen windows on a piece store
|
|
9
|
-
* holding sixteen pieces, and half of all evictions then took a piece a reader
|
|
10
|
-
* had said it wanted.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { TorrentPool } from "../services/torrent/torrent-pool.js";
|
|
16
|
-
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
17
|
-
import { Urgency } from "../services/torrent/demand/index.js";
|
|
18
|
-
import { mapForViewer, runsOf } from "../services/viewer/PriorityMap.js";
|
|
19
|
-
|
|
20
|
-
/** A torrent that is nothing but one file of a known length. */
|
|
21
|
-
function torrentOf({ length = 1_000_000, offset = 0 } = {}) {
|
|
22
|
-
return {
|
|
23
|
-
infoHash: `hash-${Math.random().toString(36).slice(2)}`,
|
|
24
|
-
pieceLength: 1024,
|
|
25
|
-
files: [{ offset, length, name: "film.mkv" }],
|
|
26
|
-
_selections: { _items: [] },
|
|
27
|
-
_select() {},
|
|
28
|
-
_deselect() {},
|
|
29
|
-
critical() {}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** The pool's method under test, called without building a pool. */
|
|
34
|
-
const applyPriorityMap = TorrentPool.prototype.applyPriorityMap;
|
|
35
|
-
|
|
36
|
-
test("seconds of film become bytes of the file", () => {
|
|
37
|
-
const torrent = torrentOf({ length: 1000 });
|
|
38
|
-
try {
|
|
39
|
-
applyPriorityMap.call(null, torrent, 0, [
|
|
40
|
-
{ from: 0, to: 100, priority: 5 },
|
|
41
|
-
{ from: 100, to: 200, priority: 3 }
|
|
42
|
-
], 200);
|
|
43
|
-
|
|
44
|
-
const { register } = demandFor(torrent);
|
|
45
|
-
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
46
|
-
assert.equal(stated.length, 2);
|
|
47
|
-
const first = stated.find((one) => one.byteStart === 0);
|
|
48
|
-
assert.equal(first.byteEnd, 499, "half the film is half the file");
|
|
49
|
-
const second = stated.find((one) => one.byteStart === 500);
|
|
50
|
-
assert.equal(second.byteEnd, 999, "the last byte of the file, not one past it");
|
|
51
|
-
} finally {
|
|
52
|
-
forgetTorrent(torrent);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("a zone is stated at the level it means, not at the level of its position", () => {
|
|
57
|
-
const torrent = torrentOf();
|
|
58
|
-
try {
|
|
59
|
-
// A viewer 300 s into a 3000 s film: one band behind them that nobody is
|
|
60
|
-
// approaching, then bands of decreasing urgency ahead.
|
|
61
|
-
const zones = runsOf(mapForViewer({
|
|
62
|
-
atSeconds: 300,
|
|
63
|
-
durationSeconds: 3000,
|
|
64
|
-
allowanceSeconds: 10
|
|
65
|
-
}));
|
|
66
|
-
assert.ok(zones.length > 5, "the map should be finer than the register's levels");
|
|
67
|
-
|
|
68
|
-
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
69
|
-
const { register } = demandFor(torrent);
|
|
70
|
-
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
71
|
-
|
|
72
|
-
const near = stated.filter((one) => one.urgency === Urgency.NEAR);
|
|
73
|
-
assert.equal(near.length, 1, "exactly one band is where the viewer is");
|
|
74
|
-
assert.equal(near[0].byteStart, Math.floor((300 / 3000) * 1_000_000));
|
|
75
|
-
|
|
76
|
-
const behind = stated.filter((one) => one.urgency === Urgency.BEHIND);
|
|
77
|
-
assert.equal(behind.length, 1, "what nobody is approaching is the one thing wanted last");
|
|
78
|
-
assert.equal(behind[0].byteStart, 0, "and it is the stretch behind the viewer");
|
|
79
|
-
|
|
80
|
-
assert.ok(
|
|
81
|
-
stated.some((one) => one.urgency === Urgency.AHEAD),
|
|
82
|
-
"the lead being built was stated at no level of its own"
|
|
83
|
-
);
|
|
84
|
-
assert.ok(
|
|
85
|
-
stated.every((one) => one.urgency !== Urgency.BLOCKED),
|
|
86
|
-
"the map claimed BLOCKED, which only a reader stopped on those bytes may say"
|
|
87
|
-
);
|
|
88
|
-
} finally {
|
|
89
|
-
forgetTorrent(torrent);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("a viewer who has stopped the picture makes the whole film wanted last", () => {
|
|
94
|
-
const torrent = torrentOf();
|
|
95
|
-
try {
|
|
96
|
-
const zones = runsOf(mapForViewer({
|
|
97
|
-
atSeconds: 300,
|
|
98
|
-
durationSeconds: 3000,
|
|
99
|
-
allowanceSeconds: 10,
|
|
100
|
-
playing: false
|
|
101
|
-
}));
|
|
102
|
-
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
103
|
-
|
|
104
|
-
const { register } = demandFor(torrent);
|
|
105
|
-
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
106
|
-
// A PAUSE REMOVES THE TIME, NOT THE DIRECTION. What they have watched is
|
|
107
|
-
// behind them and what they have not is in front, exactly as while they were
|
|
108
|
-
// watching; what changes is that nothing has a time any more, so every band
|
|
109
|
-
// of theirs yields to anybody who is on their way somewhere.
|
|
110
|
-
assert.ok(stated.length >= 2, "their position survives the pause");
|
|
111
|
-
for (const one of stated) {
|
|
112
|
-
assert.ok(
|
|
113
|
-
one.urgency === Urgency.BEHIND || one.urgency === Urgency.TAIL,
|
|
114
|
-
"and nothing of theirs is urgent, because nobody is coming"
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
const front = stated.filter((one) => one.urgency === Urgency.TAIL);
|
|
118
|
-
assert.ok(front.length >= 1, "the film in front of them is still in front");
|
|
119
|
-
assert.ok(
|
|
120
|
-
front[0].byteStart >= Math.floor((300 / 3000) * 1_000_000),
|
|
121
|
-
"and it begins where they stopped"
|
|
122
|
-
);
|
|
123
|
-
// Wanted last ABSOLUTELY, not relative to this film's own map. The two
|
|
124
|
-
// speculative levels are withheld across every torrent at once while
|
|
125
|
-
// anything urgent is missing anywhere, so this has to lose to a film
|
|
126
|
-
// somebody is actually watching — and judged against itself alone it would
|
|
127
|
-
// be the most urgent thing there is.
|
|
128
|
-
const behind = stated.filter((one) => one.urgency === Urgency.BEHIND);
|
|
129
|
-
assert.equal(behind.length, 1, "what they have already watched is wanted last of all");
|
|
130
|
-
assert.equal(behind[0].byteStart, 0, "and it is the stretch from the beginning to where they stopped");
|
|
131
|
-
} finally {
|
|
132
|
-
forgetTorrent(torrent);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("a map with fewer bands than the last one leaves nothing stated behind", () => {
|
|
137
|
-
const torrent = torrentOf();
|
|
138
|
-
try {
|
|
139
|
-
applyPriorityMap.call(null, torrent, 0, [
|
|
140
|
-
{ from: 0, to: 100, priority: 5 },
|
|
141
|
-
{ from: 100, to: 200, priority: 4 },
|
|
142
|
-
{ from: 200, to: 300, priority: 3 }
|
|
143
|
-
], 300);
|
|
144
|
-
const { register } = demandFor(torrent);
|
|
145
|
-
assert.equal(
|
|
146
|
-
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
147
|
-
3
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 300, priority: 5 }], 300);
|
|
151
|
-
assert.equal(
|
|
152
|
-
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
153
|
-
1,
|
|
154
|
-
"bands the map no longer has went on being asked for"
|
|
155
|
-
);
|
|
156
|
-
} finally {
|
|
157
|
-
forgetTorrent(torrent);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("a file of unknown length or duration is not guessed at", () => {
|
|
162
|
-
const torrent = torrentOf();
|
|
163
|
-
try {
|
|
164
|
-
applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 100, priority: 5 }], 0);
|
|
165
|
-
applyPriorityMap.call(null, torrent, 7, [{ from: 0, to: 100, priority: 5 }], 300);
|
|
166
|
-
const { register } = demandFor(torrent);
|
|
167
|
-
assert.equal(
|
|
168
|
-
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
169
|
-
0
|
|
170
|
-
);
|
|
171
|
-
} finally {
|
|
172
|
-
forgetTorrent(torrent);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What the download is told when the priority map changes.
|
|
3
|
+
*
|
|
4
|
+
* The map states seconds of film against a number. This is the one place those
|
|
5
|
+
* seconds become bytes, and the one place the map's own scale — as many bands
|
|
6
|
+
* as the film needs — is fitted onto the five levels the register has. Both
|
|
7
|
+
* were written on 2026-09-05 to replace the windows the reads themselves used
|
|
8
|
+
* to declare: fifteen reads on one file were fifteen windows on a piece store
|
|
9
|
+
* holding sixteen pieces, and half of all evictions then took a piece a reader
|
|
10
|
+
* had said it wanted.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { TorrentPool } from "../services/torrent/torrent-pool.js";
|
|
16
|
+
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
17
|
+
import { Urgency } from "../services/torrent/demand/index.js";
|
|
18
|
+
import { mapForViewer, runsOf } from "../services/viewer/PriorityMap.js";
|
|
19
|
+
|
|
20
|
+
/** A torrent that is nothing but one file of a known length. */
|
|
21
|
+
function torrentOf({ length = 1_000_000, offset = 0 } = {}) {
|
|
22
|
+
return {
|
|
23
|
+
infoHash: `hash-${Math.random().toString(36).slice(2)}`,
|
|
24
|
+
pieceLength: 1024,
|
|
25
|
+
files: [{ offset, length, name: "film.mkv" }],
|
|
26
|
+
_selections: { _items: [] },
|
|
27
|
+
_select() {},
|
|
28
|
+
_deselect() {},
|
|
29
|
+
critical() {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The pool's method under test, called without building a pool. */
|
|
34
|
+
const applyPriorityMap = TorrentPool.prototype.applyPriorityMap;
|
|
35
|
+
|
|
36
|
+
test("seconds of film become bytes of the file", () => {
|
|
37
|
+
const torrent = torrentOf({ length: 1000 });
|
|
38
|
+
try {
|
|
39
|
+
applyPriorityMap.call(null, torrent, 0, [
|
|
40
|
+
{ from: 0, to: 100, priority: 5 },
|
|
41
|
+
{ from: 100, to: 200, priority: 3 }
|
|
42
|
+
], 200);
|
|
43
|
+
|
|
44
|
+
const { register } = demandFor(torrent);
|
|
45
|
+
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
46
|
+
assert.equal(stated.length, 2);
|
|
47
|
+
const first = stated.find((one) => one.byteStart === 0);
|
|
48
|
+
assert.equal(first.byteEnd, 499, "half the film is half the file");
|
|
49
|
+
const second = stated.find((one) => one.byteStart === 500);
|
|
50
|
+
assert.equal(second.byteEnd, 999, "the last byte of the file, not one past it");
|
|
51
|
+
} finally {
|
|
52
|
+
forgetTorrent(torrent);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a zone is stated at the level it means, not at the level of its position", () => {
|
|
57
|
+
const torrent = torrentOf();
|
|
58
|
+
try {
|
|
59
|
+
// A viewer 300 s into a 3000 s film: one band behind them that nobody is
|
|
60
|
+
// approaching, then bands of decreasing urgency ahead.
|
|
61
|
+
const zones = runsOf(mapForViewer({
|
|
62
|
+
atSeconds: 300,
|
|
63
|
+
durationSeconds: 3000,
|
|
64
|
+
allowanceSeconds: 10
|
|
65
|
+
}));
|
|
66
|
+
assert.ok(zones.length > 5, "the map should be finer than the register's levels");
|
|
67
|
+
|
|
68
|
+
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
69
|
+
const { register } = demandFor(torrent);
|
|
70
|
+
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
71
|
+
|
|
72
|
+
const near = stated.filter((one) => one.urgency === Urgency.NEAR);
|
|
73
|
+
assert.equal(near.length, 1, "exactly one band is where the viewer is");
|
|
74
|
+
assert.equal(near[0].byteStart, Math.floor((300 / 3000) * 1_000_000));
|
|
75
|
+
|
|
76
|
+
const behind = stated.filter((one) => one.urgency === Urgency.BEHIND);
|
|
77
|
+
assert.equal(behind.length, 1, "what nobody is approaching is the one thing wanted last");
|
|
78
|
+
assert.equal(behind[0].byteStart, 0, "and it is the stretch behind the viewer");
|
|
79
|
+
|
|
80
|
+
assert.ok(
|
|
81
|
+
stated.some((one) => one.urgency === Urgency.AHEAD),
|
|
82
|
+
"the lead being built was stated at no level of its own"
|
|
83
|
+
);
|
|
84
|
+
assert.ok(
|
|
85
|
+
stated.every((one) => one.urgency !== Urgency.BLOCKED),
|
|
86
|
+
"the map claimed BLOCKED, which only a reader stopped on those bytes may say"
|
|
87
|
+
);
|
|
88
|
+
} finally {
|
|
89
|
+
forgetTorrent(torrent);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a viewer who has stopped the picture makes the whole film wanted last", () => {
|
|
94
|
+
const torrent = torrentOf();
|
|
95
|
+
try {
|
|
96
|
+
const zones = runsOf(mapForViewer({
|
|
97
|
+
atSeconds: 300,
|
|
98
|
+
durationSeconds: 3000,
|
|
99
|
+
allowanceSeconds: 10,
|
|
100
|
+
playing: false
|
|
101
|
+
}));
|
|
102
|
+
applyPriorityMap.call(null, torrent, 0, zones, 3000);
|
|
103
|
+
|
|
104
|
+
const { register } = demandFor(torrent);
|
|
105
|
+
const stated = register.windows().filter((one) => String(one.claimant).startsWith("priority-map:"));
|
|
106
|
+
// A PAUSE REMOVES THE TIME, NOT THE DIRECTION. What they have watched is
|
|
107
|
+
// behind them and what they have not is in front, exactly as while they were
|
|
108
|
+
// watching; what changes is that nothing has a time any more, so every band
|
|
109
|
+
// of theirs yields to anybody who is on their way somewhere.
|
|
110
|
+
assert.ok(stated.length >= 2, "their position survives the pause");
|
|
111
|
+
for (const one of stated) {
|
|
112
|
+
assert.ok(
|
|
113
|
+
one.urgency === Urgency.BEHIND || one.urgency === Urgency.TAIL,
|
|
114
|
+
"and nothing of theirs is urgent, because nobody is coming"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const front = stated.filter((one) => one.urgency === Urgency.TAIL);
|
|
118
|
+
assert.ok(front.length >= 1, "the film in front of them is still in front");
|
|
119
|
+
assert.ok(
|
|
120
|
+
front[0].byteStart >= Math.floor((300 / 3000) * 1_000_000),
|
|
121
|
+
"and it begins where they stopped"
|
|
122
|
+
);
|
|
123
|
+
// Wanted last ABSOLUTELY, not relative to this film's own map. The two
|
|
124
|
+
// speculative levels are withheld across every torrent at once while
|
|
125
|
+
// anything urgent is missing anywhere, so this has to lose to a film
|
|
126
|
+
// somebody is actually watching — and judged against itself alone it would
|
|
127
|
+
// be the most urgent thing there is.
|
|
128
|
+
const behind = stated.filter((one) => one.urgency === Urgency.BEHIND);
|
|
129
|
+
assert.equal(behind.length, 1, "what they have already watched is wanted last of all");
|
|
130
|
+
assert.equal(behind[0].byteStart, 0, "and it is the stretch from the beginning to where they stopped");
|
|
131
|
+
} finally {
|
|
132
|
+
forgetTorrent(torrent);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("a map with fewer bands than the last one leaves nothing stated behind", () => {
|
|
137
|
+
const torrent = torrentOf();
|
|
138
|
+
try {
|
|
139
|
+
applyPriorityMap.call(null, torrent, 0, [
|
|
140
|
+
{ from: 0, to: 100, priority: 5 },
|
|
141
|
+
{ from: 100, to: 200, priority: 4 },
|
|
142
|
+
{ from: 200, to: 300, priority: 3 }
|
|
143
|
+
], 300);
|
|
144
|
+
const { register } = demandFor(torrent);
|
|
145
|
+
assert.equal(
|
|
146
|
+
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
147
|
+
3
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 300, priority: 5 }], 300);
|
|
151
|
+
assert.equal(
|
|
152
|
+
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
153
|
+
1,
|
|
154
|
+
"bands the map no longer has went on being asked for"
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
forgetTorrent(torrent);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a file of unknown length or duration is not guessed at", () => {
|
|
162
|
+
const torrent = torrentOf();
|
|
163
|
+
try {
|
|
164
|
+
applyPriorityMap.call(null, torrent, 0, [{ from: 0, to: 100, priority: 5 }], 0);
|
|
165
|
+
applyPriorityMap.call(null, torrent, 7, [{ from: 0, to: 100, priority: 5 }], 300);
|
|
166
|
+
const { register } = demandFor(torrent);
|
|
167
|
+
assert.equal(
|
|
168
|
+
register.windows().filter((one) => String(one.claimant).startsWith("priority-map:")).length,
|
|
169
|
+
0
|
|
170
|
+
);
|
|
171
|
+
} finally {
|
|
172
|
+
forgetTorrent(torrent);
|
|
173
|
+
}
|
|
174
|
+
});
|