@torrent-tv/proxy 2.87.8 → 2.87.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +8 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/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 +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/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 +3 -0
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/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 +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -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 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -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
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What `/stream` does with a HEAD request.
|
|
3
|
-
*
|
|
4
|
-
* Fastify answers HEAD from the GET handler, so without an explicit branch a
|
|
5
|
-
* HEAD started a read of the entire file. Node discards the body, but the read
|
|
6
|
-
* itself runs on and the response never completes, which blocks the next
|
|
7
|
-
* request on that keep-alive connection. The keyframe index asks for the file
|
|
8
|
-
* size with exactly such a HEAD before every transcode session.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import fs from "node:fs/promises";
|
|
14
|
-
import os from "node:os";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { handleStreamGet } from "../routes/stream/get.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Minimal stand-ins for the parts of Fastify and the pool this route touches.
|
|
20
|
-
*
|
|
21
|
-
* @param {{ method: string, range?: string }} request
|
|
22
|
-
* @returns {{ req: object, reply: object, sent: object, opened: string[], state: object }}
|
|
23
|
-
*/
|
|
24
|
-
function harness({ method, range }) {
|
|
25
|
-
const opened = [];
|
|
26
|
-
const state = { prioritized: [] };
|
|
27
|
-
|
|
28
|
-
const sent = { code: 200, headers: {}, body: undefined, called: false };
|
|
29
|
-
const reply = {
|
|
30
|
-
code(value) {
|
|
31
|
-
sent.code = value;
|
|
32
|
-
return reply;
|
|
33
|
-
},
|
|
34
|
-
header(name, value) {
|
|
35
|
-
sent.headers[name.toLowerCase()] = value;
|
|
36
|
-
return reply;
|
|
37
|
-
},
|
|
38
|
-
send(body) {
|
|
39
|
-
sent.called = true;
|
|
40
|
-
sent.body = body;
|
|
41
|
-
return reply;
|
|
42
|
-
},
|
|
43
|
-
hijack() {
|
|
44
|
-
sent.hijacked = true;
|
|
45
|
-
},
|
|
46
|
-
raw: {
|
|
47
|
-
// `bindRelease` listens here; HEAD writes its response here.
|
|
48
|
-
once() {},
|
|
49
|
-
writeHead(code, headers) {
|
|
50
|
-
sent.code = code;
|
|
51
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
52
|
-
sent.headers[name.toLowerCase()] = value;
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
end() {
|
|
56
|
-
sent.called = true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const file = {
|
|
62
|
-
name: "movie.mkv",
|
|
63
|
-
length: 5_869_669_065,
|
|
64
|
-
createReadStream(options = {}) {
|
|
65
|
-
opened.push(`${options.start ?? 0}-${options.end ?? "end"}`);
|
|
66
|
-
return { on() {} };
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const torrentPool = {
|
|
71
|
-
async getTorrent() {
|
|
72
|
-
return { files: [file], sourceKey: "key" };
|
|
73
|
-
},
|
|
74
|
-
prioritizeByteRange(_torrent, fileIndex, byteStart, _windowBytes, options) {
|
|
75
|
-
state.prioritized.push({ byteStart, wholeFileRead: options?.wholeFileRead === true });
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const req = {
|
|
80
|
-
method,
|
|
81
|
-
query: { sourceType: "magnet", source: "magnet:?xt=urn:btih:abc", fileIndex: "0" },
|
|
82
|
-
headers: range ? { range } : {},
|
|
83
|
-
raw: { once() {} }
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
return { req, reply, sent, opened, state, deps: { sourceRegistry: { get: () => null }, torrentPool } };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
test("HEAD reports the size without opening a read", async () => {
|
|
90
|
-
const { req, reply, sent, opened, state, deps } = harness({ method: "HEAD" });
|
|
91
|
-
|
|
92
|
-
await handleStreamGet(req, reply, deps);
|
|
93
|
-
|
|
94
|
-
assert.deepEqual(opened, [], "HEAD started a read of the file");
|
|
95
|
-
assert.deepEqual(state.prioritized, [], "HEAD asked the swarm for a read it never made");
|
|
96
|
-
// The real size, not the zero Fastify substitutes for an empty payload — the
|
|
97
|
-
// keyframe index reads this header and treats 0 as "no index".
|
|
98
|
-
assert.equal(sent.headers["content-length"], "5869669065");
|
|
99
|
-
assert.equal(sent.headers["accept-ranges"], "bytes");
|
|
100
|
-
assert.equal(sent.called, true, "HEAD never completed its response");
|
|
101
|
-
assert.equal(sent.body, undefined, "HEAD answered with a body");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("GET still streams the bytes", async () => {
|
|
105
|
-
const { req, reply, sent, opened, deps } = harness({ method: "GET" });
|
|
106
|
-
|
|
107
|
-
await handleStreamGet(req, reply, deps);
|
|
108
|
-
|
|
109
|
-
assert.equal(opened.length, 1, "GET did not open a read");
|
|
110
|
-
assert.equal(sent.headers["content-length"], "5869669065");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("GET with a range streams only that range", async () => {
|
|
114
|
-
const { req, reply, sent, opened, deps } = harness({ method: "GET", range: "bytes=100-199" });
|
|
115
|
-
|
|
116
|
-
await handleStreamGet(req, reply, deps);
|
|
117
|
-
|
|
118
|
-
assert.deepEqual(opened, ["100-199"]);
|
|
119
|
-
assert.equal(sent.code, 206);
|
|
120
|
-
assert.equal(sent.headers["content-range"], "bytes 100-199/5869669065");
|
|
121
|
-
assert.equal(sent.headers["content-length"], "100");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// A request with no byte range says nothing about where the viewer is: ffmpeg
|
|
125
|
-
// opens its input with a plain GET and abandons it the moment it seeks, and the
|
|
126
|
-
// keyframe index and the codec probe do the same — four such reads around every
|
|
127
|
-
// encoder restart. Reported as ordinary reads at offset 0, they undid the seek
|
|
128
|
-
// that had just happened and sent the swarm walking the file from its first
|
|
129
|
-
// missing piece; a seek to 89.1% of a 4.7 GB film downloaded 2.47 GB that way.
|
|
130
|
-
test("a range-less GET is reported as a whole-file read", async () => {
|
|
131
|
-
const { req, reply, state, deps } = harness({ method: "GET" });
|
|
132
|
-
|
|
133
|
-
await handleStreamGet(req, reply, deps);
|
|
134
|
-
|
|
135
|
-
assert.deepEqual(state.prioritized, [{ byteStart: 0, wholeFileRead: true }]);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("a ranged GET is reported as a real read position", async () => {
|
|
139
|
-
const { req, reply, state, deps } = harness({ method: "GET", range: "bytes=4390000000-" });
|
|
140
|
-
|
|
141
|
-
await handleStreamGet(req, reply, deps);
|
|
142
|
-
|
|
143
|
-
assert.deepEqual(state.prioritized, [{ byteStart: 4_390_000_000, wholeFileRead: false }]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("a file downloaded whole is served from disk without touching the torrent", async () => {
|
|
147
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), "stream-whole-"));
|
|
148
|
-
const where = path.join(root, "0");
|
|
149
|
-
const bytes = Buffer.from("a film that is a file now", "utf8");
|
|
150
|
-
await fs.writeFile(where, bytes);
|
|
151
|
-
try {
|
|
152
|
-
const { req, reply, sent, deps } = harness({ method: "GET", range: "bytes=2-6" });
|
|
153
|
-
let asked = false;
|
|
154
|
-
deps.torrentPool.getTorrent = async () => {
|
|
155
|
-
asked = true;
|
|
156
|
-
throw new Error("the torrent was asked for, which is what this avoids");
|
|
157
|
-
};
|
|
158
|
-
deps.torrentPool.wholeFiles = new Map([
|
|
159
|
-
["abc/0", { path: where, length: bytes.length, name: "film.mkv" }]
|
|
160
|
-
]);
|
|
161
|
-
// The source key IS the identity — `torrent:<infohash>` — and it is all the
|
|
162
|
-
// route needs to find the file.
|
|
163
|
-
deps.sourceRegistry = { get: () => ({ sourceType: "torrent", source: "magnet:?xt=urn:btih:abc" }) };
|
|
164
|
-
req.query = { sourceKey: "torrent:abc", fileIndex: "0" };
|
|
165
|
-
|
|
166
|
-
await handleStreamGet(req, reply, deps);
|
|
167
|
-
|
|
168
|
-
assert.equal(asked, false, "the torrent was asked for");
|
|
169
|
-
assert.equal(sent.code, 206);
|
|
170
|
-
assert.equal(sent.headers["content-range"], `bytes 2-6/${bytes.length}`);
|
|
171
|
-
assert.equal(sent.headers["content-length"], "5");
|
|
172
|
-
} finally {
|
|
173
|
-
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("a file this proxy does not have whole still goes to the torrent", async () => {
|
|
178
|
-
const { req, reply, state, deps } = harness({ method: "GET", range: "bytes=0-99" });
|
|
179
|
-
deps.torrentPool.wholeFiles = new Map([["other/7", { path: "/nowhere", length: 1, name: "x" }]]);
|
|
180
|
-
await handleStreamGet(req, reply, deps);
|
|
181
|
-
assert.equal(state.prioritized.length, 1, "the ordinary path was not taken");
|
|
182
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What `/stream` does with a HEAD request.
|
|
3
|
+
*
|
|
4
|
+
* Fastify answers HEAD from the GET handler, so without an explicit branch a
|
|
5
|
+
* HEAD started a read of the entire file. Node discards the body, but the read
|
|
6
|
+
* itself runs on and the response never completes, which blocks the next
|
|
7
|
+
* request on that keep-alive connection. The keyframe index asks for the file
|
|
8
|
+
* size with exactly such a HEAD before every transcode session.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { handleStreamGet } from "../routes/stream/get.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal stand-ins for the parts of Fastify and the pool this route touches.
|
|
20
|
+
*
|
|
21
|
+
* @param {{ method: string, range?: string }} request
|
|
22
|
+
* @returns {{ req: object, reply: object, sent: object, opened: string[], state: object }}
|
|
23
|
+
*/
|
|
24
|
+
function harness({ method, range }) {
|
|
25
|
+
const opened = [];
|
|
26
|
+
const state = { prioritized: [] };
|
|
27
|
+
|
|
28
|
+
const sent = { code: 200, headers: {}, body: undefined, called: false };
|
|
29
|
+
const reply = {
|
|
30
|
+
code(value) {
|
|
31
|
+
sent.code = value;
|
|
32
|
+
return reply;
|
|
33
|
+
},
|
|
34
|
+
header(name, value) {
|
|
35
|
+
sent.headers[name.toLowerCase()] = value;
|
|
36
|
+
return reply;
|
|
37
|
+
},
|
|
38
|
+
send(body) {
|
|
39
|
+
sent.called = true;
|
|
40
|
+
sent.body = body;
|
|
41
|
+
return reply;
|
|
42
|
+
},
|
|
43
|
+
hijack() {
|
|
44
|
+
sent.hijacked = true;
|
|
45
|
+
},
|
|
46
|
+
raw: {
|
|
47
|
+
// `bindRelease` listens here; HEAD writes its response here.
|
|
48
|
+
once() {},
|
|
49
|
+
writeHead(code, headers) {
|
|
50
|
+
sent.code = code;
|
|
51
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
52
|
+
sent.headers[name.toLowerCase()] = value;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
end() {
|
|
56
|
+
sent.called = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const file = {
|
|
62
|
+
name: "movie.mkv",
|
|
63
|
+
length: 5_869_669_065,
|
|
64
|
+
createReadStream(options = {}) {
|
|
65
|
+
opened.push(`${options.start ?? 0}-${options.end ?? "end"}`);
|
|
66
|
+
return { on() {} };
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const torrentPool = {
|
|
71
|
+
async getTorrent() {
|
|
72
|
+
return { files: [file], sourceKey: "key" };
|
|
73
|
+
},
|
|
74
|
+
prioritizeByteRange(_torrent, fileIndex, byteStart, _windowBytes, options) {
|
|
75
|
+
state.prioritized.push({ byteStart, wholeFileRead: options?.wholeFileRead === true });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const req = {
|
|
80
|
+
method,
|
|
81
|
+
query: { sourceType: "magnet", source: "magnet:?xt=urn:btih:abc", fileIndex: "0" },
|
|
82
|
+
headers: range ? { range } : {},
|
|
83
|
+
raw: { once() {} }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return { req, reply, sent, opened, state, deps: { sourceRegistry: { get: () => null }, torrentPool } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test("HEAD reports the size without opening a read", async () => {
|
|
90
|
+
const { req, reply, sent, opened, state, deps } = harness({ method: "HEAD" });
|
|
91
|
+
|
|
92
|
+
await handleStreamGet(req, reply, deps);
|
|
93
|
+
|
|
94
|
+
assert.deepEqual(opened, [], "HEAD started a read of the file");
|
|
95
|
+
assert.deepEqual(state.prioritized, [], "HEAD asked the swarm for a read it never made");
|
|
96
|
+
// The real size, not the zero Fastify substitutes for an empty payload — the
|
|
97
|
+
// keyframe index reads this header and treats 0 as "no index".
|
|
98
|
+
assert.equal(sent.headers["content-length"], "5869669065");
|
|
99
|
+
assert.equal(sent.headers["accept-ranges"], "bytes");
|
|
100
|
+
assert.equal(sent.called, true, "HEAD never completed its response");
|
|
101
|
+
assert.equal(sent.body, undefined, "HEAD answered with a body");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("GET still streams the bytes", async () => {
|
|
105
|
+
const { req, reply, sent, opened, deps } = harness({ method: "GET" });
|
|
106
|
+
|
|
107
|
+
await handleStreamGet(req, reply, deps);
|
|
108
|
+
|
|
109
|
+
assert.equal(opened.length, 1, "GET did not open a read");
|
|
110
|
+
assert.equal(sent.headers["content-length"], "5869669065");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("GET with a range streams only that range", async () => {
|
|
114
|
+
const { req, reply, sent, opened, deps } = harness({ method: "GET", range: "bytes=100-199" });
|
|
115
|
+
|
|
116
|
+
await handleStreamGet(req, reply, deps);
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(opened, ["100-199"]);
|
|
119
|
+
assert.equal(sent.code, 206);
|
|
120
|
+
assert.equal(sent.headers["content-range"], "bytes 100-199/5869669065");
|
|
121
|
+
assert.equal(sent.headers["content-length"], "100");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// A request with no byte range says nothing about where the viewer is: ffmpeg
|
|
125
|
+
// opens its input with a plain GET and abandons it the moment it seeks, and the
|
|
126
|
+
// keyframe index and the codec probe do the same — four such reads around every
|
|
127
|
+
// encoder restart. Reported as ordinary reads at offset 0, they undid the seek
|
|
128
|
+
// that had just happened and sent the swarm walking the file from its first
|
|
129
|
+
// missing piece; a seek to 89.1% of a 4.7 GB film downloaded 2.47 GB that way.
|
|
130
|
+
test("a range-less GET is reported as a whole-file read", async () => {
|
|
131
|
+
const { req, reply, state, deps } = harness({ method: "GET" });
|
|
132
|
+
|
|
133
|
+
await handleStreamGet(req, reply, deps);
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(state.prioritized, [{ byteStart: 0, wholeFileRead: true }]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a ranged GET is reported as a real read position", async () => {
|
|
139
|
+
const { req, reply, state, deps } = harness({ method: "GET", range: "bytes=4390000000-" });
|
|
140
|
+
|
|
141
|
+
await handleStreamGet(req, reply, deps);
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(state.prioritized, [{ byteStart: 4_390_000_000, wholeFileRead: false }]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("a file downloaded whole is served from disk without touching the torrent", async () => {
|
|
147
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "stream-whole-"));
|
|
148
|
+
const where = path.join(root, "0");
|
|
149
|
+
const bytes = Buffer.from("a film that is a file now", "utf8");
|
|
150
|
+
await fs.writeFile(where, bytes);
|
|
151
|
+
try {
|
|
152
|
+
const { req, reply, sent, deps } = harness({ method: "GET", range: "bytes=2-6" });
|
|
153
|
+
let asked = false;
|
|
154
|
+
deps.torrentPool.getTorrent = async () => {
|
|
155
|
+
asked = true;
|
|
156
|
+
throw new Error("the torrent was asked for, which is what this avoids");
|
|
157
|
+
};
|
|
158
|
+
deps.torrentPool.wholeFiles = new Map([
|
|
159
|
+
["abc/0", { path: where, length: bytes.length, name: "film.mkv" }]
|
|
160
|
+
]);
|
|
161
|
+
// The source key IS the identity — `torrent:<infohash>` — and it is all the
|
|
162
|
+
// route needs to find the file.
|
|
163
|
+
deps.sourceRegistry = { get: () => ({ sourceType: "torrent", source: "magnet:?xt=urn:btih:abc" }) };
|
|
164
|
+
req.query = { sourceKey: "torrent:abc", fileIndex: "0" };
|
|
165
|
+
|
|
166
|
+
await handleStreamGet(req, reply, deps);
|
|
167
|
+
|
|
168
|
+
assert.equal(asked, false, "the torrent was asked for");
|
|
169
|
+
assert.equal(sent.code, 206);
|
|
170
|
+
assert.equal(sent.headers["content-range"], `bytes 2-6/${bytes.length}`);
|
|
171
|
+
assert.equal(sent.headers["content-length"], "5");
|
|
172
|
+
} finally {
|
|
173
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a file this proxy does not have whole still goes to the torrent", async () => {
|
|
178
|
+
const { req, reply, state, deps } = harness({ method: "GET", range: "bytes=0-99" });
|
|
179
|
+
deps.torrentPool.wholeFiles = new Map([["other/7", { path: "/nowhere", length: 1, name: "x" }]]);
|
|
180
|
+
await handleStreamGet(req, reply, deps);
|
|
181
|
+
assert.equal(state.prioritized.length, 1, "the ordinary path was not taken");
|
|
182
|
+
});
|