@torrent-tv/proxy 2.89.0 → 2.89.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +6 -1
- package/CHANGELOG.md +15 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +42 -8
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2282
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -349
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +476 -458
- package/services/encode/segment-formats/index.js +103 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +604 -541
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +790 -786
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1024 -1020
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1255 -1255
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +67 -0
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -0
- package/services/{server → viewer}/playback-readiness.js +635 -516
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -70
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-presentation-clock.test.js +146 -162
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -112
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -0
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -483
- package/test/playback-start-regressions.test.js +101 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/whole-file-held-read.test.js +30 -0
- 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,116 +1,116 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Presence is a fact of the connection, and it is not the same fact as
|
|
3
|
-
* position.
|
|
4
|
-
*
|
|
5
|
-
* Both halves failed together on 2026-09-05. A viewer who had arrived and asked
|
|
6
|
-
* for nothing counted as absent, so a soundtrack's encoder was stopped 1.25 s
|
|
7
|
-
* after starting with nothing produced — and its `init.mp4` was therefore never
|
|
8
|
-
* made, which is the one file the viewer needed in order to ask for the segment
|
|
9
|
-
* that would have marked them present. And nothing anywhere let a viewer go
|
|
10
|
-
* when their connection closed: the only exits were the browser's own release,
|
|
11
|
-
* which a killed tab never sends, and a silence that a paused viewer produces
|
|
12
|
-
* just as well as a departed one.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
18
|
-
import os from "node:os";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
21
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
22
|
-
import { Viewers } from "../services/viewer/Viewers.js";
|
|
23
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
24
|
-
|
|
25
|
-
const PICTURE = "aaaaaaaa00004000";
|
|
26
|
-
const SOUND = "aaaaaaaa00004001";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {string} id
|
|
30
|
-
* @param {string} dirPath
|
|
31
|
-
* @returns {object}
|
|
32
|
-
*/
|
|
33
|
-
function outputOn(id, dirPath) {
|
|
34
|
-
return {
|
|
35
|
-
id,
|
|
36
|
-
spec: outputSpec(),
|
|
37
|
-
dirPath,
|
|
38
|
-
state: "live",
|
|
39
|
-
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
|
|
40
|
-
get inputFile() { return this.file; },
|
|
41
|
-
get audioFile() { return this.file; },
|
|
42
|
-
claims: new Set(),
|
|
43
|
-
lastAccessedAt: Date.now()
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
test("asking for a viewer of an output is that viewer watching it", () => {
|
|
48
|
-
let changes = 0;
|
|
49
|
-
const viewers = new Viewers({ onChange: () => { changes += 1; } });
|
|
50
|
-
const output = { id: "out-1" };
|
|
51
|
-
|
|
52
|
-
const first = viewers.of(output, "someone");
|
|
53
|
-
assert.equal(changes, 1, "a new relation is a change");
|
|
54
|
-
assert.equal(first.outputs.has("out-1"), true);
|
|
55
|
-
|
|
56
|
-
viewers.of(output, "someone");
|
|
57
|
-
assert.equal(changes, 1, "asking again about the same output changes nothing");
|
|
58
|
-
|
|
59
|
-
const second = { id: "out-2" };
|
|
60
|
-
viewers.of(second, "someone");
|
|
61
|
-
assert.equal(changes, 2, "a second output is a change");
|
|
62
|
-
assert.equal(viewers.get("someone")?.outputs.size, 2);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("a connection closing takes the person off every output at once", () => {
|
|
66
|
-
const viewers = new Viewers();
|
|
67
|
-
const picture = { id: "picture" };
|
|
68
|
-
const sound = { id: "sound" };
|
|
69
|
-
viewers.of(picture, "watcher");
|
|
70
|
-
viewers.of(sound, "watcher");
|
|
71
|
-
viewers.of(picture, "other");
|
|
72
|
-
|
|
73
|
-
const left = viewers.hasGone("watcher");
|
|
74
|
-
|
|
75
|
-
assert.deepEqual(left.sort(), ["picture", "sound"], "both, not the one the browser holds an id for");
|
|
76
|
-
assert.equal(viewers.forOutput(picture).has("watcher"), false);
|
|
77
|
-
assert.equal(viewers.forOutput(sound).has("watcher"), false);
|
|
78
|
-
assert.equal(viewers.forOutput(picture).has("other"), true, "and nobody else goes with them");
|
|
79
|
-
assert.equal(viewers.get("watcher"), null, "the registry does not keep what it has let go");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("a viewer whose connection closed is let go of every output they were watching", async (t) => {
|
|
83
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "presence-"));
|
|
84
|
-
t.after(async () => {
|
|
85
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
86
|
-
});
|
|
87
|
-
const manager = wireOutputs({
|
|
88
|
-
enabled: true,
|
|
89
|
-
ffmpegBin: "ffmpeg",
|
|
90
|
-
localBindHost: "127.0.0.1",
|
|
91
|
-
localPort: 9090
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const picture = outputOn(PICTURE, dirPath);
|
|
95
|
-
const sound = outputOn(SOUND, dirPath);
|
|
96
|
-
manager.outputs.set(PICTURE, picture);
|
|
97
|
-
manager.outputs.set(SOUND, sound);
|
|
98
|
-
manager.viewers.of(picture, "watcher");
|
|
99
|
-
manager.viewers.of(sound, "watcher");
|
|
100
|
-
|
|
101
|
-
const released = await manager.lifecycle.viewerHasGone("watcher", "the connection closed");
|
|
102
|
-
|
|
103
|
-
assert.equal(released, 2, "the picture and the soundtrack, from one statement");
|
|
104
|
-
assert.equal(manager.viewers.get("watcher"), null);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("a viewer nobody knows costs nothing to let go of", async () => {
|
|
108
|
-
const manager = wireOutputs({
|
|
109
|
-
enabled: true,
|
|
110
|
-
ffmpegBin: "ffmpeg",
|
|
111
|
-
localBindHost: "127.0.0.1",
|
|
112
|
-
localPort: 9090
|
|
113
|
-
});
|
|
114
|
-
assert.equal(await manager.lifecycle.viewerHasGone("never-seen"), 0);
|
|
115
|
-
assert.equal(await manager.lifecycle.viewerHasGone(""), 0);
|
|
116
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Presence is a fact of the connection, and it is not the same fact as
|
|
3
|
+
* position.
|
|
4
|
+
*
|
|
5
|
+
* Both halves failed together on 2026-09-05. A viewer who had arrived and asked
|
|
6
|
+
* for nothing counted as absent, so a soundtrack's encoder was stopped 1.25 s
|
|
7
|
+
* after starting with nothing produced — and its `init.mp4` was therefore never
|
|
8
|
+
* made, which is the one file the viewer needed in order to ask for the segment
|
|
9
|
+
* that would have marked them present. And nothing anywhere let a viewer go
|
|
10
|
+
* when their connection closed: the only exits were the browser's own release,
|
|
11
|
+
* which a killed tab never sends, and a silence that a paused viewer produces
|
|
12
|
+
* just as well as a departed one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
21
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
22
|
+
import { Viewers } from "../services/viewer/Viewers.js";
|
|
23
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
24
|
+
|
|
25
|
+
const PICTURE = "aaaaaaaa00004000";
|
|
26
|
+
const SOUND = "aaaaaaaa00004001";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} id
|
|
30
|
+
* @param {string} dirPath
|
|
31
|
+
* @returns {object}
|
|
32
|
+
*/
|
|
33
|
+
function outputOn(id, dirPath) {
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
spec: outputSpec(),
|
|
37
|
+
dirPath,
|
|
38
|
+
state: "live",
|
|
39
|
+
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
|
|
40
|
+
get inputFile() { return this.file; },
|
|
41
|
+
get audioFile() { return this.file; },
|
|
42
|
+
claims: new Set(),
|
|
43
|
+
lastAccessedAt: Date.now()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("asking for a viewer of an output is that viewer watching it", () => {
|
|
48
|
+
let changes = 0;
|
|
49
|
+
const viewers = new Viewers({ onChange: () => { changes += 1; } });
|
|
50
|
+
const output = { id: "out-1" };
|
|
51
|
+
|
|
52
|
+
const first = viewers.of(output, "someone");
|
|
53
|
+
assert.equal(changes, 1, "a new relation is a change");
|
|
54
|
+
assert.equal(first.outputs.has("out-1"), true);
|
|
55
|
+
|
|
56
|
+
viewers.of(output, "someone");
|
|
57
|
+
assert.equal(changes, 1, "asking again about the same output changes nothing");
|
|
58
|
+
|
|
59
|
+
const second = { id: "out-2" };
|
|
60
|
+
viewers.of(second, "someone");
|
|
61
|
+
assert.equal(changes, 2, "a second output is a change");
|
|
62
|
+
assert.equal(viewers.get("someone")?.outputs.size, 2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a connection closing takes the person off every output at once", () => {
|
|
66
|
+
const viewers = new Viewers();
|
|
67
|
+
const picture = { id: "picture" };
|
|
68
|
+
const sound = { id: "sound" };
|
|
69
|
+
viewers.of(picture, "watcher");
|
|
70
|
+
viewers.of(sound, "watcher");
|
|
71
|
+
viewers.of(picture, "other");
|
|
72
|
+
|
|
73
|
+
const left = viewers.hasGone("watcher");
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(left.sort(), ["picture", "sound"], "both, not the one the browser holds an id for");
|
|
76
|
+
assert.equal(viewers.forOutput(picture).has("watcher"), false);
|
|
77
|
+
assert.equal(viewers.forOutput(sound).has("watcher"), false);
|
|
78
|
+
assert.equal(viewers.forOutput(picture).has("other"), true, "and nobody else goes with them");
|
|
79
|
+
assert.equal(viewers.get("watcher"), null, "the registry does not keep what it has let go");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a viewer whose connection closed is let go of every output they were watching", async (t) => {
|
|
83
|
+
const dirPath = await mkdtemp(path.join(os.tmpdir(), "presence-"));
|
|
84
|
+
t.after(async () => {
|
|
85
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
const manager = wireOutputs({
|
|
88
|
+
enabled: true,
|
|
89
|
+
ffmpegBin: "ffmpeg",
|
|
90
|
+
localBindHost: "127.0.0.1",
|
|
91
|
+
localPort: 9090
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const picture = outputOn(PICTURE, dirPath);
|
|
95
|
+
const sound = outputOn(SOUND, dirPath);
|
|
96
|
+
manager.outputs.set(PICTURE, picture);
|
|
97
|
+
manager.outputs.set(SOUND, sound);
|
|
98
|
+
manager.viewers.of(picture, "watcher");
|
|
99
|
+
manager.viewers.of(sound, "watcher");
|
|
100
|
+
|
|
101
|
+
const released = await manager.lifecycle.viewerHasGone("watcher", "the connection closed");
|
|
102
|
+
|
|
103
|
+
assert.equal(released, 2, "the picture and the soundtrack, from one statement");
|
|
104
|
+
assert.equal(manager.viewers.get("watcher"), null);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a viewer nobody knows costs nothing to let go of", async () => {
|
|
108
|
+
const manager = wireOutputs({
|
|
109
|
+
enabled: true,
|
|
110
|
+
ffmpegBin: "ffmpeg",
|
|
111
|
+
localBindHost: "127.0.0.1",
|
|
112
|
+
localPort: 9090
|
|
113
|
+
});
|
|
114
|
+
assert.equal(await manager.lifecycle.viewerHasGone("never-seen"), 0);
|
|
115
|
+
assert.equal(await manager.lifecycle.viewerHasGone(""), 0);
|
|
116
|
+
});
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Opening an output admits and places its viewer in one synchronous call.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
|
|
8
|
-
import { ViewerRequests } from "../services/server/ViewerRequests.js";
|
|
9
|
-
|
|
10
|
-
test("the first opening holds capacity before a concurrent opening is admitted", async () => {
|
|
11
|
-
const records = new Map();
|
|
12
|
-
const outputCosts = new Map([
|
|
13
|
-
["source-a:0", 0.6],
|
|
14
|
-
["source-b:0", 0.6]
|
|
15
|
-
]);
|
|
16
|
-
const viewers = {
|
|
17
|
-
get: () => null,
|
|
18
|
-
forOutput(output) {
|
|
19
|
-
return new Set(records.get(output.outputKey)?.keys() ?? []);
|
|
20
|
-
},
|
|
21
|
-
of(output, consumerId) {
|
|
22
|
-
let viewersOnOutput = records.get(output.outputKey);
|
|
23
|
-
if (!viewersOnOutput) {
|
|
24
|
-
viewersOnOutput = new Map();
|
|
25
|
-
records.set(output.outputKey, viewersOnOutput);
|
|
26
|
-
}
|
|
27
|
-
let viewer = viewersOnOutput.get(consumerId);
|
|
28
|
-
if (!viewer) {
|
|
29
|
-
viewer = {
|
|
30
|
-
position: null,
|
|
31
|
-
linkReading: () => null,
|
|
32
|
-
noteVisiblePicture: () => {}
|
|
33
|
-
};
|
|
34
|
-
viewersOnOutput.set(consumerId, viewer);
|
|
35
|
-
}
|
|
36
|
-
return viewer;
|
|
37
|
-
},
|
|
38
|
-
watchedAddresses() {
|
|
39
|
-
return new Set(
|
|
40
|
-
[...records]
|
|
41
|
-
.filter(([, members]) => [...members.values()].some((viewer) => viewer.position !== null))
|
|
42
|
-
.map(([address]) => address)
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
const admission = new EncodeAdmission({
|
|
47
|
-
liveRunsByAddress: () => new Map(),
|
|
48
|
-
preparedAddresses: () => new Set(),
|
|
49
|
-
watchedAddresses: () => viewers.watchedAddresses(),
|
|
50
|
-
loadOf: (address) => {
|
|
51
|
-
const costSec = outputCosts.get(address);
|
|
52
|
-
return costSec === undefined ? null : { costSec, fileKey: address, fileCostSec: 0 };
|
|
53
|
-
},
|
|
54
|
-
availability: () => ({ share: 1, known: true }),
|
|
55
|
-
finished: () => false
|
|
56
|
-
});
|
|
57
|
-
const opening = {
|
|
58
|
-
async open({ sourceKey, fileIndex, claim }) {
|
|
59
|
-
const output = { id: `${sourceKey}:${fileIndex}`, outputKey: `${sourceKey}:${fileIndex}` };
|
|
60
|
-
const answer = claim(output);
|
|
61
|
-
if (!answer.admitted) {
|
|
62
|
-
const error = new Error(answer.reason);
|
|
63
|
-
error.code = "OUTPUT_NO_CAPACITY";
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
return { output, existed: false, audio: null, verdict: null };
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
const requests = new ViewerRequests({
|
|
70
|
-
opening,
|
|
71
|
-
viewers,
|
|
72
|
-
admitsWatching: (output) => admission.admitsWatching(output.outputKey),
|
|
73
|
-
outputTimes: { segmentIndexForTime: (_output, seconds) => Math.floor(seconds / 4) },
|
|
74
|
-
planEncodersSoon: () => {},
|
|
75
|
-
outputs: { touch: () => {} },
|
|
76
|
-
noteServingVerdict: () => {},
|
|
77
|
-
waitUntilReady: async () => {}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const first = requests.createOrGetSession({ sourceKey: "source-a", fileIndex: 0, consumerId: "viewer-a" });
|
|
81
|
-
const second = requests.createOrGetSession({ sourceKey: "source-b", fileIndex: 0, consumerId: "viewer-b" });
|
|
82
|
-
const [firstResult, secondResult] = await Promise.allSettled([first, second]);
|
|
83
|
-
|
|
84
|
-
assert.equal(firstResult.status, "fulfilled");
|
|
85
|
-
assert.equal(secondResult.status, "rejected");
|
|
86
|
-
assert.match(secondResult.reason.message, /would make 0\.83x/);
|
|
87
|
-
assert.equal(viewers.forOutput({ outputKey: "source-a:0" }).has("viewer-a"), true);
|
|
88
|
-
assert.equal(viewers.forOutput({ outputKey: "source-b:0" }).has("viewer-b"), false);
|
|
89
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Opening an output admits and places its viewer in one synchronous call.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
|
|
8
|
+
import { ViewerRequests } from "../services/server/ViewerRequests.js";
|
|
9
|
+
|
|
10
|
+
test("the first opening holds capacity before a concurrent opening is admitted", async () => {
|
|
11
|
+
const records = new Map();
|
|
12
|
+
const outputCosts = new Map([
|
|
13
|
+
["source-a:0", 0.6],
|
|
14
|
+
["source-b:0", 0.6]
|
|
15
|
+
]);
|
|
16
|
+
const viewers = {
|
|
17
|
+
get: () => null,
|
|
18
|
+
forOutput(output) {
|
|
19
|
+
return new Set(records.get(output.outputKey)?.keys() ?? []);
|
|
20
|
+
},
|
|
21
|
+
of(output, consumerId) {
|
|
22
|
+
let viewersOnOutput = records.get(output.outputKey);
|
|
23
|
+
if (!viewersOnOutput) {
|
|
24
|
+
viewersOnOutput = new Map();
|
|
25
|
+
records.set(output.outputKey, viewersOnOutput);
|
|
26
|
+
}
|
|
27
|
+
let viewer = viewersOnOutput.get(consumerId);
|
|
28
|
+
if (!viewer) {
|
|
29
|
+
viewer = {
|
|
30
|
+
position: null,
|
|
31
|
+
linkReading: () => null,
|
|
32
|
+
noteVisiblePicture: () => {}
|
|
33
|
+
};
|
|
34
|
+
viewersOnOutput.set(consumerId, viewer);
|
|
35
|
+
}
|
|
36
|
+
return viewer;
|
|
37
|
+
},
|
|
38
|
+
watchedAddresses() {
|
|
39
|
+
return new Set(
|
|
40
|
+
[...records]
|
|
41
|
+
.filter(([, members]) => [...members.values()].some((viewer) => viewer.position !== null))
|
|
42
|
+
.map(([address]) => address)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const admission = new EncodeAdmission({
|
|
47
|
+
liveRunsByAddress: () => new Map(),
|
|
48
|
+
preparedAddresses: () => new Set(),
|
|
49
|
+
watchedAddresses: () => viewers.watchedAddresses(),
|
|
50
|
+
loadOf: (address) => {
|
|
51
|
+
const costSec = outputCosts.get(address);
|
|
52
|
+
return costSec === undefined ? null : { costSec, fileKey: address, fileCostSec: 0 };
|
|
53
|
+
},
|
|
54
|
+
availability: () => ({ share: 1, known: true }),
|
|
55
|
+
finished: () => false
|
|
56
|
+
});
|
|
57
|
+
const opening = {
|
|
58
|
+
async open({ sourceKey, fileIndex, claim }) {
|
|
59
|
+
const output = { id: `${sourceKey}:${fileIndex}`, outputKey: `${sourceKey}:${fileIndex}` };
|
|
60
|
+
const answer = claim(output);
|
|
61
|
+
if (!answer.admitted) {
|
|
62
|
+
const error = new Error(answer.reason);
|
|
63
|
+
error.code = "OUTPUT_NO_CAPACITY";
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return { output, existed: false, audio: null, verdict: null };
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const requests = new ViewerRequests({
|
|
70
|
+
opening,
|
|
71
|
+
viewers,
|
|
72
|
+
admitsWatching: (output) => admission.admitsWatching(output.outputKey),
|
|
73
|
+
outputTimes: { segmentIndexForTime: (_output, seconds) => Math.floor(seconds / 4) },
|
|
74
|
+
planEncodersSoon: () => {},
|
|
75
|
+
outputs: { touch: () => {} },
|
|
76
|
+
noteServingVerdict: () => {},
|
|
77
|
+
waitUntilReady: async () => {}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const first = requests.createOrGetSession({ sourceKey: "source-a", fileIndex: 0, consumerId: "viewer-a" });
|
|
81
|
+
const second = requests.createOrGetSession({ sourceKey: "source-b", fileIndex: 0, consumerId: "viewer-b" });
|
|
82
|
+
const [firstResult, secondResult] = await Promise.allSettled([first, second]);
|
|
83
|
+
|
|
84
|
+
assert.equal(firstResult.status, "fulfilled");
|
|
85
|
+
assert.equal(secondResult.status, "rejected");
|
|
86
|
+
assert.match(secondResult.reason.message, /would make 0\.83x/);
|
|
87
|
+
assert.equal(viewers.forOutput({ outputKey: "source-a:0" }).has("viewer-a"), true);
|
|
88
|
+
assert.equal(viewers.forOutput({ outputKey: "source-b:0" }).has("viewer-b"), false);
|
|
89
|
+
});
|