@torrent-tv/proxy 2.87.10 → 2.87.12
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 +2191 -2181
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +124 -101
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -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 +136 -129
- package/routes/api/transcode-sessions/post.js +173 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -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 +758 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1141 -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 +315 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +245 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +727 -522
- 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/playback-readiness.js +620 -0
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +992 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +365 -365
- 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/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- 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 +682 -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-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 +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +323 -0
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,325 +1,325 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The priority map, built once and handed to everybody who acts on it.
|
|
3
|
-
*
|
|
4
|
-
* One map per film, in seconds of film against a number. It is built from where
|
|
5
|
-
* the viewers are and nothing else, and both of the things that do work — the
|
|
6
|
-
* encoding and the downloading — read it and decide for themselves. They do not
|
|
7
|
-
* talk to each other, and neither of them tells this class anything.
|
|
8
|
-
*
|
|
9
|
-
* **Why it has to be published rather than asked for.** The downloading lives
|
|
10
|
-
* in another thread. Until now it took its orders from the reads themselves:
|
|
11
|
-
* every read declared a window around its own head, so fifteen reads declared
|
|
12
|
-
* fifteen windows on a piece store that holds sixteen pieces. Half of all
|
|
13
|
-
* evictions then took a piece a reader had said it wanted, two thirds of reads
|
|
14
|
-
* came back from disk, and what `/stream` handed out stopped being the file's
|
|
15
|
-
* bytes — twenty-two source-parse errors, a segment the player could not
|
|
16
|
-
* append, and an empty picture for six minutes (field 2026-09-05).
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { emptyMap, mapForViewer, mergeMaps, runsOf } from "./PriorityMap.js";
|
|
20
|
-
|
|
21
|
-
export class PriorityOrchestrator {
|
|
22
|
-
/** Where the map goes once it is built. @type {(published: object) => void} */
|
|
23
|
-
#publish;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The last map published per film and file, so an unchanged one is not
|
|
27
|
-
* resent — and what it was about, so that a file everybody has left can be
|
|
28
|
-
* published as wanting nothing.
|
|
29
|
-
*
|
|
30
|
-
* @type {Map<string, { shape: string, sourceKey: string, fileIndex: number, durationSeconds: number }>}
|
|
31
|
-
*/
|
|
32
|
-
#last = new Map();
|
|
33
|
-
|
|
34
|
-
/** The last map BUILT per film and file, for whoever reads instead of being
|
|
35
|
-
* handed it. @type {Map<string, import("./PriorityMap.js").PriorityMap>} */
|
|
36
|
-
#maps = new Map();
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The last map built per OUTPUT, from the viewers of that output alone.
|
|
40
|
-
*
|
|
41
|
-
* The same fact answered at two scopes, because the two things that act on it
|
|
42
|
-
* ask at two scopes and both are right. The swarm is asked for bytes of a
|
|
43
|
-
* FILE, and the picture, a quality step and a soundtrack of one film read the
|
|
44
|
-
* same bytes — so every viewer of any of them wants that file's bytes.
|
|
45
|
-
* Encoders are placed per OUTPUT, and a person watching 480p wants nothing of
|
|
46
|
-
* the 1080p output at all.
|
|
47
|
-
*
|
|
48
|
-
* One map for both was the second authority over encoders. Every output of a
|
|
49
|
-
* film was handed the whole film's map, so the plan wanted an encoder on every
|
|
50
|
-
* one of them; what actually stopped the ones nobody was watching was the
|
|
51
|
-
* session manager killing them by its own judgement — and since a viewer
|
|
52
|
-
* moving between steps also announces itself, the plan started them again on
|
|
53
|
-
* the next pass. Two parties answering "should this encoder exist" by different
|
|
54
|
-
* rules, several times a second.
|
|
55
|
-
*
|
|
56
|
-
* @type {Map<string, import("./PriorityMap.js").PriorityMap>}
|
|
57
|
-
*/
|
|
58
|
-
#byOutput = new Map();
|
|
59
|
-
|
|
60
|
-
/** The registry of this component that knows who watches each output. @type {{ forOutput: (output: object) => Map<string, object> }} */
|
|
61
|
-
#viewers;
|
|
62
|
-
|
|
63
|
-
/** How wide the first band of one session's file is. @type {(session: object) => number} */
|
|
64
|
-
#allowanceFor;
|
|
65
|
-
|
|
66
|
-
/** Whether this output is what that person is consuming, as opposed to one
|
|
67
|
-
* they merely hold a record on. @type {(session: object, viewer: object) => boolean} */
|
|
68
|
-
#watchedBy;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Who watches an output is this component's own registry, handed in as the
|
|
72
|
-
* registry. What it needs from elsewhere — how wide an interruption this file
|
|
73
|
-
* has shown on this swarm, and which output a person has on screen — is
|
|
74
|
-
* passed in as functions.
|
|
75
|
-
*
|
|
76
|
-
* @param {object} params
|
|
77
|
-
* @param {(published: { sourceKey: string, fileIndex: number, durationSeconds: number,
|
|
78
|
-
* zones: { from: number, to: number, priority: number }[] }) => void} params.publish
|
|
79
|
-
* @param {{ forOutput: (output: object) => Map<string, object> }} params.viewers - Required:
|
|
80
|
-
* without it every output would read as unwatched and every encoder would stop.
|
|
81
|
-
* @param {(session: object) => number} [params.allowanceFor]
|
|
82
|
-
* @param {(session: object, viewer: object) => boolean} [params.watchedBy] -
|
|
83
|
-
* Whether this output is the one that person is consuming. Which of a film's
|
|
84
|
-
* outputs a person has on screen is a fact about the film's shape, which
|
|
85
|
-
* this layer does not know; absent, every registered viewer counts, and then
|
|
86
|
-
* the per-output map says the same as the per-file one.
|
|
87
|
-
*/
|
|
88
|
-
constructor({ publish, viewers, allowanceFor, watchedBy }) {
|
|
89
|
-
if (typeof viewers?.forOutput !== "function") {
|
|
90
|
-
throw new TypeError("PriorityOrchestrator needs the viewer registry");
|
|
91
|
-
}
|
|
92
|
-
this.#publish = typeof publish === "function" ? publish : () => {};
|
|
93
|
-
this.#viewers = viewers;
|
|
94
|
-
this.#allowanceFor = typeof allowanceFor === "function" ? allowanceFor : () => 0;
|
|
95
|
-
this.#watchedBy = typeof watchedBy === "function" ? watchedBy : () => true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* A map from a set of viewers, and the ONE statement of how one is built.
|
|
100
|
-
*
|
|
101
|
-
* Asked at both scopes — once per film for the swarm, once per output for the
|
|
102
|
-
* encoders — and written once, because two copies of how a viewer's map is
|
|
103
|
-
* built is the same two-owners fault this class was split for.
|
|
104
|
-
*
|
|
105
|
-
* @param {object} params
|
|
106
|
-
* @param {number} params.durationSeconds
|
|
107
|
-
* @param {number} params.allowanceSeconds
|
|
108
|
-
* @param {{ atSeconds: number, playing: boolean }[]} params.viewers
|
|
109
|
-
* @returns {import("./PriorityMap.js").PriorityMap} A map of no length where
|
|
110
|
-
* nobody is watching or the film's length is unknown, which says the same as
|
|
111
|
-
* a map with nothing in it.
|
|
112
|
-
*/
|
|
113
|
-
#mapFrom({ durationSeconds, allowanceSeconds, viewers }) {
|
|
114
|
-
if (!(durationSeconds > 0) || !(viewers?.length > 0)) {
|
|
115
|
-
return emptyMap(0);
|
|
116
|
-
}
|
|
117
|
-
return mergeMaps(
|
|
118
|
-
viewers.map((viewer) =>
|
|
119
|
-
mapForViewer({
|
|
120
|
-
atSeconds: viewer.atSeconds,
|
|
121
|
-
durationSeconds,
|
|
122
|
-
allowanceSeconds,
|
|
123
|
-
playing: viewer.playing !== false
|
|
124
|
-
})
|
|
125
|
-
)
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* The map for one film, from everyone watching it.
|
|
131
|
-
*
|
|
132
|
-
* @param {object} params
|
|
133
|
-
* @param {string} params.sourceKey
|
|
134
|
-
* @param {number} params.fileIndex
|
|
135
|
-
* @param {number} params.durationSeconds
|
|
136
|
-
* @param {number} params.allowanceSeconds - The measured depth below which an
|
|
137
|
-
* interruption reaches a viewer of this file.
|
|
138
|
-
* @param {{ atSeconds: number, playing: boolean }[]} params.viewers
|
|
139
|
-
* @returns {import("./PriorityMap.js").PriorityMap} One number per second of
|
|
140
|
-
* film, merged over everyone watching it.
|
|
141
|
-
*/
|
|
142
|
-
build({ sourceKey, fileIndex, durationSeconds, allowanceSeconds, viewers }) {
|
|
143
|
-
const map = this.#mapFrom({ durationSeconds, allowanceSeconds, viewers });
|
|
144
|
-
const key = `${sourceKey}:${fileIndex}`;
|
|
145
|
-
this.#maps.set(key, map);
|
|
146
|
-
// Unchanged maps are not republished: the downloading rebuilds what it asks
|
|
147
|
-
// the swarm for on every one, and a viewer sitting still would otherwise
|
|
148
|
-
// make it do that several times a second. Compared as stretches rather than
|
|
149
|
-
// second by second, which is the same comparison over far fewer values.
|
|
150
|
-
const zones = runsOf(map);
|
|
151
|
-
const shape = JSON.stringify(zones);
|
|
152
|
-
if (this.#last.get(key)?.shape !== shape) {
|
|
153
|
-
// The length is remembered with the shape, so that a file whose viewers
|
|
154
|
-
// have all gone can still be spoken for: what is published then is the
|
|
155
|
-
// same statement with nothing in it, and it has to name the file it is
|
|
156
|
-
// about.
|
|
157
|
-
this.#last.set(key, { shape, sourceKey, fileIndex, durationSeconds });
|
|
158
|
-
this.#publish({ sourceKey, fileIndex, durationSeconds, zones });
|
|
159
|
-
}
|
|
160
|
-
return map;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* NOBODY WANTS ANYTHING OF THIS FILE ANY MORE — said, rather than left to be
|
|
165
|
-
* inferred from silence.
|
|
166
|
-
*
|
|
167
|
-
* The map used to be deleted from this class's memory and published nowhere,
|
|
168
|
-
* so what had been stated about that file on the swarm's behalf stood until
|
|
169
|
-
* the torrent itself was removed. Read from the register, a film nobody had
|
|
170
|
-
* watched for an hour was indistinguishable from one being watched now.
|
|
171
|
-
*
|
|
172
|
-
* @param {{ sourceKey: string, fileIndex: number, durationSeconds: number }} what
|
|
173
|
-
* @returns {void}
|
|
174
|
-
*/
|
|
175
|
-
#publishNothingFor({ sourceKey, fileIndex, durationSeconds }) {
|
|
176
|
-
this.#publish({ sourceKey, fileIndex, durationSeconds, zones: [] });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Build and publish the map for every file anybody is watching.
|
|
181
|
-
*
|
|
182
|
-
* One map per FILE, not per output: the picture, a quality step and a
|
|
183
|
-
* soundtrack of one film are three outputs reading the same bytes, and the
|
|
184
|
-
* swarm is asked for bytes. Viewers of all of them merge into one map.
|
|
185
|
-
*
|
|
186
|
-
* @param {object} params
|
|
187
|
-
* @param {Iterable<object[]>} params.sessionGroups - The live sessions, in
|
|
188
|
-
* whatever grouping the caller holds them; they are regrouped by file here.
|
|
189
|
-
* @param {number} [params.now]
|
|
190
|
-
* @returns {void}
|
|
191
|
-
*/
|
|
192
|
-
publishFor({ sessionGroups, now = Date.now() }) {
|
|
193
|
-
/** @type {Map<string, { sourceKey: string, fileIndex: number, durationSeconds: number, allowanceSeconds: number, viewers: object[] }>} */
|
|
194
|
-
const byFile = new Map();
|
|
195
|
-
/** @type {Map<string, { durationSeconds: number, allowanceSeconds: number, viewers: object[] }>} */
|
|
196
|
-
const byOutput = new Map();
|
|
197
|
-
for (const sessions of sessionGroups) {
|
|
198
|
-
for (const session of sessions) {
|
|
199
|
-
const key = `${session.sourceKey}:${session.fileIndex}`;
|
|
200
|
-
const durationSeconds = Number(session.file?.durationSeconds) || 0;
|
|
201
|
-
// The first band is as wide as an interruption this file has actually
|
|
202
|
-
// shown on this swarm, never a chosen number.
|
|
203
|
-
const allowanceSeconds = this.#allowanceFor(session);
|
|
204
|
-
let held = byFile.get(key);
|
|
205
|
-
if (!held) {
|
|
206
|
-
held = {
|
|
207
|
-
sourceKey: session.sourceKey,
|
|
208
|
-
fileIndex: session.fileIndex,
|
|
209
|
-
durationSeconds,
|
|
210
|
-
allowanceSeconds,
|
|
211
|
-
viewers: []
|
|
212
|
-
};
|
|
213
|
-
byFile.set(key, held);
|
|
214
|
-
}
|
|
215
|
-
// Every output anybody holds a session for, whether or not a viewer is
|
|
216
|
-
// consuming it — an output with nobody on it must get a map with
|
|
217
|
-
// nothing in it, which is how the plan is told to stop its encoders.
|
|
218
|
-
// Left out, it would keep the map it had when somebody was watching.
|
|
219
|
-
const address = session.outputKey ?? "";
|
|
220
|
-
let mine = byOutput.get(address);
|
|
221
|
-
if (!mine) {
|
|
222
|
-
mine = { durationSeconds, allowanceSeconds, viewers: [] };
|
|
223
|
-
byOutput.set(address, mine);
|
|
224
|
-
}
|
|
225
|
-
for (const viewer of this.#viewers.forOutput(session).values()) {
|
|
226
|
-
if (!viewer.isPresent()) {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
const stated = {
|
|
230
|
-
atSeconds: viewer.positionSeconds(now) ?? 0,
|
|
231
|
-
// WATCHING OR WAITING, not merely playing. Three states reach this
|
|
232
|
-
// one question: a viewer whose picture is advancing, a viewer
|
|
233
|
-
// blocked on material we owe them, and a viewer who stopped it
|
|
234
|
-
// themselves. The first two want the film in front of them NOW and
|
|
235
|
-
// share the upper band; only the third can wait. A page that is not
|
|
236
|
-
// on screen has its timers throttled and asks for nothing, which is
|
|
237
|
-
// indistinguishable from a full cushion, so it is not consuming
|
|
238
|
-
// either — told apart on the page, folded into one question here.
|
|
239
|
-
playing: typeof viewer.wantsFilmNow === "function"
|
|
240
|
-
? viewer.wantsFilmNow()
|
|
241
|
-
: viewer.playing === true || viewer.waiting === true
|
|
242
|
-
};
|
|
243
|
-
held.viewers.push(stated);
|
|
244
|
-
if (this.#watchedBy(session, viewer)) {
|
|
245
|
-
mine.viewers.push(stated);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// WHAT THIS PASS SAW IS ALL THERE IS. Everything below is derived from the
|
|
251
|
-
// live sessions, so a file or an output that is not among them is gone —
|
|
252
|
-
// and these maps are the projection of that, never a memory of it. Left to
|
|
253
|
-
// accumulate they were three maps that only grew, which is the shape of half
|
|
254
|
-
// the memory faults recorded in this repository, and `forget` was written
|
|
255
|
-
// for it and called from nowhere.
|
|
256
|
-
for (const key of [...this.#maps.keys()]) {
|
|
257
|
-
if (!byFile.has(key)) {
|
|
258
|
-
const last = this.#last.get(key);
|
|
259
|
-
this.#maps.delete(key);
|
|
260
|
-
this.#last.delete(key);
|
|
261
|
-
if (last) {
|
|
262
|
-
this.#publishNothingFor(last);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
for (const address of [...this.#byOutput.keys()]) {
|
|
267
|
-
if (!byOutput.has(address)) {
|
|
268
|
-
this.#byOutput.delete(address);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
for (const one of byFile.values()) {
|
|
272
|
-
// A file of unknown length cannot be divided into zones at all. A file
|
|
273
|
-
// whose viewers have all gone CAN be spoken for, and must be: `build`
|
|
274
|
-
// with nobody watching produces a map with nothing in it, which is the
|
|
275
|
-
// truth and is what withdraws what was stated for them. Skipped, as it
|
|
276
|
-
// was, the last thing said about that file stood for as long as the
|
|
277
|
-
// torrent did.
|
|
278
|
-
if (one.durationSeconds > 0) {
|
|
279
|
-
this.build(one);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
for (const [address, one] of byOutput) {
|
|
283
|
-
this.#byOutput.set(address, this.#mapFrom(one));
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Nobody is watching this file any more.
|
|
289
|
-
*
|
|
290
|
-
* @param {string} sourceKey
|
|
291
|
-
* @param {number} fileIndex
|
|
292
|
-
*/
|
|
293
|
-
/**
|
|
294
|
-
* The map this class last built for one file.
|
|
295
|
-
*
|
|
296
|
-
* Read by whoever acts on it and cannot be handed it at the moment it is
|
|
297
|
-
* made — the encoding decides per output, and one file has several. It is the
|
|
298
|
-
* SAME map: built once here, from where the viewers are, and neither read
|
|
299
|
-
* changes it.
|
|
300
|
-
*
|
|
301
|
-
* @param {string} sourceKey
|
|
302
|
-
* @param {number} fileIndex
|
|
303
|
-
* @returns {import("./PriorityMap.js").PriorityMap} A map of no length where
|
|
304
|
-
* none was built, which says the same as a map with nothing in it.
|
|
305
|
-
*/
|
|
306
|
-
mapFor(sourceKey, fileIndex) {
|
|
307
|
-
return this.#maps.get(`${sourceKey}:${fileIndex}`) ?? emptyMap(0);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* The map for ONE output, from the viewers consuming that output.
|
|
312
|
-
*
|
|
313
|
-
* What the encoding reads. A map of no length says nobody is on this output,
|
|
314
|
-
* which is what makes an encoder on it unwanted — and it is a statement, not
|
|
315
|
-
* an absence: the walk above writes one for every output a session exists
|
|
316
|
-
* for, including the ones everybody has left.
|
|
317
|
-
*
|
|
318
|
-
* @param {string} address
|
|
319
|
-
* @returns {import("./PriorityMap.js").PriorityMap}
|
|
320
|
-
*/
|
|
321
|
-
mapForOutput(address) {
|
|
322
|
-
return this.#byOutput.get(address) ?? emptyMap(0);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file The priority map, built once and handed to everybody who acts on it.
|
|
3
|
+
*
|
|
4
|
+
* One map per film, in seconds of film against a number. It is built from where
|
|
5
|
+
* the viewers are and nothing else, and both of the things that do work — the
|
|
6
|
+
* encoding and the downloading — read it and decide for themselves. They do not
|
|
7
|
+
* talk to each other, and neither of them tells this class anything.
|
|
8
|
+
*
|
|
9
|
+
* **Why it has to be published rather than asked for.** The downloading lives
|
|
10
|
+
* in another thread. Until now it took its orders from the reads themselves:
|
|
11
|
+
* every read declared a window around its own head, so fifteen reads declared
|
|
12
|
+
* fifteen windows on a piece store that holds sixteen pieces. Half of all
|
|
13
|
+
* evictions then took a piece a reader had said it wanted, two thirds of reads
|
|
14
|
+
* came back from disk, and what `/stream` handed out stopped being the file's
|
|
15
|
+
* bytes — twenty-two source-parse errors, a segment the player could not
|
|
16
|
+
* append, and an empty picture for six minutes (field 2026-09-05).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { emptyMap, mapForViewer, mergeMaps, runsOf } from "./PriorityMap.js";
|
|
20
|
+
|
|
21
|
+
export class PriorityOrchestrator {
|
|
22
|
+
/** Where the map goes once it is built. @type {(published: object) => void} */
|
|
23
|
+
#publish;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The last map published per film and file, so an unchanged one is not
|
|
27
|
+
* resent — and what it was about, so that a file everybody has left can be
|
|
28
|
+
* published as wanting nothing.
|
|
29
|
+
*
|
|
30
|
+
* @type {Map<string, { shape: string, sourceKey: string, fileIndex: number, durationSeconds: number }>}
|
|
31
|
+
*/
|
|
32
|
+
#last = new Map();
|
|
33
|
+
|
|
34
|
+
/** The last map BUILT per film and file, for whoever reads instead of being
|
|
35
|
+
* handed it. @type {Map<string, import("./PriorityMap.js").PriorityMap>} */
|
|
36
|
+
#maps = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The last map built per OUTPUT, from the viewers of that output alone.
|
|
40
|
+
*
|
|
41
|
+
* The same fact answered at two scopes, because the two things that act on it
|
|
42
|
+
* ask at two scopes and both are right. The swarm is asked for bytes of a
|
|
43
|
+
* FILE, and the picture, a quality step and a soundtrack of one film read the
|
|
44
|
+
* same bytes — so every viewer of any of them wants that file's bytes.
|
|
45
|
+
* Encoders are placed per OUTPUT, and a person watching 480p wants nothing of
|
|
46
|
+
* the 1080p output at all.
|
|
47
|
+
*
|
|
48
|
+
* One map for both was the second authority over encoders. Every output of a
|
|
49
|
+
* film was handed the whole film's map, so the plan wanted an encoder on every
|
|
50
|
+
* one of them; what actually stopped the ones nobody was watching was the
|
|
51
|
+
* session manager killing them by its own judgement — and since a viewer
|
|
52
|
+
* moving between steps also announces itself, the plan started them again on
|
|
53
|
+
* the next pass. Two parties answering "should this encoder exist" by different
|
|
54
|
+
* rules, several times a second.
|
|
55
|
+
*
|
|
56
|
+
* @type {Map<string, import("./PriorityMap.js").PriorityMap>}
|
|
57
|
+
*/
|
|
58
|
+
#byOutput = new Map();
|
|
59
|
+
|
|
60
|
+
/** The registry of this component that knows who watches each output. @type {{ forOutput: (output: object) => Map<string, object> }} */
|
|
61
|
+
#viewers;
|
|
62
|
+
|
|
63
|
+
/** How wide the first band of one session's file is. @type {(session: object) => number} */
|
|
64
|
+
#allowanceFor;
|
|
65
|
+
|
|
66
|
+
/** Whether this output is what that person is consuming, as opposed to one
|
|
67
|
+
* they merely hold a record on. @type {(session: object, viewer: object) => boolean} */
|
|
68
|
+
#watchedBy;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Who watches an output is this component's own registry, handed in as the
|
|
72
|
+
* registry. What it needs from elsewhere — how wide an interruption this file
|
|
73
|
+
* has shown on this swarm, and which output a person has on screen — is
|
|
74
|
+
* passed in as functions.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} params
|
|
77
|
+
* @param {(published: { sourceKey: string, fileIndex: number, durationSeconds: number,
|
|
78
|
+
* zones: { from: number, to: number, priority: number }[] }) => void} params.publish
|
|
79
|
+
* @param {{ forOutput: (output: object) => Map<string, object> }} params.viewers - Required:
|
|
80
|
+
* without it every output would read as unwatched and every encoder would stop.
|
|
81
|
+
* @param {(session: object) => number} [params.allowanceFor]
|
|
82
|
+
* @param {(session: object, viewer: object) => boolean} [params.watchedBy] -
|
|
83
|
+
* Whether this output is the one that person is consuming. Which of a film's
|
|
84
|
+
* outputs a person has on screen is a fact about the film's shape, which
|
|
85
|
+
* this layer does not know; absent, every registered viewer counts, and then
|
|
86
|
+
* the per-output map says the same as the per-file one.
|
|
87
|
+
*/
|
|
88
|
+
constructor({ publish, viewers, allowanceFor, watchedBy }) {
|
|
89
|
+
if (typeof viewers?.forOutput !== "function") {
|
|
90
|
+
throw new TypeError("PriorityOrchestrator needs the viewer registry");
|
|
91
|
+
}
|
|
92
|
+
this.#publish = typeof publish === "function" ? publish : () => {};
|
|
93
|
+
this.#viewers = viewers;
|
|
94
|
+
this.#allowanceFor = typeof allowanceFor === "function" ? allowanceFor : () => 0;
|
|
95
|
+
this.#watchedBy = typeof watchedBy === "function" ? watchedBy : () => true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A map from a set of viewers, and the ONE statement of how one is built.
|
|
100
|
+
*
|
|
101
|
+
* Asked at both scopes — once per film for the swarm, once per output for the
|
|
102
|
+
* encoders — and written once, because two copies of how a viewer's map is
|
|
103
|
+
* built is the same two-owners fault this class was split for.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} params
|
|
106
|
+
* @param {number} params.durationSeconds
|
|
107
|
+
* @param {number} params.allowanceSeconds
|
|
108
|
+
* @param {{ atSeconds: number, playing: boolean }[]} params.viewers
|
|
109
|
+
* @returns {import("./PriorityMap.js").PriorityMap} A map of no length where
|
|
110
|
+
* nobody is watching or the film's length is unknown, which says the same as
|
|
111
|
+
* a map with nothing in it.
|
|
112
|
+
*/
|
|
113
|
+
#mapFrom({ durationSeconds, allowanceSeconds, viewers }) {
|
|
114
|
+
if (!(durationSeconds > 0) || !(viewers?.length > 0)) {
|
|
115
|
+
return emptyMap(0);
|
|
116
|
+
}
|
|
117
|
+
return mergeMaps(
|
|
118
|
+
viewers.map((viewer) =>
|
|
119
|
+
mapForViewer({
|
|
120
|
+
atSeconds: viewer.atSeconds,
|
|
121
|
+
durationSeconds,
|
|
122
|
+
allowanceSeconds,
|
|
123
|
+
playing: viewer.playing !== false
|
|
124
|
+
})
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The map for one film, from everyone watching it.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} params
|
|
133
|
+
* @param {string} params.sourceKey
|
|
134
|
+
* @param {number} params.fileIndex
|
|
135
|
+
* @param {number} params.durationSeconds
|
|
136
|
+
* @param {number} params.allowanceSeconds - The measured depth below which an
|
|
137
|
+
* interruption reaches a viewer of this file.
|
|
138
|
+
* @param {{ atSeconds: number, playing: boolean }[]} params.viewers
|
|
139
|
+
* @returns {import("./PriorityMap.js").PriorityMap} One number per second of
|
|
140
|
+
* film, merged over everyone watching it.
|
|
141
|
+
*/
|
|
142
|
+
build({ sourceKey, fileIndex, durationSeconds, allowanceSeconds, viewers }) {
|
|
143
|
+
const map = this.#mapFrom({ durationSeconds, allowanceSeconds, viewers });
|
|
144
|
+
const key = `${sourceKey}:${fileIndex}`;
|
|
145
|
+
this.#maps.set(key, map);
|
|
146
|
+
// Unchanged maps are not republished: the downloading rebuilds what it asks
|
|
147
|
+
// the swarm for on every one, and a viewer sitting still would otherwise
|
|
148
|
+
// make it do that several times a second. Compared as stretches rather than
|
|
149
|
+
// second by second, which is the same comparison over far fewer values.
|
|
150
|
+
const zones = runsOf(map);
|
|
151
|
+
const shape = JSON.stringify(zones);
|
|
152
|
+
if (this.#last.get(key)?.shape !== shape) {
|
|
153
|
+
// The length is remembered with the shape, so that a file whose viewers
|
|
154
|
+
// have all gone can still be spoken for: what is published then is the
|
|
155
|
+
// same statement with nothing in it, and it has to name the file it is
|
|
156
|
+
// about.
|
|
157
|
+
this.#last.set(key, { shape, sourceKey, fileIndex, durationSeconds });
|
|
158
|
+
this.#publish({ sourceKey, fileIndex, durationSeconds, zones });
|
|
159
|
+
}
|
|
160
|
+
return map;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* NOBODY WANTS ANYTHING OF THIS FILE ANY MORE — said, rather than left to be
|
|
165
|
+
* inferred from silence.
|
|
166
|
+
*
|
|
167
|
+
* The map used to be deleted from this class's memory and published nowhere,
|
|
168
|
+
* so what had been stated about that file on the swarm's behalf stood until
|
|
169
|
+
* the torrent itself was removed. Read from the register, a film nobody had
|
|
170
|
+
* watched for an hour was indistinguishable from one being watched now.
|
|
171
|
+
*
|
|
172
|
+
* @param {{ sourceKey: string, fileIndex: number, durationSeconds: number }} what
|
|
173
|
+
* @returns {void}
|
|
174
|
+
*/
|
|
175
|
+
#publishNothingFor({ sourceKey, fileIndex, durationSeconds }) {
|
|
176
|
+
this.#publish({ sourceKey, fileIndex, durationSeconds, zones: [] });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build and publish the map for every file anybody is watching.
|
|
181
|
+
*
|
|
182
|
+
* One map per FILE, not per output: the picture, a quality step and a
|
|
183
|
+
* soundtrack of one film are three outputs reading the same bytes, and the
|
|
184
|
+
* swarm is asked for bytes. Viewers of all of them merge into one map.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} params
|
|
187
|
+
* @param {Iterable<object[]>} params.sessionGroups - The live sessions, in
|
|
188
|
+
* whatever grouping the caller holds them; they are regrouped by file here.
|
|
189
|
+
* @param {number} [params.now]
|
|
190
|
+
* @returns {void}
|
|
191
|
+
*/
|
|
192
|
+
publishFor({ sessionGroups, now = Date.now() }) {
|
|
193
|
+
/** @type {Map<string, { sourceKey: string, fileIndex: number, durationSeconds: number, allowanceSeconds: number, viewers: object[] }>} */
|
|
194
|
+
const byFile = new Map();
|
|
195
|
+
/** @type {Map<string, { durationSeconds: number, allowanceSeconds: number, viewers: object[] }>} */
|
|
196
|
+
const byOutput = new Map();
|
|
197
|
+
for (const sessions of sessionGroups) {
|
|
198
|
+
for (const session of sessions) {
|
|
199
|
+
const key = `${session.sourceKey}:${session.fileIndex}`;
|
|
200
|
+
const durationSeconds = Number(session.file?.durationSeconds) || 0;
|
|
201
|
+
// The first band is as wide as an interruption this file has actually
|
|
202
|
+
// shown on this swarm, never a chosen number.
|
|
203
|
+
const allowanceSeconds = this.#allowanceFor(session);
|
|
204
|
+
let held = byFile.get(key);
|
|
205
|
+
if (!held) {
|
|
206
|
+
held = {
|
|
207
|
+
sourceKey: session.sourceKey,
|
|
208
|
+
fileIndex: session.fileIndex,
|
|
209
|
+
durationSeconds,
|
|
210
|
+
allowanceSeconds,
|
|
211
|
+
viewers: []
|
|
212
|
+
};
|
|
213
|
+
byFile.set(key, held);
|
|
214
|
+
}
|
|
215
|
+
// Every output anybody holds a session for, whether or not a viewer is
|
|
216
|
+
// consuming it — an output with nobody on it must get a map with
|
|
217
|
+
// nothing in it, which is how the plan is told to stop its encoders.
|
|
218
|
+
// Left out, it would keep the map it had when somebody was watching.
|
|
219
|
+
const address = session.outputKey ?? "";
|
|
220
|
+
let mine = byOutput.get(address);
|
|
221
|
+
if (!mine) {
|
|
222
|
+
mine = { durationSeconds, allowanceSeconds, viewers: [] };
|
|
223
|
+
byOutput.set(address, mine);
|
|
224
|
+
}
|
|
225
|
+
for (const viewer of this.#viewers.forOutput(session).values()) {
|
|
226
|
+
if (!viewer.isPresent()) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const stated = {
|
|
230
|
+
atSeconds: viewer.positionSeconds(now) ?? 0,
|
|
231
|
+
// WATCHING OR WAITING, not merely playing. Three states reach this
|
|
232
|
+
// one question: a viewer whose picture is advancing, a viewer
|
|
233
|
+
// blocked on material we owe them, and a viewer who stopped it
|
|
234
|
+
// themselves. The first two want the film in front of them NOW and
|
|
235
|
+
// share the upper band; only the third can wait. A page that is not
|
|
236
|
+
// on screen has its timers throttled and asks for nothing, which is
|
|
237
|
+
// indistinguishable from a full cushion, so it is not consuming
|
|
238
|
+
// either — told apart on the page, folded into one question here.
|
|
239
|
+
playing: typeof viewer.wantsFilmNow === "function"
|
|
240
|
+
? viewer.wantsFilmNow()
|
|
241
|
+
: viewer.playing === true || viewer.waiting === true
|
|
242
|
+
};
|
|
243
|
+
held.viewers.push(stated);
|
|
244
|
+
if (this.#watchedBy(session, viewer)) {
|
|
245
|
+
mine.viewers.push(stated);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// WHAT THIS PASS SAW IS ALL THERE IS. Everything below is derived from the
|
|
251
|
+
// live sessions, so a file or an output that is not among them is gone —
|
|
252
|
+
// and these maps are the projection of that, never a memory of it. Left to
|
|
253
|
+
// accumulate they were three maps that only grew, which is the shape of half
|
|
254
|
+
// the memory faults recorded in this repository, and `forget` was written
|
|
255
|
+
// for it and called from nowhere.
|
|
256
|
+
for (const key of [...this.#maps.keys()]) {
|
|
257
|
+
if (!byFile.has(key)) {
|
|
258
|
+
const last = this.#last.get(key);
|
|
259
|
+
this.#maps.delete(key);
|
|
260
|
+
this.#last.delete(key);
|
|
261
|
+
if (last) {
|
|
262
|
+
this.#publishNothingFor(last);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const address of [...this.#byOutput.keys()]) {
|
|
267
|
+
if (!byOutput.has(address)) {
|
|
268
|
+
this.#byOutput.delete(address);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const one of byFile.values()) {
|
|
272
|
+
// A file of unknown length cannot be divided into zones at all. A file
|
|
273
|
+
// whose viewers have all gone CAN be spoken for, and must be: `build`
|
|
274
|
+
// with nobody watching produces a map with nothing in it, which is the
|
|
275
|
+
// truth and is what withdraws what was stated for them. Skipped, as it
|
|
276
|
+
// was, the last thing said about that file stood for as long as the
|
|
277
|
+
// torrent did.
|
|
278
|
+
if (one.durationSeconds > 0) {
|
|
279
|
+
this.build(one);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const [address, one] of byOutput) {
|
|
283
|
+
this.#byOutput.set(address, this.#mapFrom(one));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Nobody is watching this file any more.
|
|
289
|
+
*
|
|
290
|
+
* @param {string} sourceKey
|
|
291
|
+
* @param {number} fileIndex
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* The map this class last built for one file.
|
|
295
|
+
*
|
|
296
|
+
* Read by whoever acts on it and cannot be handed it at the moment it is
|
|
297
|
+
* made — the encoding decides per output, and one file has several. It is the
|
|
298
|
+
* SAME map: built once here, from where the viewers are, and neither read
|
|
299
|
+
* changes it.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} sourceKey
|
|
302
|
+
* @param {number} fileIndex
|
|
303
|
+
* @returns {import("./PriorityMap.js").PriorityMap} A map of no length where
|
|
304
|
+
* none was built, which says the same as a map with nothing in it.
|
|
305
|
+
*/
|
|
306
|
+
mapFor(sourceKey, fileIndex) {
|
|
307
|
+
return this.#maps.get(`${sourceKey}:${fileIndex}`) ?? emptyMap(0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The map for ONE output, from the viewers consuming that output.
|
|
312
|
+
*
|
|
313
|
+
* What the encoding reads. A map of no length says nobody is on this output,
|
|
314
|
+
* which is what makes an encoder on it unwanted — and it is a statement, not
|
|
315
|
+
* an absence: the walk above writes one for every output a session exists
|
|
316
|
+
* for, including the ones everybody has left.
|
|
317
|
+
*
|
|
318
|
+
* @param {string} address
|
|
319
|
+
* @returns {import("./PriorityMap.js").PriorityMap}
|
|
320
|
+
*/
|
|
321
|
+
mapForOutput(address) {
|
|
322
|
+
return this.#byOutput.get(address) ?? emptyMap(0);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
}
|