@torrent-tv/proxy 2.87.9 → 2.87.11
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 +2186 -2177
- 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/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 +136 -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 +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 +1110 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -928
- 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 +703 -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/playback-readiness.js +620 -0
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +989 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +675 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/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,359 +1,359 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What this proxy has seen its own admitted encodes do, kept between
|
|
3
|
-
* runs and used only on the configuration it was seen on (roadmap item 97,
|
|
4
|
-
* step 14).
|
|
5
|
-
*
|
|
6
|
-
* NOT A TABLE OF QUALITY, and not a condition for anything. The proxy selects,
|
|
7
|
-
* admits and serves with nothing kept here at all: the startup calibration is
|
|
8
|
-
* what qualifies a mode and prices it, and the admission is what decides
|
|
9
|
-
* whether there is room. What is kept here makes three of those figures more
|
|
10
|
-
* exact on THIS machine once it has done the work for real:
|
|
11
|
-
*
|
|
12
|
-
* 1. how fast an encode of this mode, at this size, actually ran alone on this
|
|
13
|
-
* kind of source — used in place of the startup prediction when an output
|
|
14
|
-
* of that mode is priced;
|
|
15
|
-
* 2. what the segments of an output of this mode and rate control actually
|
|
16
|
-
* carried, average and peak — used as the peak of an encoder that states no
|
|
17
|
-
* bound of its own (a hardware encoder has no `maxrate`), which otherwise
|
|
18
|
-
* has no figure at all;
|
|
19
|
-
* 3. how long an output of this mode took to have the piece a viewer needed
|
|
20
|
-
* after it was asked for, and what that viewer held when they moved — used
|
|
21
|
-
* as the time to ready when deciding whether a draining buffer is a threat.
|
|
22
|
-
*
|
|
23
|
-
* WHAT IS NEVER KEPT: a picture, a clip, a segment, anything about a viewer
|
|
24
|
-
* beyond the seconds they held, and anything that leaves this machine.
|
|
25
|
-
*
|
|
26
|
-
* WHAT DECIDES WHETHER A FIGURE APPLIES is the configuration it was seen on:
|
|
27
|
-
* every key starts with the configuration key of the encoder that made it
|
|
28
|
-
* (`fingerprint.js`). A figure seen on another ffmpeg, another libx264, another
|
|
29
|
-
* processor or thread count, another device or driver matches nothing and is
|
|
30
|
-
* dropped when the file is read — not converted, not scaled.
|
|
31
|
-
*
|
|
32
|
-
* AND WHETHER IT IS COMPARABLE. A speed is used only for a source of the same
|
|
33
|
-
* codec, bit depth and frame, whose bitrate is no higher than the one it was
|
|
34
|
-
* seen on — a slower reading on harder material is safe to apply to easier
|
|
35
|
-
* material, the reverse is not — and it is the SLOWEST such reading that is
|
|
36
|
-
* used. The largest peak and the longest preparation are used, for the same
|
|
37
|
-
* reason: each errs toward the viewer.
|
|
38
|
-
*
|
|
39
|
-
* Written when something is noted — a run ending, a move completing — which
|
|
40
|
-
* is rare, so a process that is killed loses at most what it was doing.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
44
|
-
import { configurationKeyOf, kindOfEncoderName } from "./fingerprint.js";
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* How many readings are kept per key. A history window, not a figure about the
|
|
48
|
-
* machine: long enough that one run cannot decide, short enough that the
|
|
49
|
-
* figure follows the host as it changes. The same length the host timings keep
|
|
50
|
-
* (`quality/HostTimings.js`).
|
|
51
|
-
*/
|
|
52
|
-
const READINGS_KEPT = 20;
|
|
53
|
-
|
|
54
|
-
/** The version of the file's shape; a file of another shape is not read. */
|
|
55
|
-
const FILE_VERSION = 1;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @typedef {object} ContentDescriptor
|
|
59
|
-
* @property {string} codec
|
|
60
|
-
* @property {number | null} bitDepth
|
|
61
|
-
* @property {number} width
|
|
62
|
-
* @property {number} height
|
|
63
|
-
* @property {number} megabitsPerSecond
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
export class LocalObservations {
|
|
67
|
-
/** @type {import("./fingerprint.js").HostFingerprint} */
|
|
68
|
-
#fingerprint;
|
|
69
|
-
|
|
70
|
-
/** @type {string} */
|
|
71
|
-
#filePath;
|
|
72
|
-
|
|
73
|
-
/** @type {{ info: (m: string) => void, warn: (m: string) => void }} */
|
|
74
|
-
#logger;
|
|
75
|
-
|
|
76
|
-
/** @type {() => number} */
|
|
77
|
-
#now;
|
|
78
|
-
|
|
79
|
-
/** @type {{ speed: Record<string, object[]>, rates: Record<string, object[]>, preparation: Record<string, object[]> }} */
|
|
80
|
-
#entries = { speed: {}, rates: {}, preparation: {} };
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @param {object} params
|
|
84
|
-
* @param {import("./fingerprint.js").HostFingerprint} params.fingerprint
|
|
85
|
-
* @param {string} params.filePath - Where the readings are kept; empty keeps
|
|
86
|
-
* them for this process only.
|
|
87
|
-
* @param {{ info: (m: string) => void, warn: (m: string) => void }} [params.logger]
|
|
88
|
-
* @param {() => number} [params.now]
|
|
89
|
-
*/
|
|
90
|
-
constructor({ fingerprint, filePath, logger = null, now = Date.now }) {
|
|
91
|
-
if (!fingerprint || typeof fingerprint !== "object") {
|
|
92
|
-
throw new TypeError("LocalObservations requires the host's fingerprint");
|
|
93
|
-
}
|
|
94
|
-
this.#fingerprint = fingerprint;
|
|
95
|
-
this.#filePath = typeof filePath === "string" ? filePath : "";
|
|
96
|
-
this.#logger = logger ?? { info: () => {}, warn: () => {} };
|
|
97
|
-
this.#now = now;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* The key of the MODE an output is encoded in: the configuration, the
|
|
102
|
-
* encoder, the frame and rate, the speed setting and whether it tone-maps.
|
|
103
|
-
* Null for an output that re-encodes no picture.
|
|
104
|
-
*
|
|
105
|
-
* @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
|
|
106
|
-
* @returns {string | null}
|
|
107
|
-
*/
|
|
108
|
-
modeKeyOf(spec) {
|
|
109
|
-
const encode = spec?.video?.encode;
|
|
110
|
-
if (!encode) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
const configuration = configurationKeyOf(this.#fingerprint, {
|
|
114
|
-
name: encode.encoder,
|
|
115
|
-
kind: kindOfEncoderName(encode.encoder)
|
|
116
|
-
});
|
|
117
|
-
return `${configuration}|${encode.encoder}/${encode.width}x${encode.height}@${encode.fps}/` +
|
|
118
|
-
`${encode.preset ?? "-"}/${encode.tonemap ? "tonemap" : "none"}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* The mode's key with its rate control, which is what bounds what its
|
|
123
|
-
* segments carry.
|
|
124
|
-
*
|
|
125
|
-
* @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
|
|
126
|
-
* @returns {string | null}
|
|
127
|
-
*/
|
|
128
|
-
rateKeyOf(spec) {
|
|
129
|
-
const mode = this.modeKeyOf(spec);
|
|
130
|
-
if (mode === null) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
const rate = spec.video.encode.rateControl;
|
|
134
|
-
return `${mode}/vbv=${rate ? `${rate.maxrateKbps}k-${rate.bufsizeKbps}k@${rate.level ?? "-"}` : "-"}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Read what an earlier run kept, dropping whatever no current configuration
|
|
139
|
-
* matches.
|
|
140
|
-
*
|
|
141
|
-
* @param {string[]} currentConfigurations - The configuration keys of every
|
|
142
|
-
* encoder this process may use.
|
|
143
|
-
* @returns {void}
|
|
144
|
-
*/
|
|
145
|
-
load(currentConfigurations) {
|
|
146
|
-
if (!this.#filePath) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
let raw;
|
|
150
|
-
try {
|
|
151
|
-
raw = JSON.parse(readFileSync(this.#filePath, "utf8"));
|
|
152
|
-
} catch {
|
|
153
|
-
return; // no file yet, or unreadable: nothing is known, which is the base case
|
|
154
|
-
}
|
|
155
|
-
if (raw?.version !== FILE_VERSION || !raw.entries || typeof raw.entries !== "object") {
|
|
156
|
-
this.#logger.info(`local observations: ${this.#filePath} is of another shape; starting afresh`);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
const allowed = new Set(currentConfigurations);
|
|
160
|
-
let kept = 0;
|
|
161
|
-
let dropped = 0;
|
|
162
|
-
for (const kind of ["speed", "rates", "preparation"]) {
|
|
163
|
-
const section = raw.entries[kind] && typeof raw.entries[kind] === "object" ? raw.entries[kind] : {};
|
|
164
|
-
for (const [key, readings] of Object.entries(section)) {
|
|
165
|
-
const configuration = key.split("|")[0];
|
|
166
|
-
if (!allowed.has(configuration) || !Array.isArray(readings)) {
|
|
167
|
-
dropped += 1;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
this.#entries[kind][key] = readings.slice(-READINGS_KEPT);
|
|
171
|
-
kept += 1;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
this.#logger.info(
|
|
175
|
-
`local observations: ${kept} key(s) kept from ${this.#filePath}` +
|
|
176
|
-
(dropped > 0 ? `, ${dropped} dropped because they were seen on a configuration this host no longer has` : "")
|
|
177
|
-
);
|
|
178
|
-
if (dropped > 0) {
|
|
179
|
-
this.#save();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* An encode of this output has ended: keep how fast it ran alone and what
|
|
185
|
-
* its segments carried.
|
|
186
|
-
*
|
|
187
|
-
* @param {object} params
|
|
188
|
-
* @param {import("./output/OutputSpec.js").OutputSpec} params.spec
|
|
189
|
-
* @param {ContentDescriptor | null} params.content - What it was encoded from.
|
|
190
|
-
* @param {number | null} params.aloneSpeedX - Seconds of film per second,
|
|
191
|
-
* measured with nothing else encoding.
|
|
192
|
-
* @param {{ averageKbps: number, peakKbps: number } | null} params.segmentKbps
|
|
193
|
-
* @returns {void}
|
|
194
|
-
*/
|
|
195
|
-
noteEncode({ spec, content, aloneSpeedX, segmentKbps }) {
|
|
196
|
-
const modeKey = this.modeKeyOf(spec);
|
|
197
|
-
if (modeKey === null) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
let changed = false;
|
|
201
|
-
if (Number.isFinite(aloneSpeedX) && aloneSpeedX > 0 && LocalObservations.#usableContent(content)) {
|
|
202
|
-
this.#push("speed", modeKey, { content, speedX: aloneSpeedX, at: this.#now() });
|
|
203
|
-
changed = true;
|
|
204
|
-
}
|
|
205
|
-
const rateKey = this.rateKeyOf(spec);
|
|
206
|
-
if (
|
|
207
|
-
segmentKbps &&
|
|
208
|
-
Number.isFinite(segmentKbps.averageKbps) && segmentKbps.averageKbps > 0 &&
|
|
209
|
-
Number.isFinite(segmentKbps.peakKbps) && segmentKbps.peakKbps > 0
|
|
210
|
-
) {
|
|
211
|
-
this.#push("rates", rateKey, {
|
|
212
|
-
averageKbps: segmentKbps.averageKbps,
|
|
213
|
-
peakKbps: segmentKbps.peakKbps,
|
|
214
|
-
at: this.#now()
|
|
215
|
-
});
|
|
216
|
-
changed = true;
|
|
217
|
-
}
|
|
218
|
-
if (changed) {
|
|
219
|
-
this.#save();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* A viewer has been moved onto an output that was being prepared for them:
|
|
225
|
-
* keep how long it took and what they held when they moved.
|
|
226
|
-
*
|
|
227
|
-
* @param {object} params
|
|
228
|
-
* @param {import("./output/OutputSpec.js").OutputSpec} params.spec - The output moved onto.
|
|
229
|
-
* @param {number} params.seconds - From the preparation starting to the move.
|
|
230
|
-
* @param {number | null} params.bufferedSec - What the viewer held at the move.
|
|
231
|
-
* @returns {void}
|
|
232
|
-
*/
|
|
233
|
-
notePreparation({ spec, seconds, bufferedSec }) {
|
|
234
|
-
const modeKey = this.modeKeyOf(spec);
|
|
235
|
-
if (modeKey === null || !(Number.isFinite(seconds) && seconds >= 0)) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
this.#push("preparation", modeKey, {
|
|
239
|
-
seconds,
|
|
240
|
-
bufferedSec: Number.isFinite(bufferedSec) ? bufferedSec : null,
|
|
241
|
-
at: this.#now()
|
|
242
|
-
});
|
|
243
|
-
this.#save();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* The slowest this mode has been seen running alone on comparable material,
|
|
248
|
-
* or null where nothing comparable has been seen.
|
|
249
|
-
*
|
|
250
|
-
* @param {import("./output/OutputSpec.js").OutputSpec} spec
|
|
251
|
-
* @param {ContentDescriptor | null} content
|
|
252
|
-
* @returns {number | null}
|
|
253
|
-
*/
|
|
254
|
-
slowestAloneSpeed(spec, content) {
|
|
255
|
-
const modeKey = this.modeKeyOf(spec);
|
|
256
|
-
if (modeKey === null || !LocalObservations.#usableContent(content)) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
let slowest = null;
|
|
260
|
-
for (const reading of this.#entries.speed[modeKey] ?? []) {
|
|
261
|
-
const seen = reading.content;
|
|
262
|
-
if (
|
|
263
|
-
seen?.codec === content.codec &&
|
|
264
|
-
(seen?.bitDepth ?? null) === (content.bitDepth ?? null) &&
|
|
265
|
-
seen?.width === content.width &&
|
|
266
|
-
seen?.height === content.height &&
|
|
267
|
-
seen?.megabitsPerSecond >= content.megabitsPerSecond
|
|
268
|
-
) {
|
|
269
|
-
slowest = slowest === null ? reading.speedX : Math.min(slowest, reading.speedX);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return slowest;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* The largest peak an output of this mode and rate control has been seen
|
|
277
|
-
* carrying, in megabits per second, or null.
|
|
278
|
-
*
|
|
279
|
-
* @param {import("./output/OutputSpec.js").OutputSpec} spec
|
|
280
|
-
* @returns {number | null}
|
|
281
|
-
*/
|
|
282
|
-
peakMbps(spec) {
|
|
283
|
-
const rateKey = this.rateKeyOf(spec);
|
|
284
|
-
const readings = rateKey === null ? [] : (this.#entries.rates[rateKey] ?? []);
|
|
285
|
-
return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.peakKbps)) / 1000;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* The longest an output of this mode has been seen taking to be ready for a
|
|
290
|
-
* viewer moving onto it, in seconds, or null.
|
|
291
|
-
*
|
|
292
|
-
* @param {import("./output/OutputSpec.js").OutputSpec} spec
|
|
293
|
-
* @returns {number | null}
|
|
294
|
-
*/
|
|
295
|
-
longestPreparationSec(spec) {
|
|
296
|
-
const modeKey = this.modeKeyOf(spec);
|
|
297
|
-
const readings = modeKey === null ? [] : (this.#entries.preparation[modeKey] ?? []);
|
|
298
|
-
return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.seconds));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* @param {"speed" | "rates" | "preparation"} kind
|
|
303
|
-
* @param {string} key
|
|
304
|
-
* @param {object} reading
|
|
305
|
-
* @returns {void}
|
|
306
|
-
*/
|
|
307
|
-
#push(kind, key, reading) {
|
|
308
|
-
const readings = [...(this.#entries[kind][key] ?? []), reading].slice(-READINGS_KEPT);
|
|
309
|
-
this.#entries[kind][key] = readings;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** @returns {void} */
|
|
313
|
-
#save() {
|
|
314
|
-
if (!this.#filePath) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
try {
|
|
318
|
-
writeFileSync(this.#filePath, JSON.stringify({ version: FILE_VERSION, entries: this.#entries }));
|
|
319
|
-
} catch (error) {
|
|
320
|
-
// A read-only install keeps what it learned for this process only.
|
|
321
|
-
this.#logger.warn(
|
|
322
|
-
`local observations: could not write ${this.#filePath} (${error instanceof Error ? error.message : String(error)})`
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* @param {ContentDescriptor | null} content
|
|
329
|
-
* @returns {boolean}
|
|
330
|
-
*/
|
|
331
|
-
static #usableContent(content) {
|
|
332
|
-
return Boolean(content) &&
|
|
333
|
-
typeof content.codec === "string" && content.codec.length > 0 &&
|
|
334
|
-
content.width > 0 && content.height > 0 &&
|
|
335
|
-
Number.isFinite(content.megabitsPerSecond) && content.megabitsPerSecond > 0;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* What an observation of an encode is filed as having been made FROM.
|
|
341
|
-
*
|
|
342
|
-
* @param {{ width: number | null, height: number | null, decode: { codec: string, bitDepth: number | null, megabitsPerSecond: number } | null } | null} file
|
|
343
|
-
* @returns {ContentDescriptor | null}
|
|
344
|
-
*/
|
|
345
|
-
export function contentOf(file) {
|
|
346
|
-
const decode = file?.decode ?? null;
|
|
347
|
-
const width = Number(file?.width) || 0;
|
|
348
|
-
const height = Number(file?.height) || 0;
|
|
349
|
-
if (!decode || width <= 0 || height <= 0) {
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
return {
|
|
353
|
-
codec: decode.codec,
|
|
354
|
-
bitDepth: decode.bitDepth ?? null,
|
|
355
|
-
width,
|
|
356
|
-
height,
|
|
357
|
-
megabitsPerSecond: decode.megabitsPerSecond
|
|
358
|
-
};
|
|
359
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file What this proxy has seen its own admitted encodes do, kept between
|
|
3
|
+
* runs and used only on the configuration it was seen on (roadmap item 97,
|
|
4
|
+
* step 14).
|
|
5
|
+
*
|
|
6
|
+
* NOT A TABLE OF QUALITY, and not a condition for anything. The proxy selects,
|
|
7
|
+
* admits and serves with nothing kept here at all: the startup calibration is
|
|
8
|
+
* what qualifies a mode and prices it, and the admission is what decides
|
|
9
|
+
* whether there is room. What is kept here makes three of those figures more
|
|
10
|
+
* exact on THIS machine once it has done the work for real:
|
|
11
|
+
*
|
|
12
|
+
* 1. how fast an encode of this mode, at this size, actually ran alone on this
|
|
13
|
+
* kind of source — used in place of the startup prediction when an output
|
|
14
|
+
* of that mode is priced;
|
|
15
|
+
* 2. what the segments of an output of this mode and rate control actually
|
|
16
|
+
* carried, average and peak — used as the peak of an encoder that states no
|
|
17
|
+
* bound of its own (a hardware encoder has no `maxrate`), which otherwise
|
|
18
|
+
* has no figure at all;
|
|
19
|
+
* 3. how long an output of this mode took to have the piece a viewer needed
|
|
20
|
+
* after it was asked for, and what that viewer held when they moved — used
|
|
21
|
+
* as the time to ready when deciding whether a draining buffer is a threat.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IS NEVER KEPT: a picture, a clip, a segment, anything about a viewer
|
|
24
|
+
* beyond the seconds they held, and anything that leaves this machine.
|
|
25
|
+
*
|
|
26
|
+
* WHAT DECIDES WHETHER A FIGURE APPLIES is the configuration it was seen on:
|
|
27
|
+
* every key starts with the configuration key of the encoder that made it
|
|
28
|
+
* (`fingerprint.js`). A figure seen on another ffmpeg, another libx264, another
|
|
29
|
+
* processor or thread count, another device or driver matches nothing and is
|
|
30
|
+
* dropped when the file is read — not converted, not scaled.
|
|
31
|
+
*
|
|
32
|
+
* AND WHETHER IT IS COMPARABLE. A speed is used only for a source of the same
|
|
33
|
+
* codec, bit depth and frame, whose bitrate is no higher than the one it was
|
|
34
|
+
* seen on — a slower reading on harder material is safe to apply to easier
|
|
35
|
+
* material, the reverse is not — and it is the SLOWEST such reading that is
|
|
36
|
+
* used. The largest peak and the longest preparation are used, for the same
|
|
37
|
+
* reason: each errs toward the viewer.
|
|
38
|
+
*
|
|
39
|
+
* Written when something is noted — a run ending, a move completing — which
|
|
40
|
+
* is rare, so a process that is killed loses at most what it was doing.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
44
|
+
import { configurationKeyOf, kindOfEncoderName } from "./fingerprint.js";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How many readings are kept per key. A history window, not a figure about the
|
|
48
|
+
* machine: long enough that one run cannot decide, short enough that the
|
|
49
|
+
* figure follows the host as it changes. The same length the host timings keep
|
|
50
|
+
* (`quality/HostTimings.js`).
|
|
51
|
+
*/
|
|
52
|
+
const READINGS_KEPT = 20;
|
|
53
|
+
|
|
54
|
+
/** The version of the file's shape; a file of another shape is not read. */
|
|
55
|
+
const FILE_VERSION = 1;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {object} ContentDescriptor
|
|
59
|
+
* @property {string} codec
|
|
60
|
+
* @property {number | null} bitDepth
|
|
61
|
+
* @property {number} width
|
|
62
|
+
* @property {number} height
|
|
63
|
+
* @property {number} megabitsPerSecond
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
export class LocalObservations {
|
|
67
|
+
/** @type {import("./fingerprint.js").HostFingerprint} */
|
|
68
|
+
#fingerprint;
|
|
69
|
+
|
|
70
|
+
/** @type {string} */
|
|
71
|
+
#filePath;
|
|
72
|
+
|
|
73
|
+
/** @type {{ info: (m: string) => void, warn: (m: string) => void }} */
|
|
74
|
+
#logger;
|
|
75
|
+
|
|
76
|
+
/** @type {() => number} */
|
|
77
|
+
#now;
|
|
78
|
+
|
|
79
|
+
/** @type {{ speed: Record<string, object[]>, rates: Record<string, object[]>, preparation: Record<string, object[]> }} */
|
|
80
|
+
#entries = { speed: {}, rates: {}, preparation: {} };
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {object} params
|
|
84
|
+
* @param {import("./fingerprint.js").HostFingerprint} params.fingerprint
|
|
85
|
+
* @param {string} params.filePath - Where the readings are kept; empty keeps
|
|
86
|
+
* them for this process only.
|
|
87
|
+
* @param {{ info: (m: string) => void, warn: (m: string) => void }} [params.logger]
|
|
88
|
+
* @param {() => number} [params.now]
|
|
89
|
+
*/
|
|
90
|
+
constructor({ fingerprint, filePath, logger = null, now = Date.now }) {
|
|
91
|
+
if (!fingerprint || typeof fingerprint !== "object") {
|
|
92
|
+
throw new TypeError("LocalObservations requires the host's fingerprint");
|
|
93
|
+
}
|
|
94
|
+
this.#fingerprint = fingerprint;
|
|
95
|
+
this.#filePath = typeof filePath === "string" ? filePath : "";
|
|
96
|
+
this.#logger = logger ?? { info: () => {}, warn: () => {} };
|
|
97
|
+
this.#now = now;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The key of the MODE an output is encoded in: the configuration, the
|
|
102
|
+
* encoder, the frame and rate, the speed setting and whether it tone-maps.
|
|
103
|
+
* Null for an output that re-encodes no picture.
|
|
104
|
+
*
|
|
105
|
+
* @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
|
|
106
|
+
* @returns {string | null}
|
|
107
|
+
*/
|
|
108
|
+
modeKeyOf(spec) {
|
|
109
|
+
const encode = spec?.video?.encode;
|
|
110
|
+
if (!encode) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const configuration = configurationKeyOf(this.#fingerprint, {
|
|
114
|
+
name: encode.encoder,
|
|
115
|
+
kind: kindOfEncoderName(encode.encoder)
|
|
116
|
+
});
|
|
117
|
+
return `${configuration}|${encode.encoder}/${encode.width}x${encode.height}@${encode.fps}/` +
|
|
118
|
+
`${encode.preset ?? "-"}/${encode.tonemap ? "tonemap" : "none"}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The mode's key with its rate control, which is what bounds what its
|
|
123
|
+
* segments carry.
|
|
124
|
+
*
|
|
125
|
+
* @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
|
|
126
|
+
* @returns {string | null}
|
|
127
|
+
*/
|
|
128
|
+
rateKeyOf(spec) {
|
|
129
|
+
const mode = this.modeKeyOf(spec);
|
|
130
|
+
if (mode === null) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const rate = spec.video.encode.rateControl;
|
|
134
|
+
return `${mode}/vbv=${rate ? `${rate.maxrateKbps}k-${rate.bufsizeKbps}k@${rate.level ?? "-"}` : "-"}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Read what an earlier run kept, dropping whatever no current configuration
|
|
139
|
+
* matches.
|
|
140
|
+
*
|
|
141
|
+
* @param {string[]} currentConfigurations - The configuration keys of every
|
|
142
|
+
* encoder this process may use.
|
|
143
|
+
* @returns {void}
|
|
144
|
+
*/
|
|
145
|
+
load(currentConfigurations) {
|
|
146
|
+
if (!this.#filePath) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
let raw;
|
|
150
|
+
try {
|
|
151
|
+
raw = JSON.parse(readFileSync(this.#filePath, "utf8"));
|
|
152
|
+
} catch {
|
|
153
|
+
return; // no file yet, or unreadable: nothing is known, which is the base case
|
|
154
|
+
}
|
|
155
|
+
if (raw?.version !== FILE_VERSION || !raw.entries || typeof raw.entries !== "object") {
|
|
156
|
+
this.#logger.info(`local observations: ${this.#filePath} is of another shape; starting afresh`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const allowed = new Set(currentConfigurations);
|
|
160
|
+
let kept = 0;
|
|
161
|
+
let dropped = 0;
|
|
162
|
+
for (const kind of ["speed", "rates", "preparation"]) {
|
|
163
|
+
const section = raw.entries[kind] && typeof raw.entries[kind] === "object" ? raw.entries[kind] : {};
|
|
164
|
+
for (const [key, readings] of Object.entries(section)) {
|
|
165
|
+
const configuration = key.split("|")[0];
|
|
166
|
+
if (!allowed.has(configuration) || !Array.isArray(readings)) {
|
|
167
|
+
dropped += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
this.#entries[kind][key] = readings.slice(-READINGS_KEPT);
|
|
171
|
+
kept += 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.#logger.info(
|
|
175
|
+
`local observations: ${kept} key(s) kept from ${this.#filePath}` +
|
|
176
|
+
(dropped > 0 ? `, ${dropped} dropped because they were seen on a configuration this host no longer has` : "")
|
|
177
|
+
);
|
|
178
|
+
if (dropped > 0) {
|
|
179
|
+
this.#save();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* An encode of this output has ended: keep how fast it ran alone and what
|
|
185
|
+
* its segments carried.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} params
|
|
188
|
+
* @param {import("./output/OutputSpec.js").OutputSpec} params.spec
|
|
189
|
+
* @param {ContentDescriptor | null} params.content - What it was encoded from.
|
|
190
|
+
* @param {number | null} params.aloneSpeedX - Seconds of film per second,
|
|
191
|
+
* measured with nothing else encoding.
|
|
192
|
+
* @param {{ averageKbps: number, peakKbps: number } | null} params.segmentKbps
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
noteEncode({ spec, content, aloneSpeedX, segmentKbps }) {
|
|
196
|
+
const modeKey = this.modeKeyOf(spec);
|
|
197
|
+
if (modeKey === null) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let changed = false;
|
|
201
|
+
if (Number.isFinite(aloneSpeedX) && aloneSpeedX > 0 && LocalObservations.#usableContent(content)) {
|
|
202
|
+
this.#push("speed", modeKey, { content, speedX: aloneSpeedX, at: this.#now() });
|
|
203
|
+
changed = true;
|
|
204
|
+
}
|
|
205
|
+
const rateKey = this.rateKeyOf(spec);
|
|
206
|
+
if (
|
|
207
|
+
segmentKbps &&
|
|
208
|
+
Number.isFinite(segmentKbps.averageKbps) && segmentKbps.averageKbps > 0 &&
|
|
209
|
+
Number.isFinite(segmentKbps.peakKbps) && segmentKbps.peakKbps > 0
|
|
210
|
+
) {
|
|
211
|
+
this.#push("rates", rateKey, {
|
|
212
|
+
averageKbps: segmentKbps.averageKbps,
|
|
213
|
+
peakKbps: segmentKbps.peakKbps,
|
|
214
|
+
at: this.#now()
|
|
215
|
+
});
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
if (changed) {
|
|
219
|
+
this.#save();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* A viewer has been moved onto an output that was being prepared for them:
|
|
225
|
+
* keep how long it took and what they held when they moved.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} params
|
|
228
|
+
* @param {import("./output/OutputSpec.js").OutputSpec} params.spec - The output moved onto.
|
|
229
|
+
* @param {number} params.seconds - From the preparation starting to the move.
|
|
230
|
+
* @param {number | null} params.bufferedSec - What the viewer held at the move.
|
|
231
|
+
* @returns {void}
|
|
232
|
+
*/
|
|
233
|
+
notePreparation({ spec, seconds, bufferedSec }) {
|
|
234
|
+
const modeKey = this.modeKeyOf(spec);
|
|
235
|
+
if (modeKey === null || !(Number.isFinite(seconds) && seconds >= 0)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.#push("preparation", modeKey, {
|
|
239
|
+
seconds,
|
|
240
|
+
bufferedSec: Number.isFinite(bufferedSec) ? bufferedSec : null,
|
|
241
|
+
at: this.#now()
|
|
242
|
+
});
|
|
243
|
+
this.#save();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The slowest this mode has been seen running alone on comparable material,
|
|
248
|
+
* or null where nothing comparable has been seen.
|
|
249
|
+
*
|
|
250
|
+
* @param {import("./output/OutputSpec.js").OutputSpec} spec
|
|
251
|
+
* @param {ContentDescriptor | null} content
|
|
252
|
+
* @returns {number | null}
|
|
253
|
+
*/
|
|
254
|
+
slowestAloneSpeed(spec, content) {
|
|
255
|
+
const modeKey = this.modeKeyOf(spec);
|
|
256
|
+
if (modeKey === null || !LocalObservations.#usableContent(content)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
let slowest = null;
|
|
260
|
+
for (const reading of this.#entries.speed[modeKey] ?? []) {
|
|
261
|
+
const seen = reading.content;
|
|
262
|
+
if (
|
|
263
|
+
seen?.codec === content.codec &&
|
|
264
|
+
(seen?.bitDepth ?? null) === (content.bitDepth ?? null) &&
|
|
265
|
+
seen?.width === content.width &&
|
|
266
|
+
seen?.height === content.height &&
|
|
267
|
+
seen?.megabitsPerSecond >= content.megabitsPerSecond
|
|
268
|
+
) {
|
|
269
|
+
slowest = slowest === null ? reading.speedX : Math.min(slowest, reading.speedX);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return slowest;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The largest peak an output of this mode and rate control has been seen
|
|
277
|
+
* carrying, in megabits per second, or null.
|
|
278
|
+
*
|
|
279
|
+
* @param {import("./output/OutputSpec.js").OutputSpec} spec
|
|
280
|
+
* @returns {number | null}
|
|
281
|
+
*/
|
|
282
|
+
peakMbps(spec) {
|
|
283
|
+
const rateKey = this.rateKeyOf(spec);
|
|
284
|
+
const readings = rateKey === null ? [] : (this.#entries.rates[rateKey] ?? []);
|
|
285
|
+
return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.peakKbps)) / 1000;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The longest an output of this mode has been seen taking to be ready for a
|
|
290
|
+
* viewer moving onto it, in seconds, or null.
|
|
291
|
+
*
|
|
292
|
+
* @param {import("./output/OutputSpec.js").OutputSpec} spec
|
|
293
|
+
* @returns {number | null}
|
|
294
|
+
*/
|
|
295
|
+
longestPreparationSec(spec) {
|
|
296
|
+
const modeKey = this.modeKeyOf(spec);
|
|
297
|
+
const readings = modeKey === null ? [] : (this.#entries.preparation[modeKey] ?? []);
|
|
298
|
+
return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.seconds));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {"speed" | "rates" | "preparation"} kind
|
|
303
|
+
* @param {string} key
|
|
304
|
+
* @param {object} reading
|
|
305
|
+
* @returns {void}
|
|
306
|
+
*/
|
|
307
|
+
#push(kind, key, reading) {
|
|
308
|
+
const readings = [...(this.#entries[kind][key] ?? []), reading].slice(-READINGS_KEPT);
|
|
309
|
+
this.#entries[kind][key] = readings;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** @returns {void} */
|
|
313
|
+
#save() {
|
|
314
|
+
if (!this.#filePath) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
writeFileSync(this.#filePath, JSON.stringify({ version: FILE_VERSION, entries: this.#entries }));
|
|
319
|
+
} catch (error) {
|
|
320
|
+
// A read-only install keeps what it learned for this process only.
|
|
321
|
+
this.#logger.warn(
|
|
322
|
+
`local observations: could not write ${this.#filePath} (${error instanceof Error ? error.message : String(error)})`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {ContentDescriptor | null} content
|
|
329
|
+
* @returns {boolean}
|
|
330
|
+
*/
|
|
331
|
+
static #usableContent(content) {
|
|
332
|
+
return Boolean(content) &&
|
|
333
|
+
typeof content.codec === "string" && content.codec.length > 0 &&
|
|
334
|
+
content.width > 0 && content.height > 0 &&
|
|
335
|
+
Number.isFinite(content.megabitsPerSecond) && content.megabitsPerSecond > 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* What an observation of an encode is filed as having been made FROM.
|
|
341
|
+
*
|
|
342
|
+
* @param {{ width: number | null, height: number | null, decode: { codec: string, bitDepth: number | null, megabitsPerSecond: number } | null } | null} file
|
|
343
|
+
* @returns {ContentDescriptor | null}
|
|
344
|
+
*/
|
|
345
|
+
export function contentOf(file) {
|
|
346
|
+
const decode = file?.decode ?? null;
|
|
347
|
+
const width = Number(file?.width) || 0;
|
|
348
|
+
const height = Number(file?.height) || 0;
|
|
349
|
+
if (!decode || width <= 0 || height <= 0) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
codec: decode.codec,
|
|
354
|
+
bitDepth: decode.bitDepth ?? null,
|
|
355
|
+
width,
|
|
356
|
+
height,
|
|
357
|
+
megabitsPerSecond: decode.megabitsPerSecond
|
|
358
|
+
};
|
|
359
|
+
}
|