@torrent-tv/proxy 2.89.5 → 2.89.7
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 +338 -338
- package/CHANGELOG.md +16 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- 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/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -753
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +475 -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 +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- 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 +68 -68
- 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/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- 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 +240 -240
- package/test/helpers/manager.js +62 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +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 +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- 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 +87 -87
- 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-completeness.test.js +37 -1
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- 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 +160 -160
- package/test/quality-variants.test.js +1270 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +64 -64
- package/test/same-height-switch.test.js +663 -663
- 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-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 +222 -221
- package/test/segment-serve-wiring.test.js +9 -3
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +111 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/supply-margin.test.js +190 -190
- 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 +508 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +92 -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
package/test/source-file.test.js
CHANGED
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The file as a handle: its key, its name, what a probe said about it,
|
|
3
|
-
* and the address this proxy serves it at.
|
|
4
|
-
*
|
|
5
|
-
* The reason this object exists is the key. Twenty places in the session
|
|
6
|
-
* manager assembled `${sourceKey}:${fileIndex}` by hand, through five different
|
|
7
|
-
* spellings, to reach caches that are keyed by a file. These checks pin the
|
|
8
|
-
* shape those call sites now depend on, and the one property the registry has
|
|
9
|
-
* to have: the same file asked for twice is the same object, or a fact learned
|
|
10
|
-
* through one reference would be invisible through the other.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { SourceFile, SourceFiles, sourceDecodeCharacteristics } from "../services/media/SourceFile.js";
|
|
16
|
-
|
|
17
|
-
test("a file's key is the pair that identifies it, spelled in one place", () => {
|
|
18
|
-
const file = new SourceFile({ sourceKey: "abc123", fileIndex: 4 });
|
|
19
|
-
assert.equal(file.key, "abc123:4");
|
|
20
|
-
assert.equal(SourceFiles.keyFor("abc123", 4), file.key);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("a file with no name is named by its index, so no log line is about a nameless thing", () => {
|
|
24
|
-
assert.equal(new SourceFile({ sourceKey: "s", fileIndex: 7 }).name, "file#7");
|
|
25
|
-
assert.equal(new SourceFile({ sourceKey: "s", fileIndex: 7, name: " Film.mkv " }).name, "Film.mkv");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("facts are absent until a probe answers, and 'absent' is not 'zero'", () => {
|
|
29
|
-
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
30
|
-
assert.equal(file.width, null, "an unread frame size is unknown, not 0x0");
|
|
31
|
-
assert.equal(file.height, null);
|
|
32
|
-
assert.equal(file.fps, null);
|
|
33
|
-
assert.equal(file.durationSeconds, null);
|
|
34
|
-
assert.equal(file.hasDuration, false);
|
|
35
|
-
assert.equal(file.isHdr, false, "a file nobody has read is not claimed to be HDR");
|
|
36
|
-
assert.equal(file.startTime, 0, "no stated start time means the timeline begins at zero");
|
|
37
|
-
assert.equal(file.decode, null, "and nothing is priced from facts that were never read");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("readings are merged, because two readers answer about different things", () => {
|
|
41
|
-
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
42
|
-
// ffmpeg's banner: the frame size and rate, and no start time.
|
|
43
|
-
file.learn({ width: 1920, height: 1080, fps: 24 });
|
|
44
|
-
// The container's own header, read for one field — a sidecar soundtrack is
|
|
45
|
-
// read for exactly this and nothing else.
|
|
46
|
-
file.learn({ startTime: 0.083 });
|
|
47
|
-
assert.equal(file.startTime, 0.083);
|
|
48
|
-
assert.equal(file.width, 1920, "the second reader must not erase what the first found");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("a fresher reading replaces the first, because a cold torrent answers incompletely", () => {
|
|
52
|
-
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
53
|
-
file.learn({ width: 1920, height: 1080, fps: 24, durationSeconds: 0, startTime: 1.5 });
|
|
54
|
-
assert.equal(file.width, 1920);
|
|
55
|
-
assert.equal(file.hasDuration, false, "a duration of zero is no duration");
|
|
56
|
-
assert.equal(file.startTime, 1.5);
|
|
57
|
-
|
|
58
|
-
file.learn({ width: 1920, height: 1080, fps: 24, durationSeconds: 5400, startTime: 1.5 });
|
|
59
|
-
assert.equal(file.durationSeconds, 5400);
|
|
60
|
-
assert.equal(file.hasDuration, true);
|
|
61
|
-
|
|
62
|
-
file.learn(null);
|
|
63
|
-
assert.equal(file.durationSeconds, 5400, "nothing is unlearned by an answer that did not come");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("what decoding costs is derived from the file's own facts, per codec family", () => {
|
|
67
|
-
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
68
|
-
file.learn({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000, codec: "hevc", bitDepth: 10 });
|
|
69
|
-
assert.deepEqual(file.decode, {
|
|
70
|
-
megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
|
|
71
|
-
megabitsPerSecond: 8,
|
|
72
|
-
codec: "hevc",
|
|
73
|
-
bitDepth: 10
|
|
74
|
-
});
|
|
75
|
-
// The same arithmetic, still callable on a plain reading — the offer prices a
|
|
76
|
-
// file it has not made a session for.
|
|
77
|
-
assert.deepEqual(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000 }), {
|
|
78
|
-
megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
|
|
79
|
-
megabitsPerSecond: 8,
|
|
80
|
-
codec: "",
|
|
81
|
-
bitDepth: null
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("the address carries the file and the session, and the proxy's own base is a parameter", () => {
|
|
86
|
-
const file = new SourceFile({ sourceKey: "abc", fileIndex: 2 });
|
|
87
|
-
const url = file.streamUrl("http://127.0.0.1:9090", { sessionId: "sess-1" });
|
|
88
|
-
assert.equal(url.pathname, "/stream");
|
|
89
|
-
assert.equal(url.searchParams.get("sourceKey"), "abc");
|
|
90
|
-
assert.equal(url.searchParams.get("fileIndex"), "2");
|
|
91
|
-
assert.equal(url.searchParams.get("session"), "sess-1");
|
|
92
|
-
|
|
93
|
-
const noSession = file.streamUrl("http://127.0.0.1:9090");
|
|
94
|
-
assert.equal(noSession.searchParams.has("session"), false, "a read nobody is accounting for says so");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("the same file asked for twice is the same object, or a fact learned once is lost", () => {
|
|
98
|
-
const files = new SourceFiles();
|
|
99
|
-
const first = files.get("abc", 1, "Film.mkv");
|
|
100
|
-
first.learn({ width: 1280, height: 720, fps: 25, durationSeconds: 60 });
|
|
101
|
-
|
|
102
|
-
const second = files.get("abc", 1);
|
|
103
|
-
assert.equal(second, first, "two viewers of one file must not hold two copies of its facts");
|
|
104
|
-
assert.equal(second.width, 1280);
|
|
105
|
-
assert.equal(second.name, "Film.mkv");
|
|
106
|
-
|
|
107
|
-
const other = files.get("abc", 2);
|
|
108
|
-
assert.notEqual(other, first, "and a different file in the same torrent is a different file");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("a name learned later is kept, since the first caller often has only an index", () => {
|
|
112
|
-
const files = new SourceFiles();
|
|
113
|
-
const bare = files.get("abc", 1);
|
|
114
|
-
assert.equal(bare.name, "file#1");
|
|
115
|
-
files.get("abc", 1, "Film.mkv");
|
|
116
|
-
assert.equal(bare.name, "Film.mkv");
|
|
117
|
-
files.get("abc", 1, "Something.else.mkv");
|
|
118
|
-
assert.equal(bare.name, "Film.mkv", "and it is not renamed underneath a log line already written");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("files nobody names are dropped, and a file still named survives the sweep", () => {
|
|
122
|
-
const files = new SourceFiles();
|
|
123
|
-
const kept = files.get("abc", 1);
|
|
124
|
-
files.get("abc", 2);
|
|
125
|
-
files.get("def", 9);
|
|
126
|
-
assert.equal(files.size, 3);
|
|
127
|
-
|
|
128
|
-
const dropped = files.forgetUnused(new Set([kept]));
|
|
129
|
-
assert.equal(dropped, 2);
|
|
130
|
-
assert.equal(files.size, 1);
|
|
131
|
-
assert.equal(files.peek(kept.key), kept);
|
|
132
|
-
assert.equal(files.peek("abc:2"), null);
|
|
133
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The file as a handle: its key, its name, what a probe said about it,
|
|
3
|
+
* and the address this proxy serves it at.
|
|
4
|
+
*
|
|
5
|
+
* The reason this object exists is the key. Twenty places in the session
|
|
6
|
+
* manager assembled `${sourceKey}:${fileIndex}` by hand, through five different
|
|
7
|
+
* spellings, to reach caches that are keyed by a file. These checks pin the
|
|
8
|
+
* shape those call sites now depend on, and the one property the registry has
|
|
9
|
+
* to have: the same file asked for twice is the same object, or a fact learned
|
|
10
|
+
* through one reference would be invisible through the other.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { SourceFile, SourceFiles, sourceDecodeCharacteristics } from "../services/media/SourceFile.js";
|
|
16
|
+
|
|
17
|
+
test("a file's key is the pair that identifies it, spelled in one place", () => {
|
|
18
|
+
const file = new SourceFile({ sourceKey: "abc123", fileIndex: 4 });
|
|
19
|
+
assert.equal(file.key, "abc123:4");
|
|
20
|
+
assert.equal(SourceFiles.keyFor("abc123", 4), file.key);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("a file with no name is named by its index, so no log line is about a nameless thing", () => {
|
|
24
|
+
assert.equal(new SourceFile({ sourceKey: "s", fileIndex: 7 }).name, "file#7");
|
|
25
|
+
assert.equal(new SourceFile({ sourceKey: "s", fileIndex: 7, name: " Film.mkv " }).name, "Film.mkv");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("facts are absent until a probe answers, and 'absent' is not 'zero'", () => {
|
|
29
|
+
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
30
|
+
assert.equal(file.width, null, "an unread frame size is unknown, not 0x0");
|
|
31
|
+
assert.equal(file.height, null);
|
|
32
|
+
assert.equal(file.fps, null);
|
|
33
|
+
assert.equal(file.durationSeconds, null);
|
|
34
|
+
assert.equal(file.hasDuration, false);
|
|
35
|
+
assert.equal(file.isHdr, false, "a file nobody has read is not claimed to be HDR");
|
|
36
|
+
assert.equal(file.startTime, 0, "no stated start time means the timeline begins at zero");
|
|
37
|
+
assert.equal(file.decode, null, "and nothing is priced from facts that were never read");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("readings are merged, because two readers answer about different things", () => {
|
|
41
|
+
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
42
|
+
// ffmpeg's banner: the frame size and rate, and no start time.
|
|
43
|
+
file.learn({ width: 1920, height: 1080, fps: 24 });
|
|
44
|
+
// The container's own header, read for one field — a sidecar soundtrack is
|
|
45
|
+
// read for exactly this and nothing else.
|
|
46
|
+
file.learn({ startTime: 0.083 });
|
|
47
|
+
assert.equal(file.startTime, 0.083);
|
|
48
|
+
assert.equal(file.width, 1920, "the second reader must not erase what the first found");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a fresher reading replaces the first, because a cold torrent answers incompletely", () => {
|
|
52
|
+
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
53
|
+
file.learn({ width: 1920, height: 1080, fps: 24, durationSeconds: 0, startTime: 1.5 });
|
|
54
|
+
assert.equal(file.width, 1920);
|
|
55
|
+
assert.equal(file.hasDuration, false, "a duration of zero is no duration");
|
|
56
|
+
assert.equal(file.startTime, 1.5);
|
|
57
|
+
|
|
58
|
+
file.learn({ width: 1920, height: 1080, fps: 24, durationSeconds: 5400, startTime: 1.5 });
|
|
59
|
+
assert.equal(file.durationSeconds, 5400);
|
|
60
|
+
assert.equal(file.hasDuration, true);
|
|
61
|
+
|
|
62
|
+
file.learn(null);
|
|
63
|
+
assert.equal(file.durationSeconds, 5400, "nothing is unlearned by an answer that did not come");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("what decoding costs is derived from the file's own facts, per codec family", () => {
|
|
67
|
+
const file = new SourceFile({ sourceKey: "s", fileIndex: 0 });
|
|
68
|
+
file.learn({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000, codec: "hevc", bitDepth: 10 });
|
|
69
|
+
assert.deepEqual(file.decode, {
|
|
70
|
+
megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
|
|
71
|
+
megabitsPerSecond: 8,
|
|
72
|
+
codec: "hevc",
|
|
73
|
+
bitDepth: 10
|
|
74
|
+
});
|
|
75
|
+
// The same arithmetic, still callable on a plain reading — the offer prices a
|
|
76
|
+
// file it has not made a session for.
|
|
77
|
+
assert.deepEqual(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000 }), {
|
|
78
|
+
megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
|
|
79
|
+
megabitsPerSecond: 8,
|
|
80
|
+
codec: "",
|
|
81
|
+
bitDepth: null
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("the address carries the file and the session, and the proxy's own base is a parameter", () => {
|
|
86
|
+
const file = new SourceFile({ sourceKey: "abc", fileIndex: 2 });
|
|
87
|
+
const url = file.streamUrl("http://127.0.0.1:9090", { sessionId: "sess-1" });
|
|
88
|
+
assert.equal(url.pathname, "/stream");
|
|
89
|
+
assert.equal(url.searchParams.get("sourceKey"), "abc");
|
|
90
|
+
assert.equal(url.searchParams.get("fileIndex"), "2");
|
|
91
|
+
assert.equal(url.searchParams.get("session"), "sess-1");
|
|
92
|
+
|
|
93
|
+
const noSession = file.streamUrl("http://127.0.0.1:9090");
|
|
94
|
+
assert.equal(noSession.searchParams.has("session"), false, "a read nobody is accounting for says so");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("the same file asked for twice is the same object, or a fact learned once is lost", () => {
|
|
98
|
+
const files = new SourceFiles();
|
|
99
|
+
const first = files.get("abc", 1, "Film.mkv");
|
|
100
|
+
first.learn({ width: 1280, height: 720, fps: 25, durationSeconds: 60 });
|
|
101
|
+
|
|
102
|
+
const second = files.get("abc", 1);
|
|
103
|
+
assert.equal(second, first, "two viewers of one file must not hold two copies of its facts");
|
|
104
|
+
assert.equal(second.width, 1280);
|
|
105
|
+
assert.equal(second.name, "Film.mkv");
|
|
106
|
+
|
|
107
|
+
const other = files.get("abc", 2);
|
|
108
|
+
assert.notEqual(other, first, "and a different file in the same torrent is a different file");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a name learned later is kept, since the first caller often has only an index", () => {
|
|
112
|
+
const files = new SourceFiles();
|
|
113
|
+
const bare = files.get("abc", 1);
|
|
114
|
+
assert.equal(bare.name, "file#1");
|
|
115
|
+
files.get("abc", 1, "Film.mkv");
|
|
116
|
+
assert.equal(bare.name, "Film.mkv");
|
|
117
|
+
files.get("abc", 1, "Something.else.mkv");
|
|
118
|
+
assert.equal(bare.name, "Film.mkv", "and it is not renamed underneath a log line already written");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("files nobody names are dropped, and a file still named survives the sweep", () => {
|
|
122
|
+
const files = new SourceFiles();
|
|
123
|
+
const kept = files.get("abc", 1);
|
|
124
|
+
files.get("abc", 2);
|
|
125
|
+
files.get("def", 9);
|
|
126
|
+
assert.equal(files.size, 3);
|
|
127
|
+
|
|
128
|
+
const dropped = files.forgetUnused(new Set([kept]));
|
|
129
|
+
assert.equal(dropped, 2);
|
|
130
|
+
assert.equal(files.size, 1);
|
|
131
|
+
assert.equal(files.peek(kept.key), kept);
|
|
132
|
+
assert.equal(files.peek("abc:2"), null);
|
|
133
|
+
});
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { createSourceRegistry } from "../store/source-registry.js";
|
|
4
|
-
|
|
5
|
-
const infoHash = "0123456789abcdef0123456789abcdef01234567";
|
|
6
|
-
|
|
7
|
-
test("magnet links for one torrent share a source key", async () => {
|
|
8
|
-
const registry = createSourceRegistry();
|
|
9
|
-
const first = `magnet:?xt=urn:btih:${infoHash}&dn=First+name&tr=udp%3A%2F%2Ftracker.one%3A80`;
|
|
10
|
-
const second = `magnet:?xt=urn:btih:${infoHash}&dn=Different+name&tr=udp%3A%2F%2Ftracker.two%3A80`;
|
|
11
|
-
|
|
12
|
-
const firstKey = await registry.upsert("magnet", first);
|
|
13
|
-
const secondKey = await registry.upsert("magnet", second);
|
|
14
|
-
|
|
15
|
-
assert.equal(firstKey, `torrent:${infoHash}`);
|
|
16
|
-
assert.equal(secondKey, firstKey);
|
|
17
|
-
assert.equal(registry.get(firstKey)?.source, second);
|
|
18
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createSourceRegistry } from "../store/source-registry.js";
|
|
4
|
+
|
|
5
|
+
const infoHash = "0123456789abcdef0123456789abcdef01234567";
|
|
6
|
+
|
|
7
|
+
test("magnet links for one torrent share a source key", async () => {
|
|
8
|
+
const registry = createSourceRegistry();
|
|
9
|
+
const first = `magnet:?xt=urn:btih:${infoHash}&dn=First+name&tr=udp%3A%2F%2Ftracker.one%3A80`;
|
|
10
|
+
const second = `magnet:?xt=urn:btih:${infoHash}&dn=Different+name&tr=udp%3A%2F%2Ftracker.two%3A80`;
|
|
11
|
+
|
|
12
|
+
const firstKey = await registry.upsert("magnet", first);
|
|
13
|
+
const secondKey = await registry.upsert("magnet", second);
|
|
14
|
+
|
|
15
|
+
assert.equal(firstKey, `torrent:${infoHash}`);
|
|
16
|
+
assert.equal(secondKey, firstKey);
|
|
17
|
+
assert.equal(registry.get(firstKey)?.source, second);
|
|
18
|
+
});
|
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Where a second encoder joins a stretch, and whether it is worth having.
|
|
3
|
-
*
|
|
4
|
-
* Both halves are arithmetic and neither was asked before. The stretch used to
|
|
5
|
-
* be halved, unconditionally:
|
|
6
|
-
*
|
|
7
|
-
* widestFrom = from + Math.floor(Math.min(room, to - from + 1) / 2)
|
|
8
|
-
*
|
|
9
|
-
* Halving IS the answer when both encoders are fresh and owe the same. It is not
|
|
10
|
-
* when one of them is already partway through a piece, and it never asked the
|
|
11
|
-
* other question at all — whether two encoders under this host's measured
|
|
12
|
-
* contention beat one at full speed.
|
|
13
|
-
*
|
|
14
|
-
* The derivation. A stretch of unmade film runs from `from` to `to`; whoever is
|
|
15
|
-
* already on it stands at `from` and owes `w` before the piece under it exists;
|
|
16
|
-
* a fresh one placed at `x` owes `d` — its start and then a whole piece — and
|
|
17
|
-
* both then work at the rate two encoders leave each other:
|
|
18
|
-
*
|
|
19
|
-
* the one there closes [from, x-1]: w + (x - 1 - from) / r rises with x
|
|
20
|
-
* the fresh one closes [x, to]: d + (to - x) / r falls with x
|
|
21
|
-
*
|
|
22
|
-
* x* = (from + to + 1) / 2 + (d - w) * r / 2
|
|
23
|
-
*
|
|
24
|
-
* The shift is `firstByteWait / (2 * perPiece)` pieces, so it depends on how
|
|
25
|
-
* long a piece takes here: 0.24 of a piece at 4.45x on a 4.2 s grid, but 0.96 at
|
|
26
|
-
* 8.9x and 2.14 at 20x — a whole segment and more on the copy branch, where a
|
|
27
|
-
* piece is short and a start is not. Measuring only the re-encode rate
|
|
28
|
-
* understated it.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import test from "node:test";
|
|
32
|
-
import assert from "node:assert/strict";
|
|
33
|
-
import { planEncoders } from "../services/encode/EncodePlan.js";
|
|
34
|
-
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
35
|
-
|
|
36
|
-
const SEGMENT_SECONDS = 4.2;
|
|
37
|
-
const LAST = 199;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* One encoder on a long stretch nobody has made, and room for more.
|
|
41
|
-
*
|
|
42
|
-
* @param {{ speedX: number, penalty: number, head?: number, ageMs?: number,
|
|
43
|
-
* maxRuns?: number }} params
|
|
44
|
-
* @returns {{ actions: object[], run: object }}
|
|
45
|
-
*/
|
|
46
|
-
function planFor({ speedX, penalty, head = 0, ageMs = 60_000, maxRuns = 3, runs = null }) {
|
|
47
|
-
const coverage = new CoverageMap({ segmentCount: LAST + 1 });
|
|
48
|
-
const run = { from: 0, to: -1, head, speedX, isAlive: true, startedAt: 1_000_000 };
|
|
49
|
-
const live = runs ?? [run];
|
|
50
|
-
for (const one of live) {
|
|
51
|
-
coverage.claim(one, one.from, one.to);
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
run,
|
|
55
|
-
actions: planEncoders({
|
|
56
|
-
coverage,
|
|
57
|
-
// One zone over the whole film, due now, so nothing but the arithmetic of
|
|
58
|
-
// sharing it decides anything.
|
|
59
|
-
windows: [{ from: 0, to: LAST, priority: 100, withinSeconds: 0, behind: false }],
|
|
60
|
-
runs: live,
|
|
61
|
-
maxRuns,
|
|
62
|
-
segmentSeconds: SEGMENT_SECONDS,
|
|
63
|
-
speedX,
|
|
64
|
-
killCostSec: 0.04,
|
|
65
|
-
firstByteWaitSec: 0.9,
|
|
66
|
-
moveCostSec: 0.94,
|
|
67
|
-
refetchSecPerFilmSecond: 0,
|
|
68
|
-
// Measured on the addon host: 1.70 at 854x480, 1.98 at 1920x1080. Raised
|
|
69
|
-
// to the power of how many others there are, so a third costs again.
|
|
70
|
-
contentionPenaltyFor: (others) => (others <= 0 ? 1 : penalty ** others),
|
|
71
|
-
now: 1_000_000 + ageMs
|
|
72
|
-
})
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
test("one encoder that reaches everything in time is left to do it alone", () => {
|
|
77
|
-
// A zone whose deadline GROWS with distance — which is what the map states,
|
|
78
|
-
// one segment at a time — is comfortably served by one encoder at 4.45x: #199
|
|
79
|
-
// is due in 835 s and arrives in 189 s. So there is no position to fill and
|
|
80
|
-
// nothing to split, whatever the budget says.
|
|
81
|
-
//
|
|
82
|
-
// Measured rather than assumed: this is what the plan answers, and the test
|
|
83
|
-
// was written expecting two encoders before it was run.
|
|
84
|
-
const { actions } = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3 });
|
|
85
|
-
|
|
86
|
-
assert.deepEqual(
|
|
87
|
-
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
88
|
-
["keep #0..#-1"],
|
|
89
|
-
"one encoder, its road to the end of the film"
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("a stretch nobody is on is split at the middle, and the halves bound each other", () => {
|
|
94
|
-
// Both encoders are fresh, so both owe the same and `d - w` is zero: the
|
|
95
|
-
// meeting point is the midpoint, which is what halving always said. What is
|
|
96
|
-
// new is that each one's road ENDS where the next begins, so neither writes
|
|
97
|
-
// into the other's names.
|
|
98
|
-
const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 2, runs: [] }).actions;
|
|
99
|
-
|
|
100
|
-
assert.deepEqual(
|
|
101
|
-
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
102
|
-
["start #0..#100", "start #101..#199"],
|
|
103
|
-
"0..199 halved at 100"
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("a third encoder halves the widest half that is left", () => {
|
|
108
|
-
const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3, runs: [] }).actions;
|
|
109
|
-
|
|
110
|
-
assert.deepEqual(
|
|
111
|
-
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
112
|
-
["start #0..#50", "start #51..#100", "start #101..#199"],
|
|
113
|
-
"the first half is halved again, greedily, widest first"
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("a stretch of one piece is not split at all", () => {
|
|
118
|
-
const coverage = new CoverageMap({ segmentCount: 2 });
|
|
119
|
-
const actions = planEncoders({
|
|
120
|
-
coverage,
|
|
121
|
-
windows: [{ from: 0, to: 0, priority: 100, withinSeconds: 0, behind: false }],
|
|
122
|
-
runs: [],
|
|
123
|
-
maxRuns: 3,
|
|
124
|
-
segmentSeconds: SEGMENT_SECONDS,
|
|
125
|
-
speedX: 8.9,
|
|
126
|
-
killCostSec: 0.04,
|
|
127
|
-
firstByteWaitSec: 0.9,
|
|
128
|
-
moveCostSec: 0.94,
|
|
129
|
-
refetchSecPerFilmSecond: 0,
|
|
130
|
-
contentionPenaltyFor: () => 1,
|
|
131
|
-
now: 1_000_000
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
assert.equal(
|
|
135
|
-
actions.filter((one) => one.type === "start").length,
|
|
136
|
-
1,
|
|
137
|
-
"there is nothing to share"
|
|
138
|
-
);
|
|
139
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a second encoder joins a stretch, and whether it is worth having.
|
|
3
|
+
*
|
|
4
|
+
* Both halves are arithmetic and neither was asked before. The stretch used to
|
|
5
|
+
* be halved, unconditionally:
|
|
6
|
+
*
|
|
7
|
+
* widestFrom = from + Math.floor(Math.min(room, to - from + 1) / 2)
|
|
8
|
+
*
|
|
9
|
+
* Halving IS the answer when both encoders are fresh and owe the same. It is not
|
|
10
|
+
* when one of them is already partway through a piece, and it never asked the
|
|
11
|
+
* other question at all — whether two encoders under this host's measured
|
|
12
|
+
* contention beat one at full speed.
|
|
13
|
+
*
|
|
14
|
+
* The derivation. A stretch of unmade film runs from `from` to `to`; whoever is
|
|
15
|
+
* already on it stands at `from` and owes `w` before the piece under it exists;
|
|
16
|
+
* a fresh one placed at `x` owes `d` — its start and then a whole piece — and
|
|
17
|
+
* both then work at the rate two encoders leave each other:
|
|
18
|
+
*
|
|
19
|
+
* the one there closes [from, x-1]: w + (x - 1 - from) / r rises with x
|
|
20
|
+
* the fresh one closes [x, to]: d + (to - x) / r falls with x
|
|
21
|
+
*
|
|
22
|
+
* x* = (from + to + 1) / 2 + (d - w) * r / 2
|
|
23
|
+
*
|
|
24
|
+
* The shift is `firstByteWait / (2 * perPiece)` pieces, so it depends on how
|
|
25
|
+
* long a piece takes here: 0.24 of a piece at 4.45x on a 4.2 s grid, but 0.96 at
|
|
26
|
+
* 8.9x and 2.14 at 20x — a whole segment and more on the copy branch, where a
|
|
27
|
+
* piece is short and a start is not. Measuring only the re-encode rate
|
|
28
|
+
* understated it.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import test from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { planEncoders } from "../services/encode/EncodePlan.js";
|
|
34
|
+
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
35
|
+
|
|
36
|
+
const SEGMENT_SECONDS = 4.2;
|
|
37
|
+
const LAST = 199;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One encoder on a long stretch nobody has made, and room for more.
|
|
41
|
+
*
|
|
42
|
+
* @param {{ speedX: number, penalty: number, head?: number, ageMs?: number,
|
|
43
|
+
* maxRuns?: number }} params
|
|
44
|
+
* @returns {{ actions: object[], run: object }}
|
|
45
|
+
*/
|
|
46
|
+
function planFor({ speedX, penalty, head = 0, ageMs = 60_000, maxRuns = 3, runs = null }) {
|
|
47
|
+
const coverage = new CoverageMap({ segmentCount: LAST + 1 });
|
|
48
|
+
const run = { from: 0, to: -1, head, speedX, isAlive: true, startedAt: 1_000_000 };
|
|
49
|
+
const live = runs ?? [run];
|
|
50
|
+
for (const one of live) {
|
|
51
|
+
coverage.claim(one, one.from, one.to);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
run,
|
|
55
|
+
actions: planEncoders({
|
|
56
|
+
coverage,
|
|
57
|
+
// One zone over the whole film, due now, so nothing but the arithmetic of
|
|
58
|
+
// sharing it decides anything.
|
|
59
|
+
windows: [{ from: 0, to: LAST, priority: 100, withinSeconds: 0, behind: false }],
|
|
60
|
+
runs: live,
|
|
61
|
+
maxRuns,
|
|
62
|
+
segmentSeconds: SEGMENT_SECONDS,
|
|
63
|
+
speedX,
|
|
64
|
+
killCostSec: 0.04,
|
|
65
|
+
firstByteWaitSec: 0.9,
|
|
66
|
+
moveCostSec: 0.94,
|
|
67
|
+
refetchSecPerFilmSecond: 0,
|
|
68
|
+
// Measured on the addon host: 1.70 at 854x480, 1.98 at 1920x1080. Raised
|
|
69
|
+
// to the power of how many others there are, so a third costs again.
|
|
70
|
+
contentionPenaltyFor: (others) => (others <= 0 ? 1 : penalty ** others),
|
|
71
|
+
now: 1_000_000 + ageMs
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test("one encoder that reaches everything in time is left to do it alone", () => {
|
|
77
|
+
// A zone whose deadline GROWS with distance — which is what the map states,
|
|
78
|
+
// one segment at a time — is comfortably served by one encoder at 4.45x: #199
|
|
79
|
+
// is due in 835 s and arrives in 189 s. So there is no position to fill and
|
|
80
|
+
// nothing to split, whatever the budget says.
|
|
81
|
+
//
|
|
82
|
+
// Measured rather than assumed: this is what the plan answers, and the test
|
|
83
|
+
// was written expecting two encoders before it was run.
|
|
84
|
+
const { actions } = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3 });
|
|
85
|
+
|
|
86
|
+
assert.deepEqual(
|
|
87
|
+
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
88
|
+
["keep #0..#-1"],
|
|
89
|
+
"one encoder, its road to the end of the film"
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a stretch nobody is on is split at the middle, and the halves bound each other", () => {
|
|
94
|
+
// Both encoders are fresh, so both owe the same and `d - w` is zero: the
|
|
95
|
+
// meeting point is the midpoint, which is what halving always said. What is
|
|
96
|
+
// new is that each one's road ENDS where the next begins, so neither writes
|
|
97
|
+
// into the other's names.
|
|
98
|
+
const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 2, runs: [] }).actions;
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(
|
|
101
|
+
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
102
|
+
["start #0..#100", "start #101..#199"],
|
|
103
|
+
"0..199 halved at 100"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a third encoder halves the widest half that is left", () => {
|
|
108
|
+
const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3, runs: [] }).actions;
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
|
|
112
|
+
["start #0..#50", "start #51..#100", "start #101..#199"],
|
|
113
|
+
"the first half is halved again, greedily, widest first"
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a stretch of one piece is not split at all", () => {
|
|
118
|
+
const coverage = new CoverageMap({ segmentCount: 2 });
|
|
119
|
+
const actions = planEncoders({
|
|
120
|
+
coverage,
|
|
121
|
+
windows: [{ from: 0, to: 0, priority: 100, withinSeconds: 0, behind: false }],
|
|
122
|
+
runs: [],
|
|
123
|
+
maxRuns: 3,
|
|
124
|
+
segmentSeconds: SEGMENT_SECONDS,
|
|
125
|
+
speedX: 8.9,
|
|
126
|
+
killCostSec: 0.04,
|
|
127
|
+
firstByteWaitSec: 0.9,
|
|
128
|
+
moveCostSec: 0.94,
|
|
129
|
+
refetchSecPerFilmSecond: 0,
|
|
130
|
+
contentionPenaltyFor: () => 1,
|
|
131
|
+
now: 1_000_000
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.equal(
|
|
135
|
+
actions.filter((one) => one.type === "start").length,
|
|
136
|
+
1,
|
|
137
|
+
"there is nothing to share"
|
|
138
|
+
);
|
|
139
|
+
});
|