@torrent-tv/proxy 2.87.7 → 2.87.8
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 +4 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/data-channel-handler.js +21 -8
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -0
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The piece store behind a torrent, reached through what it does.
|
|
3
|
-
*
|
|
4
|
-
* WebTorrent wraps the store it was given in stores of its own, each holding
|
|
5
|
-
* the next one as `store`, so the store the torrent component talks to is found
|
|
6
|
-
* by walking that chain. It is recognised by the interface this component uses
|
|
7
|
-
* — which pieces the readers have asked to keep (`protectedRanges`) — and not by
|
|
8
|
-
* its class: the torrent component does not import the storage component's
|
|
9
|
-
* implementation, it is handed it where the thread is assembled
|
|
10
|
-
* (`worker/worker.js`).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** How deep WebTorrent's own wrappers are allowed to go before the walk gives up. */
|
|
14
|
-
const WRAPPER_DEPTH = 8;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {{ store?: object } | null | undefined} torrent
|
|
18
|
-
* @returns {object | null}
|
|
19
|
-
*/
|
|
20
|
-
export function pieceStoreOf(torrent) {
|
|
21
|
-
let candidate = torrent?.store;
|
|
22
|
-
for (let depth = 0; candidate && depth < WRAPPER_DEPTH; depth += 1) {
|
|
23
|
-
if (typeof candidate.protectedRanges === "function") {
|
|
24
|
-
return candidate;
|
|
25
|
-
}
|
|
26
|
-
candidate = candidate.store;
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file The piece store behind a torrent, reached through what it does.
|
|
3
|
+
*
|
|
4
|
+
* WebTorrent wraps the store it was given in stores of its own, each holding
|
|
5
|
+
* the next one as `store`, so the store the torrent component talks to is found
|
|
6
|
+
* by walking that chain. It is recognised by the interface this component uses
|
|
7
|
+
* — which pieces the readers have asked to keep (`protectedRanges`) — and not by
|
|
8
|
+
* its class: the torrent component does not import the storage component's
|
|
9
|
+
* implementation, it is handed it where the thread is assembled
|
|
10
|
+
* (`worker/worker.js`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** How deep WebTorrent's own wrappers are allowed to go before the walk gives up. */
|
|
14
|
+
const WRAPPER_DEPTH = 8;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ store?: object } | null | undefined} torrent
|
|
18
|
+
* @returns {object | null}
|
|
19
|
+
*/
|
|
20
|
+
export function pieceStoreOf(torrent) {
|
|
21
|
+
let candidate = torrent?.store;
|
|
22
|
+
for (let depth = 0; candidate && depth < WRAPPER_DEPTH; depth += 1) {
|
|
23
|
+
if (typeof candidate.protectedRanges === "function") {
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
candidate = candidate.store;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -1,262 +1,262 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What speed a step must run at, and how much buffer a viewer needs —
|
|
3
|
-
* both derived from the supply's own interruptions rather than chosen by hand.
|
|
4
|
-
*
|
|
5
|
-
* The two numbers this replaces were guesses. The speed margin was 1.5, and on
|
|
6
|
-
* the field session of 2026-08-17 a step admitted by it ran at 1.05x and
|
|
7
|
-
* stalled; the pre-buffer target was 25 s, which is sixteen seconds of waiting
|
|
8
|
-
* before the picture starts that nobody had shown to be necessary.
|
|
9
|
-
*
|
|
10
|
-
* Both follow from the same two measured quantities, and from nothing else:
|
|
11
|
-
*
|
|
12
|
-
* W — how long the supply is INTERRUPTED (the worst recent stall);
|
|
13
|
-
* T — how long the encoder RUNS between two stalls (the median recent gap).
|
|
14
|
-
*
|
|
15
|
-
* Both words are load-bearing, and getting either wrong is what produced a
|
|
16
|
-
* demanded speed of 4422x on 2026-08-31.
|
|
17
|
-
*
|
|
18
|
-
* A stall is not a wait. Several readers walk one file — the picture and each
|
|
19
|
-
* audio rendition — so a piece that has not arrived blocks all of them, and
|
|
20
|
-
* their waits end within milliseconds of each other. Counted as separate
|
|
21
|
-
* interruptions they gave an interval of 0.00 s. Waits are therefore merged into
|
|
22
|
-
* the stretches during which the supply was not delivering, however many readers
|
|
23
|
-
* noticed.
|
|
24
|
-
*
|
|
25
|
-
* And T is the RUNNING time, from the end of one stall to the start of the next
|
|
26
|
-
* — not the spacing between their ends, which includes a stall's own duration
|
|
27
|
-
* and so credits the encoder with cushion it was not building.
|
|
28
|
-
*
|
|
29
|
-
* **The margin.** A step producing at speed `v` gains `v - 1` seconds of
|
|
30
|
-
* cushion for every second it runs, and an interruption of `W` seconds costs
|
|
31
|
-
* `W`. It therefore survives its own supply only if what it gains between
|
|
32
|
-
* interruptions exceeds what one costs:
|
|
33
|
-
*
|
|
34
|
-
* (v - 1) × T > W i.e. v > 1 + W / T
|
|
35
|
-
*
|
|
36
|
-
* On the field torrent: stalls of 1.49 s with 0.73 s of running between them,
|
|
37
|
-
* the worst 3.16 s, so the honest bar is 5.33 — against the 1.5 that was
|
|
38
|
-
* assumed, and the 1.05 that was measured. (An earlier reading of the same data
|
|
39
|
-
* gave 2.42 by using the end-to-end spacing; it was too low in the same
|
|
40
|
-
* direction as the guess it replaced.) The same arithmetic explains why a copied
|
|
41
|
-
* stream never stalls: at 8x it gains 10.7 s between stalls and loses at most
|
|
42
|
-
* 4.8 s.
|
|
43
|
-
*
|
|
44
|
-
* **The buffer.** It must cover the worst interruption that can arrive before
|
|
45
|
-
* it can be refilled, whichever source that interruption comes from, plus the
|
|
46
|
-
* segment being played — which must be whole:
|
|
47
|
-
*
|
|
48
|
-
* B = segment duration + max(W_supply, D_production, T_transfer)
|
|
49
|
-
*
|
|
50
|
-
* On the same session that is 7-9 s rather than 25. It rises by itself when a
|
|
51
|
-
* session's interruptions grow, which is what makes it safe to lower: the
|
|
52
|
-
* figure is continuously measured, not chosen once.
|
|
53
|
-
*
|
|
54
|
-
* Every quantity here is measured. Nothing in this file is a coefficient, and
|
|
55
|
-
* nothing smooths, weights or decays anything.
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* One measured interruption: how long a read waited, and when the wait ended.
|
|
60
|
-
*
|
|
61
|
-
* @typedef {object} SupplyWait
|
|
62
|
-
* @property {number} waitedMs - How long the read was blocked.
|
|
63
|
-
* @property {number} at - Wall-clock ms when the wait ended.
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The speed a step must sustain to survive this file's supply on this swarm.
|
|
68
|
-
*
|
|
69
|
-
* Returns null when the evidence does not exist yet — fewer than two waits
|
|
70
|
-
* means no interval has been observed, and an interval invented from one point
|
|
71
|
-
* would be exactly the kind of number this file exists to remove. A caller with
|
|
72
|
-
* null must say it does not know, never substitute a default.
|
|
73
|
-
*
|
|
74
|
-
* @param {SupplyWait[]} waits - Recent waits, in any order.
|
|
75
|
-
* @returns {{ requiredSpeed: number, worstWaitSec: number, medianIntervalSec: number, samples: number } | null}
|
|
76
|
-
*/
|
|
77
|
-
export function requiredSpeedFrom(waits) {
|
|
78
|
-
const ordered = usableWaits(waits);
|
|
79
|
-
if (ordered.length < 2) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
// One INTERRUPTION, not one wait. Several readers walk the same file — the
|
|
83
|
-
// picture and each audio rendition — and a piece that has not arrived blocks
|
|
84
|
-
// all of them at once. Counted as separate interruptions, those simultaneous
|
|
85
|
-
// waits gave a near-zero interval and therefore a required speed of thousands:
|
|
86
|
-
// measured 2026-08-31, `worst wait 13.26s, one every 0.00s, 2 measured` became
|
|
87
|
-
// 4422.00x, and every quality step was refused against it.
|
|
88
|
-
const interruptions = mergeOverlapping(ordered);
|
|
89
|
-
if (interruptions.length < 2) {
|
|
90
|
-
// One interruption shows no interval, and an interval invented from one
|
|
91
|
-
// point is exactly what this file exists to remove.
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
const worstWaitSec = Math.max(...interruptions.map((one) => one.end - one.start)) / 1000;
|
|
95
|
-
const intervals = [];
|
|
96
|
-
for (let index = 1; index < interruptions.length; index += 1) {
|
|
97
|
-
// From the END of one interruption to the START of the next: that is the
|
|
98
|
-
// stretch the encoder actually runs for and builds cushion in. Measuring
|
|
99
|
-
// end-to-end instead counted each interruption's own duration as part of
|
|
100
|
-
// the recovery it is supposed to be recovered from.
|
|
101
|
-
const gapMs = interruptions[index].start - interruptions[index - 1].end;
|
|
102
|
-
if (gapMs > 0) {
|
|
103
|
-
intervals.push(gapMs / 1000);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (intervals.length === 0) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
const medianIntervalSec = median(intervals);
|
|
110
|
-
if (!(medianIntervalSec > 0)) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
// THE SHARE OF ITS TIME THE READING LOST, over the stretch actually observed.
|
|
114
|
-
//
|
|
115
|
-
// The model is unchanged and it was always right: if a fraction `f` of the
|
|
116
|
-
// time is spent not delivering, then producing one second of film takes
|
|
117
|
-
// `1/(1 - f)` seconds, so a step must run that much faster than realtime.
|
|
118
|
-
// What was wrong was the two quantities fed into it — the WORST single
|
|
119
|
-
// interruption divided by the MEDIAN gap between interruptions, a maximum
|
|
120
|
-
// over a median, from two populations that need not be the same events at
|
|
121
|
-
// all. It asks what would happen if the worst interruption recurred at the
|
|
122
|
-
// typical rate, which is a compound case that never occurs, and it divides by
|
|
123
|
-
// a gap that goes to zero whenever interruptions arrive in a burst.
|
|
124
|
-
//
|
|
125
|
-
// Field 2026-09-08: 0.79 s (one jump, on a file already downloaded whole)
|
|
126
|
-
// over 0.01 s (the gaps inside a burst of microsecond waits) gave 158.60x,
|
|
127
|
-
// and the quality budget refused every step against it forty times in one
|
|
128
|
-
// session. The same measurements as a share of time lost give 1.00x, because
|
|
129
|
-
// that is what a reading of a complete file loses.
|
|
130
|
-
//
|
|
131
|
-
// The denominator here cannot vanish: it is the span the interruptions are
|
|
132
|
-
// spread over, which contains them.
|
|
133
|
-
// OVER WHOLE CYCLES, from the first interruption's start to the last one's
|
|
134
|
-
// start. That window holds exactly one running stretch per interruption in
|
|
135
|
-
// it, so the share does not depend on where the window happens to be cut —
|
|
136
|
-
// measured to the last interruption's END it counts one interruption more
|
|
137
|
-
// than it counts running stretches, and the same supply then reads 1.54x or
|
|
138
|
-
// 1.45x according to nothing but the moment the log line was printed.
|
|
139
|
-
const last = interruptions.length - 1;
|
|
140
|
-
const spanSec = (interruptions[last].start - interruptions[0].start) / 1000;
|
|
141
|
-
const lostSec = interruptions
|
|
142
|
-
.slice(0, last)
|
|
143
|
-
.reduce((total, one) => total + (one.end - one.start), 0) / 1000;
|
|
144
|
-
// A span that is all interruption says the supply delivered nothing at all
|
|
145
|
-
// while it was watched. There is no speed that survives that, and saying a
|
|
146
|
-
// huge number is less honest than saying it is not a speed question — so the
|
|
147
|
-
// largest figure any step is ever compared against is stated and named.
|
|
148
|
-
const lostShare = spanSec > 0 ? Math.min(0.99, lostSec / spanSec) : 0;
|
|
149
|
-
return {
|
|
150
|
-
requiredSpeed: 1 / (1 - lostShare),
|
|
151
|
-
lostShare,
|
|
152
|
-
spanSec,
|
|
153
|
-
lostSec,
|
|
154
|
-
worstWaitSec,
|
|
155
|
-
medianIntervalSec,
|
|
156
|
-
// Interruptions, not waits: what the figure is derived from. The two differ
|
|
157
|
-
// whenever more than one reader walks the file, and reporting the raw count
|
|
158
|
-
// is what made the 4422x line look better evidenced than it was — "24
|
|
159
|
-
// measured" was 24 waits over far fewer actual stalls.
|
|
160
|
-
samples: interruptions.length,
|
|
161
|
-
waits: ordered.length
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Waits joined into the interruptions they actually were.
|
|
167
|
-
*
|
|
168
|
-
* A wait spans `[at - waitedMs, at]`. Two that overlap or touch are one stretch
|
|
169
|
-
* during which the supply was not delivering, however many readers noticed it.
|
|
170
|
-
*
|
|
171
|
-
* @param {SupplyWait[]} ordered - Usable waits, oldest END first.
|
|
172
|
-
* @returns {Array<{ start: number, end: number }>} Disjoint, in time order.
|
|
173
|
-
*/
|
|
174
|
-
function mergeOverlapping(ordered) {
|
|
175
|
-
const spans = ordered
|
|
176
|
-
.map((wait) => ({ start: wait.at - wait.waitedMs, end: wait.at }))
|
|
177
|
-
.sort((left, right) => left.start - right.start);
|
|
178
|
-
/** @type {Array<{ start: number, end: number }>} */
|
|
179
|
-
const merged = [];
|
|
180
|
-
for (const span of spans) {
|
|
181
|
-
const last = merged[merged.length - 1];
|
|
182
|
-
if (last && span.start <= last.end) {
|
|
183
|
-
last.end = Math.max(last.end, span.end);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
merged.push({ ...span });
|
|
187
|
-
}
|
|
188
|
-
return merged;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* The smallest buffer at which no interruption reaches the viewer.
|
|
193
|
-
*
|
|
194
|
-
* Each term is the worst OBSERVED value over a recent window, and a term with
|
|
195
|
-
* nothing observed contributes nothing rather than a guess. The segment is
|
|
196
|
-
* always included: the one being played has to be whole.
|
|
197
|
-
*
|
|
198
|
-
* @param {object} observed
|
|
199
|
-
* @param {number} observed.segmentSeconds - The session's segment duration.
|
|
200
|
-
* @param {number} [observed.worstSupplyWaitSec] - Longest wait for a piece.
|
|
201
|
-
* @param {number} [observed.worstProductionGapSec] - Longest gap between
|
|
202
|
-
* consecutive segments beyond their own length: what a step at 1.0x costs.
|
|
203
|
-
* @param {number} [observed.worstTransferSec] - Longest time to move one
|
|
204
|
-
* segment over the channel to the viewer.
|
|
205
|
-
* @returns {{ seconds: number, from: string } | null} Null when the segment
|
|
206
|
-
* duration is unknown, since then nothing here can be stated.
|
|
207
|
-
*/
|
|
208
|
-
export function minimumBufferFrom(observed = {}) {
|
|
209
|
-
const segmentSeconds = Number(observed.segmentSeconds);
|
|
210
|
-
if (!Number.isFinite(segmentSeconds) || segmentSeconds <= 0) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
const terms = [
|
|
214
|
-
{ name: "supply", seconds: positiveOrZero(observed.worstSupplyWaitSec) },
|
|
215
|
-
{ name: "production", seconds: positiveOrZero(observed.worstProductionGapSec) },
|
|
216
|
-
{ name: "transfer", seconds: positiveOrZero(observed.worstTransferSec) }
|
|
217
|
-
];
|
|
218
|
-
const worst = terms.reduce(
|
|
219
|
-
(largest, term) => (term.seconds > largest.seconds ? term : largest),
|
|
220
|
-
{ name: "none", seconds: 0 }
|
|
221
|
-
);
|
|
222
|
-
return {
|
|
223
|
-
seconds: segmentSeconds + worst.seconds,
|
|
224
|
-
// Which interruption sets the figure, so a session's log says what the
|
|
225
|
-
// viewer is actually waiting for rather than only how long.
|
|
226
|
-
from: worst.name
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Waits that can be reasoned about, oldest first.
|
|
232
|
-
*
|
|
233
|
-
* @param {SupplyWait[]} waits
|
|
234
|
-
* @returns {SupplyWait[]}
|
|
235
|
-
*/
|
|
236
|
-
function usableWaits(waits) {
|
|
237
|
-
if (!Array.isArray(waits)) {
|
|
238
|
-
return [];
|
|
239
|
-
}
|
|
240
|
-
return waits
|
|
241
|
-
.filter((wait) => Number.isFinite(wait?.waitedMs) && wait.waitedMs > 0 && Number.isFinite(wait?.at))
|
|
242
|
-
.sort((left, right) => left.at - right.at);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* @param {number[]} values - Non-empty.
|
|
247
|
-
* @returns {number}
|
|
248
|
-
*/
|
|
249
|
-
function median(values) {
|
|
250
|
-
const sorted = [...values].sort((left, right) => left - right);
|
|
251
|
-
const middle = Math.floor(sorted.length / 2);
|
|
252
|
-
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* @param {unknown} value
|
|
257
|
-
* @returns {number}
|
|
258
|
-
*/
|
|
259
|
-
function positiveOrZero(value) {
|
|
260
|
-
const numeric = Number(value);
|
|
261
|
-
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
|
262
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file What speed a step must run at, and how much buffer a viewer needs —
|
|
3
|
+
* both derived from the supply's own interruptions rather than chosen by hand.
|
|
4
|
+
*
|
|
5
|
+
* The two numbers this replaces were guesses. The speed margin was 1.5, and on
|
|
6
|
+
* the field session of 2026-08-17 a step admitted by it ran at 1.05x and
|
|
7
|
+
* stalled; the pre-buffer target was 25 s, which is sixteen seconds of waiting
|
|
8
|
+
* before the picture starts that nobody had shown to be necessary.
|
|
9
|
+
*
|
|
10
|
+
* Both follow from the same two measured quantities, and from nothing else:
|
|
11
|
+
*
|
|
12
|
+
* W — how long the supply is INTERRUPTED (the worst recent stall);
|
|
13
|
+
* T — how long the encoder RUNS between two stalls (the median recent gap).
|
|
14
|
+
*
|
|
15
|
+
* Both words are load-bearing, and getting either wrong is what produced a
|
|
16
|
+
* demanded speed of 4422x on 2026-08-31.
|
|
17
|
+
*
|
|
18
|
+
* A stall is not a wait. Several readers walk one file — the picture and each
|
|
19
|
+
* audio rendition — so a piece that has not arrived blocks all of them, and
|
|
20
|
+
* their waits end within milliseconds of each other. Counted as separate
|
|
21
|
+
* interruptions they gave an interval of 0.00 s. Waits are therefore merged into
|
|
22
|
+
* the stretches during which the supply was not delivering, however many readers
|
|
23
|
+
* noticed.
|
|
24
|
+
*
|
|
25
|
+
* And T is the RUNNING time, from the end of one stall to the start of the next
|
|
26
|
+
* — not the spacing between their ends, which includes a stall's own duration
|
|
27
|
+
* and so credits the encoder with cushion it was not building.
|
|
28
|
+
*
|
|
29
|
+
* **The margin.** A step producing at speed `v` gains `v - 1` seconds of
|
|
30
|
+
* cushion for every second it runs, and an interruption of `W` seconds costs
|
|
31
|
+
* `W`. It therefore survives its own supply only if what it gains between
|
|
32
|
+
* interruptions exceeds what one costs:
|
|
33
|
+
*
|
|
34
|
+
* (v - 1) × T > W i.e. v > 1 + W / T
|
|
35
|
+
*
|
|
36
|
+
* On the field torrent: stalls of 1.49 s with 0.73 s of running between them,
|
|
37
|
+
* the worst 3.16 s, so the honest bar is 5.33 — against the 1.5 that was
|
|
38
|
+
* assumed, and the 1.05 that was measured. (An earlier reading of the same data
|
|
39
|
+
* gave 2.42 by using the end-to-end spacing; it was too low in the same
|
|
40
|
+
* direction as the guess it replaced.) The same arithmetic explains why a copied
|
|
41
|
+
* stream never stalls: at 8x it gains 10.7 s between stalls and loses at most
|
|
42
|
+
* 4.8 s.
|
|
43
|
+
*
|
|
44
|
+
* **The buffer.** It must cover the worst interruption that can arrive before
|
|
45
|
+
* it can be refilled, whichever source that interruption comes from, plus the
|
|
46
|
+
* segment being played — which must be whole:
|
|
47
|
+
*
|
|
48
|
+
* B = segment duration + max(W_supply, D_production, T_transfer)
|
|
49
|
+
*
|
|
50
|
+
* On the same session that is 7-9 s rather than 25. It rises by itself when a
|
|
51
|
+
* session's interruptions grow, which is what makes it safe to lower: the
|
|
52
|
+
* figure is continuously measured, not chosen once.
|
|
53
|
+
*
|
|
54
|
+
* Every quantity here is measured. Nothing in this file is a coefficient, and
|
|
55
|
+
* nothing smooths, weights or decays anything.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One measured interruption: how long a read waited, and when the wait ended.
|
|
60
|
+
*
|
|
61
|
+
* @typedef {object} SupplyWait
|
|
62
|
+
* @property {number} waitedMs - How long the read was blocked.
|
|
63
|
+
* @property {number} at - Wall-clock ms when the wait ended.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The speed a step must sustain to survive this file's supply on this swarm.
|
|
68
|
+
*
|
|
69
|
+
* Returns null when the evidence does not exist yet — fewer than two waits
|
|
70
|
+
* means no interval has been observed, and an interval invented from one point
|
|
71
|
+
* would be exactly the kind of number this file exists to remove. A caller with
|
|
72
|
+
* null must say it does not know, never substitute a default.
|
|
73
|
+
*
|
|
74
|
+
* @param {SupplyWait[]} waits - Recent waits, in any order.
|
|
75
|
+
* @returns {{ requiredSpeed: number, worstWaitSec: number, medianIntervalSec: number, samples: number } | null}
|
|
76
|
+
*/
|
|
77
|
+
export function requiredSpeedFrom(waits) {
|
|
78
|
+
const ordered = usableWaits(waits);
|
|
79
|
+
if (ordered.length < 2) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// One INTERRUPTION, not one wait. Several readers walk the same file — the
|
|
83
|
+
// picture and each audio rendition — and a piece that has not arrived blocks
|
|
84
|
+
// all of them at once. Counted as separate interruptions, those simultaneous
|
|
85
|
+
// waits gave a near-zero interval and therefore a required speed of thousands:
|
|
86
|
+
// measured 2026-08-31, `worst wait 13.26s, one every 0.00s, 2 measured` became
|
|
87
|
+
// 4422.00x, and every quality step was refused against it.
|
|
88
|
+
const interruptions = mergeOverlapping(ordered);
|
|
89
|
+
if (interruptions.length < 2) {
|
|
90
|
+
// One interruption shows no interval, and an interval invented from one
|
|
91
|
+
// point is exactly what this file exists to remove.
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const worstWaitSec = Math.max(...interruptions.map((one) => one.end - one.start)) / 1000;
|
|
95
|
+
const intervals = [];
|
|
96
|
+
for (let index = 1; index < interruptions.length; index += 1) {
|
|
97
|
+
// From the END of one interruption to the START of the next: that is the
|
|
98
|
+
// stretch the encoder actually runs for and builds cushion in. Measuring
|
|
99
|
+
// end-to-end instead counted each interruption's own duration as part of
|
|
100
|
+
// the recovery it is supposed to be recovered from.
|
|
101
|
+
const gapMs = interruptions[index].start - interruptions[index - 1].end;
|
|
102
|
+
if (gapMs > 0) {
|
|
103
|
+
intervals.push(gapMs / 1000);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (intervals.length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const medianIntervalSec = median(intervals);
|
|
110
|
+
if (!(medianIntervalSec > 0)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// THE SHARE OF ITS TIME THE READING LOST, over the stretch actually observed.
|
|
114
|
+
//
|
|
115
|
+
// The model is unchanged and it was always right: if a fraction `f` of the
|
|
116
|
+
// time is spent not delivering, then producing one second of film takes
|
|
117
|
+
// `1/(1 - f)` seconds, so a step must run that much faster than realtime.
|
|
118
|
+
// What was wrong was the two quantities fed into it — the WORST single
|
|
119
|
+
// interruption divided by the MEDIAN gap between interruptions, a maximum
|
|
120
|
+
// over a median, from two populations that need not be the same events at
|
|
121
|
+
// all. It asks what would happen if the worst interruption recurred at the
|
|
122
|
+
// typical rate, which is a compound case that never occurs, and it divides by
|
|
123
|
+
// a gap that goes to zero whenever interruptions arrive in a burst.
|
|
124
|
+
//
|
|
125
|
+
// Field 2026-09-08: 0.79 s (one jump, on a file already downloaded whole)
|
|
126
|
+
// over 0.01 s (the gaps inside a burst of microsecond waits) gave 158.60x,
|
|
127
|
+
// and the quality budget refused every step against it forty times in one
|
|
128
|
+
// session. The same measurements as a share of time lost give 1.00x, because
|
|
129
|
+
// that is what a reading of a complete file loses.
|
|
130
|
+
//
|
|
131
|
+
// The denominator here cannot vanish: it is the span the interruptions are
|
|
132
|
+
// spread over, which contains them.
|
|
133
|
+
// OVER WHOLE CYCLES, from the first interruption's start to the last one's
|
|
134
|
+
// start. That window holds exactly one running stretch per interruption in
|
|
135
|
+
// it, so the share does not depend on where the window happens to be cut —
|
|
136
|
+
// measured to the last interruption's END it counts one interruption more
|
|
137
|
+
// than it counts running stretches, and the same supply then reads 1.54x or
|
|
138
|
+
// 1.45x according to nothing but the moment the log line was printed.
|
|
139
|
+
const last = interruptions.length - 1;
|
|
140
|
+
const spanSec = (interruptions[last].start - interruptions[0].start) / 1000;
|
|
141
|
+
const lostSec = interruptions
|
|
142
|
+
.slice(0, last)
|
|
143
|
+
.reduce((total, one) => total + (one.end - one.start), 0) / 1000;
|
|
144
|
+
// A span that is all interruption says the supply delivered nothing at all
|
|
145
|
+
// while it was watched. There is no speed that survives that, and saying a
|
|
146
|
+
// huge number is less honest than saying it is not a speed question — so the
|
|
147
|
+
// largest figure any step is ever compared against is stated and named.
|
|
148
|
+
const lostShare = spanSec > 0 ? Math.min(0.99, lostSec / spanSec) : 0;
|
|
149
|
+
return {
|
|
150
|
+
requiredSpeed: 1 / (1 - lostShare),
|
|
151
|
+
lostShare,
|
|
152
|
+
spanSec,
|
|
153
|
+
lostSec,
|
|
154
|
+
worstWaitSec,
|
|
155
|
+
medianIntervalSec,
|
|
156
|
+
// Interruptions, not waits: what the figure is derived from. The two differ
|
|
157
|
+
// whenever more than one reader walks the file, and reporting the raw count
|
|
158
|
+
// is what made the 4422x line look better evidenced than it was — "24
|
|
159
|
+
// measured" was 24 waits over far fewer actual stalls.
|
|
160
|
+
samples: interruptions.length,
|
|
161
|
+
waits: ordered.length
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Waits joined into the interruptions they actually were.
|
|
167
|
+
*
|
|
168
|
+
* A wait spans `[at - waitedMs, at]`. Two that overlap or touch are one stretch
|
|
169
|
+
* during which the supply was not delivering, however many readers noticed it.
|
|
170
|
+
*
|
|
171
|
+
* @param {SupplyWait[]} ordered - Usable waits, oldest END first.
|
|
172
|
+
* @returns {Array<{ start: number, end: number }>} Disjoint, in time order.
|
|
173
|
+
*/
|
|
174
|
+
function mergeOverlapping(ordered) {
|
|
175
|
+
const spans = ordered
|
|
176
|
+
.map((wait) => ({ start: wait.at - wait.waitedMs, end: wait.at }))
|
|
177
|
+
.sort((left, right) => left.start - right.start);
|
|
178
|
+
/** @type {Array<{ start: number, end: number }>} */
|
|
179
|
+
const merged = [];
|
|
180
|
+
for (const span of spans) {
|
|
181
|
+
const last = merged[merged.length - 1];
|
|
182
|
+
if (last && span.start <= last.end) {
|
|
183
|
+
last.end = Math.max(last.end, span.end);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
merged.push({ ...span });
|
|
187
|
+
}
|
|
188
|
+
return merged;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The smallest buffer at which no interruption reaches the viewer.
|
|
193
|
+
*
|
|
194
|
+
* Each term is the worst OBSERVED value over a recent window, and a term with
|
|
195
|
+
* nothing observed contributes nothing rather than a guess. The segment is
|
|
196
|
+
* always included: the one being played has to be whole.
|
|
197
|
+
*
|
|
198
|
+
* @param {object} observed
|
|
199
|
+
* @param {number} observed.segmentSeconds - The session's segment duration.
|
|
200
|
+
* @param {number} [observed.worstSupplyWaitSec] - Longest wait for a piece.
|
|
201
|
+
* @param {number} [observed.worstProductionGapSec] - Longest gap between
|
|
202
|
+
* consecutive segments beyond their own length: what a step at 1.0x costs.
|
|
203
|
+
* @param {number} [observed.worstTransferSec] - Longest time to move one
|
|
204
|
+
* segment over the channel to the viewer.
|
|
205
|
+
* @returns {{ seconds: number, from: string } | null} Null when the segment
|
|
206
|
+
* duration is unknown, since then nothing here can be stated.
|
|
207
|
+
*/
|
|
208
|
+
export function minimumBufferFrom(observed = {}) {
|
|
209
|
+
const segmentSeconds = Number(observed.segmentSeconds);
|
|
210
|
+
if (!Number.isFinite(segmentSeconds) || segmentSeconds <= 0) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const terms = [
|
|
214
|
+
{ name: "supply", seconds: positiveOrZero(observed.worstSupplyWaitSec) },
|
|
215
|
+
{ name: "production", seconds: positiveOrZero(observed.worstProductionGapSec) },
|
|
216
|
+
{ name: "transfer", seconds: positiveOrZero(observed.worstTransferSec) }
|
|
217
|
+
];
|
|
218
|
+
const worst = terms.reduce(
|
|
219
|
+
(largest, term) => (term.seconds > largest.seconds ? term : largest),
|
|
220
|
+
{ name: "none", seconds: 0 }
|
|
221
|
+
);
|
|
222
|
+
return {
|
|
223
|
+
seconds: segmentSeconds + worst.seconds,
|
|
224
|
+
// Which interruption sets the figure, so a session's log says what the
|
|
225
|
+
// viewer is actually waiting for rather than only how long.
|
|
226
|
+
from: worst.name
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Waits that can be reasoned about, oldest first.
|
|
232
|
+
*
|
|
233
|
+
* @param {SupplyWait[]} waits
|
|
234
|
+
* @returns {SupplyWait[]}
|
|
235
|
+
*/
|
|
236
|
+
function usableWaits(waits) {
|
|
237
|
+
if (!Array.isArray(waits)) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
return waits
|
|
241
|
+
.filter((wait) => Number.isFinite(wait?.waitedMs) && wait.waitedMs > 0 && Number.isFinite(wait?.at))
|
|
242
|
+
.sort((left, right) => left.at - right.at);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {number[]} values - Non-empty.
|
|
247
|
+
* @returns {number}
|
|
248
|
+
*/
|
|
249
|
+
function median(values) {
|
|
250
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
251
|
+
const middle = Math.floor(sorted.length / 2);
|
|
252
|
+
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {unknown} value
|
|
257
|
+
* @returns {number}
|
|
258
|
+
*/
|
|
259
|
+
function positiveOrZero(value) {
|
|
260
|
+
const numeric = Number(value);
|
|
261
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
|
262
|
+
}
|