@torrent-tv/proxy 2.88.2 → 2.88.4
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 +2259 -2246
- 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 +23 -4
- 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/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- 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 +783 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -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 +1150 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -254
- package/services/encode/quality/output-format.js +319 -315
- 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 +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +21 -6
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +298 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -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 +1344 -1244
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -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/audio-bound.js +233 -0
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +13 -8
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +995 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +70 -0
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -131
- 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/fixtures/audio-frame-boundary.json +1 -0
- package/test/fmp4-presentation-clock.test.js +147 -147
- 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 +112 -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 +270 -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 +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -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 +105 -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/soundtrack-rate-bound.test.js +289 -0
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-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 +504 -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,178 +1,178 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What starting and stopping an encoder costs on THIS host, measured
|
|
3
|
-
* before any viewer exists.
|
|
4
|
-
*
|
|
5
|
-
* WHY IT HAS TO BE MEASURED AT STARTUP. Both figures decide one thing: whether
|
|
6
|
-
* to leave an encoder where it stands or kill it and start another elsewhere.
|
|
7
|
-
* The plan compares when the wanted pieces appear under each arrangement, and
|
|
8
|
-
* for that it needs how long a fresh encoder takes to produce anything and how
|
|
9
|
-
* long killing one takes.
|
|
10
|
-
*
|
|
11
|
-
* Until now both were learned only from runs that had ENDED, so at a cold open
|
|
12
|
-
* they were zero — and zero does not read as "not measured", it reads as
|
|
13
|
-
* "free". A warming encoder then owed one piece and a moved one owed
|
|
14
|
-
* `0 + 0 + one piece`: the same figure to the millisecond. The tie fell to
|
|
15
|
-
* position, so any advantage however small won, and the plan moved the encoder
|
|
16
|
-
* on every pass. Field 2026-09-08, the first fifteen seconds of a session:
|
|
17
|
-
* start at #68, a second later kill and start at #69, half a second later kill
|
|
18
|
-
* and start at #68 again, each dying having produced nothing. Over two days,
|
|
19
|
-
* 153 runs stopped that way and 68 of them made no segment at all.
|
|
20
|
-
*
|
|
21
|
-
* ONE RUN GIVES BOTH READINGS. An encoder is started on a generated picture
|
|
22
|
-
* through the same pipeline a session uses, timed until it says it has closed
|
|
23
|
-
* its first piece, then killed and timed until it exits. Nothing about the
|
|
24
|
-
* measurement is chosen: it is the same encoder, the same muxer, the same
|
|
25
|
-
* channel the encoder announces finished pieces on.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import { spawn } from "node:child_process";
|
|
29
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
30
|
-
import os from "node:os";
|
|
31
|
-
import path from "node:path";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* How long the measurement may take before it is abandoned.
|
|
35
|
-
*
|
|
36
|
-
* Not a property of the host and not a figure anything is derived from: it
|
|
37
|
-
* bounds a startup step so a machine that cannot produce a piece at all does not
|
|
38
|
-
* hold the proxy closed. A host that hits it has said something useful — that
|
|
39
|
-
* its first piece takes longer than this — and the plan is told the bound rather
|
|
40
|
-
* than a zero.
|
|
41
|
-
*/
|
|
42
|
-
const GIVE_UP_AFTER_MS = 30_000;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Measure a start and a stop on this host.
|
|
46
|
-
*
|
|
47
|
-
* @param {object} params
|
|
48
|
-
* @param {string} params.ffmpegBin
|
|
49
|
-
* @param {import("./Encoder.js").Encoder} params.encoder - The encoder this
|
|
50
|
-
* proxy has chosen, so the reading is of the thing that will actually run.
|
|
51
|
-
* @param {number} [params.segmentDurationSec]
|
|
52
|
-
* @param {{ info: Function, warn: Function }} [params.logger]
|
|
53
|
-
* @param {() => number} [params.now]
|
|
54
|
-
* @returns {Promise<{ firstByteWaitSec: number, killCostSec: number } | null>}
|
|
55
|
-
* Null where nothing could be measured, which is said rather than passed off
|
|
56
|
-
* as a zero.
|
|
57
|
-
*/
|
|
58
|
-
export async function measureStartAndStop({
|
|
59
|
-
ffmpegBin,
|
|
60
|
-
encoder,
|
|
61
|
-
segmentDurationSec = 4,
|
|
62
|
-
logger = null,
|
|
63
|
-
now = Date.now
|
|
64
|
-
}) {
|
|
65
|
-
const log = logger ?? { info: () => {}, warn: () => {} };
|
|
66
|
-
const dir = mkdtempSync(path.join(os.tmpdir(), "tt-startstop-"));
|
|
67
|
-
try {
|
|
68
|
-
const args = [
|
|
69
|
-
"-hide_banner",
|
|
70
|
-
"-nostats",
|
|
71
|
-
"-loglevel",
|
|
72
|
-
"error",
|
|
73
|
-
// The device, where the kind takes one.
|
|
74
|
-
...(typeof encoder?.benchmarkInputArgs === "function" ? encoder.benchmarkInputArgs() : []),
|
|
75
|
-
// A generated picture: the reading is of this host's encoder and muxer,
|
|
76
|
-
// and a file would add its own reading and its own download.
|
|
77
|
-
"-f",
|
|
78
|
-
"lavfi",
|
|
79
|
-
"-i",
|
|
80
|
-
`testsrc2=size=640x360:rate=25`,
|
|
81
|
-
"-t",
|
|
82
|
-
String(segmentDurationSec * 4),
|
|
83
|
-
// The encoder this proxy has chosen, asked for its own arguments: the
|
|
84
|
-
// reading must be of the thing that will actually run, since what a start
|
|
85
|
-
// costs is mostly the encoder opening.
|
|
86
|
-
// The same arguments the throughput benchmark uses, for the same reason:
|
|
87
|
-
// a start is timed on the encoder itself, not on a scaler in front of it.
|
|
88
|
-
...(typeof encoder?.benchmarkArgs === "function"
|
|
89
|
-
? encoder.benchmarkArgs(null)
|
|
90
|
-
: ["-c:v", "libx264", "-preset", "ultrafast"]),
|
|
91
|
-
"-an",
|
|
92
|
-
"-f",
|
|
93
|
-
"segment",
|
|
94
|
-
"-segment_time",
|
|
95
|
-
String(segmentDurationSec),
|
|
96
|
-
// The channel the encoder names its finished pieces on — the same one a
|
|
97
|
-
// session reads, so "the first piece exists" means here what it means
|
|
98
|
-
// there.
|
|
99
|
-
"-segment_list",
|
|
100
|
-
"pipe:3",
|
|
101
|
-
"-segment_list_flags",
|
|
102
|
-
"+live",
|
|
103
|
-
"-segment_format",
|
|
104
|
-
"mp4",
|
|
105
|
-
path.join(dir, "seg-%05d.mp4")
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
const spawnedAt = now();
|
|
109
|
-
const child = spawn(ffmpegBin, args, {
|
|
110
|
-
stdio: ["ignore", "ignore", "pipe", "pipe"],
|
|
111
|
-
windowsHide: true
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const firstPiece = await new Promise((resolve) => {
|
|
115
|
-
const timer = setTimeout(() => resolve(null), GIVE_UP_AFTER_MS);
|
|
116
|
-
timer.unref?.();
|
|
117
|
-
child.stdio?.[3]?.on("data", () => {
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
resolve(now() - spawnedAt);
|
|
120
|
-
});
|
|
121
|
-
child.on("error", () => {
|
|
122
|
-
clearTimeout(timer);
|
|
123
|
-
resolve(null);
|
|
124
|
-
});
|
|
125
|
-
child.on("exit", () => {
|
|
126
|
-
clearTimeout(timer);
|
|
127
|
-
resolve(null);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (firstPiece === null) {
|
|
132
|
-
try {
|
|
133
|
-
child.kill("SIGKILL");
|
|
134
|
-
} catch {
|
|
135
|
-
// Already gone.
|
|
136
|
-
}
|
|
137
|
-
log.warn("hwaccel: a start could not be measured; the plan is told so rather than told zero");
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const killedAt = now();
|
|
142
|
-
const died = await new Promise((resolve) => {
|
|
143
|
-
const timer = setTimeout(() => resolve(null), GIVE_UP_AFTER_MS);
|
|
144
|
-
timer.unref?.();
|
|
145
|
-
child.on("exit", () => {
|
|
146
|
-
clearTimeout(timer);
|
|
147
|
-
resolve(now() - killedAt);
|
|
148
|
-
});
|
|
149
|
-
try {
|
|
150
|
-
child.kill("SIGTERM");
|
|
151
|
-
} catch {
|
|
152
|
-
clearTimeout(timer);
|
|
153
|
-
resolve(null);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const firstByteWaitSec = firstPiece / 1000;
|
|
158
|
-
const killCostSec = died === null ? 0 : died / 1000;
|
|
159
|
-
log.info(
|
|
160
|
-
`hwaccel: a start costs ${firstByteWaitSec.toFixed(2)}s to a first piece and ` +
|
|
161
|
-
`a stop ${killCostSec.toFixed(2)}s on this host — measured before any viewer, ` +
|
|
162
|
-
"because a plan told zero reads it as free and moves an encoder for nothing"
|
|
163
|
-
);
|
|
164
|
-
return { firstByteWaitSec, killCostSec };
|
|
165
|
-
} catch (error) {
|
|
166
|
-
log.warn(
|
|
167
|
-
`hwaccel: a start and a stop could not be measured: ` +
|
|
168
|
-
`${error instanceof Error ? error.message : String(error)}`
|
|
169
|
-
);
|
|
170
|
-
return null;
|
|
171
|
-
} finally {
|
|
172
|
-
try {
|
|
173
|
-
rmSync(dir, { recursive: true, force: true });
|
|
174
|
-
} catch {
|
|
175
|
-
// best effort
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file What starting and stopping an encoder costs on THIS host, measured
|
|
3
|
+
* before any viewer exists.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT HAS TO BE MEASURED AT STARTUP. Both figures decide one thing: whether
|
|
6
|
+
* to leave an encoder where it stands or kill it and start another elsewhere.
|
|
7
|
+
* The plan compares when the wanted pieces appear under each arrangement, and
|
|
8
|
+
* for that it needs how long a fresh encoder takes to produce anything and how
|
|
9
|
+
* long killing one takes.
|
|
10
|
+
*
|
|
11
|
+
* Until now both were learned only from runs that had ENDED, so at a cold open
|
|
12
|
+
* they were zero — and zero does not read as "not measured", it reads as
|
|
13
|
+
* "free". A warming encoder then owed one piece and a moved one owed
|
|
14
|
+
* `0 + 0 + one piece`: the same figure to the millisecond. The tie fell to
|
|
15
|
+
* position, so any advantage however small won, and the plan moved the encoder
|
|
16
|
+
* on every pass. Field 2026-09-08, the first fifteen seconds of a session:
|
|
17
|
+
* start at #68, a second later kill and start at #69, half a second later kill
|
|
18
|
+
* and start at #68 again, each dying having produced nothing. Over two days,
|
|
19
|
+
* 153 runs stopped that way and 68 of them made no segment at all.
|
|
20
|
+
*
|
|
21
|
+
* ONE RUN GIVES BOTH READINGS. An encoder is started on a generated picture
|
|
22
|
+
* through the same pipeline a session uses, timed until it says it has closed
|
|
23
|
+
* its first piece, then killed and timed until it exits. Nothing about the
|
|
24
|
+
* measurement is chosen: it is the same encoder, the same muxer, the same
|
|
25
|
+
* channel the encoder announces finished pieces on.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
29
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
30
|
+
import os from "node:os";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How long the measurement may take before it is abandoned.
|
|
35
|
+
*
|
|
36
|
+
* Not a property of the host and not a figure anything is derived from: it
|
|
37
|
+
* bounds a startup step so a machine that cannot produce a piece at all does not
|
|
38
|
+
* hold the proxy closed. A host that hits it has said something useful — that
|
|
39
|
+
* its first piece takes longer than this — and the plan is told the bound rather
|
|
40
|
+
* than a zero.
|
|
41
|
+
*/
|
|
42
|
+
const GIVE_UP_AFTER_MS = 30_000;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Measure a start and a stop on this host.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} params
|
|
48
|
+
* @param {string} params.ffmpegBin
|
|
49
|
+
* @param {import("./Encoder.js").Encoder} params.encoder - The encoder this
|
|
50
|
+
* proxy has chosen, so the reading is of the thing that will actually run.
|
|
51
|
+
* @param {number} [params.segmentDurationSec]
|
|
52
|
+
* @param {{ info: Function, warn: Function }} [params.logger]
|
|
53
|
+
* @param {() => number} [params.now]
|
|
54
|
+
* @returns {Promise<{ firstByteWaitSec: number, killCostSec: number } | null>}
|
|
55
|
+
* Null where nothing could be measured, which is said rather than passed off
|
|
56
|
+
* as a zero.
|
|
57
|
+
*/
|
|
58
|
+
export async function measureStartAndStop({
|
|
59
|
+
ffmpegBin,
|
|
60
|
+
encoder,
|
|
61
|
+
segmentDurationSec = 4,
|
|
62
|
+
logger = null,
|
|
63
|
+
now = Date.now
|
|
64
|
+
}) {
|
|
65
|
+
const log = logger ?? { info: () => {}, warn: () => {} };
|
|
66
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "tt-startstop-"));
|
|
67
|
+
try {
|
|
68
|
+
const args = [
|
|
69
|
+
"-hide_banner",
|
|
70
|
+
"-nostats",
|
|
71
|
+
"-loglevel",
|
|
72
|
+
"error",
|
|
73
|
+
// The device, where the kind takes one.
|
|
74
|
+
...(typeof encoder?.benchmarkInputArgs === "function" ? encoder.benchmarkInputArgs() : []),
|
|
75
|
+
// A generated picture: the reading is of this host's encoder and muxer,
|
|
76
|
+
// and a file would add its own reading and its own download.
|
|
77
|
+
"-f",
|
|
78
|
+
"lavfi",
|
|
79
|
+
"-i",
|
|
80
|
+
`testsrc2=size=640x360:rate=25`,
|
|
81
|
+
"-t",
|
|
82
|
+
String(segmentDurationSec * 4),
|
|
83
|
+
// The encoder this proxy has chosen, asked for its own arguments: the
|
|
84
|
+
// reading must be of the thing that will actually run, since what a start
|
|
85
|
+
// costs is mostly the encoder opening.
|
|
86
|
+
// The same arguments the throughput benchmark uses, for the same reason:
|
|
87
|
+
// a start is timed on the encoder itself, not on a scaler in front of it.
|
|
88
|
+
...(typeof encoder?.benchmarkArgs === "function"
|
|
89
|
+
? encoder.benchmarkArgs(null)
|
|
90
|
+
: ["-c:v", "libx264", "-preset", "ultrafast"]),
|
|
91
|
+
"-an",
|
|
92
|
+
"-f",
|
|
93
|
+
"segment",
|
|
94
|
+
"-segment_time",
|
|
95
|
+
String(segmentDurationSec),
|
|
96
|
+
// The channel the encoder names its finished pieces on — the same one a
|
|
97
|
+
// session reads, so "the first piece exists" means here what it means
|
|
98
|
+
// there.
|
|
99
|
+
"-segment_list",
|
|
100
|
+
"pipe:3",
|
|
101
|
+
"-segment_list_flags",
|
|
102
|
+
"+live",
|
|
103
|
+
"-segment_format",
|
|
104
|
+
"mp4",
|
|
105
|
+
path.join(dir, "seg-%05d.mp4")
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const spawnedAt = now();
|
|
109
|
+
const child = spawn(ffmpegBin, args, {
|
|
110
|
+
stdio: ["ignore", "ignore", "pipe", "pipe"],
|
|
111
|
+
windowsHide: true
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const firstPiece = await new Promise((resolve) => {
|
|
115
|
+
const timer = setTimeout(() => resolve(null), GIVE_UP_AFTER_MS);
|
|
116
|
+
timer.unref?.();
|
|
117
|
+
child.stdio?.[3]?.on("data", () => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
resolve(now() - spawnedAt);
|
|
120
|
+
});
|
|
121
|
+
child.on("error", () => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
resolve(null);
|
|
124
|
+
});
|
|
125
|
+
child.on("exit", () => {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
resolve(null);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (firstPiece === null) {
|
|
132
|
+
try {
|
|
133
|
+
child.kill("SIGKILL");
|
|
134
|
+
} catch {
|
|
135
|
+
// Already gone.
|
|
136
|
+
}
|
|
137
|
+
log.warn("hwaccel: a start could not be measured; the plan is told so rather than told zero");
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const killedAt = now();
|
|
142
|
+
const died = await new Promise((resolve) => {
|
|
143
|
+
const timer = setTimeout(() => resolve(null), GIVE_UP_AFTER_MS);
|
|
144
|
+
timer.unref?.();
|
|
145
|
+
child.on("exit", () => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
resolve(now() - killedAt);
|
|
148
|
+
});
|
|
149
|
+
try {
|
|
150
|
+
child.kill("SIGTERM");
|
|
151
|
+
} catch {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
resolve(null);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const firstByteWaitSec = firstPiece / 1000;
|
|
158
|
+
const killCostSec = died === null ? 0 : died / 1000;
|
|
159
|
+
log.info(
|
|
160
|
+
`hwaccel: a start costs ${firstByteWaitSec.toFixed(2)}s to a first piece and ` +
|
|
161
|
+
`a stop ${killCostSec.toFixed(2)}s on this host — measured before any viewer, ` +
|
|
162
|
+
"because a plan told zero reads it as free and moves an encoder for nothing"
|
|
163
|
+
);
|
|
164
|
+
return { firstByteWaitSec, killCostSec };
|
|
165
|
+
} catch (error) {
|
|
166
|
+
log.warn(
|
|
167
|
+
`hwaccel: a start and a stop could not be measured: ` +
|
|
168
|
+
`${error instanceof Error ? error.message : String(error)}`
|
|
169
|
+
);
|
|
170
|
+
return null;
|
|
171
|
+
} finally {
|
|
172
|
+
try {
|
|
173
|
+
rmSync(dir, { recursive: true, force: true });
|
|
174
|
+
} catch {
|
|
175
|
+
// best effort
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How fast a calibrated encoding mode runs at a given frame size, from
|
|
3
|
-
* the readings the startup calibration took (`encode/calibration.js`).
|
|
4
|
-
*
|
|
5
|
-
* Pure arithmetic over measured figures, kept apart from the calibration so
|
|
6
|
-
* that everything which prices an encode can read it without reaching the code
|
|
7
|
-
* that runs ffmpeg.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {object} SizeReading
|
|
12
|
-
* @property {number} width
|
|
13
|
-
* @property {number} height
|
|
14
|
-
* @property {number} pixelsPerSec
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The rate of pictures this mode would encode at this frame, in pixels per
|
|
19
|
-
* second, or null where this machine has not shown it can.
|
|
20
|
-
*
|
|
21
|
-
* An entry with no readings by size — built before sizes were read, or by a
|
|
22
|
-
* check that states one figure — answers with its one figure, as it always did.
|
|
23
|
-
*
|
|
24
|
-
* @param {{ pixelsPerSec: number, bySize?: SizeReading[], interpolationError?: number | null }} entry
|
|
25
|
-
* @param {{ width: number, height: number } | null | undefined} frame
|
|
26
|
-
* @returns {number | null}
|
|
27
|
-
*/
|
|
28
|
-
export function throughputAt(entry, frame) {
|
|
29
|
-
const bySize = Array.isArray(entry?.bySize) ? entry.bySize : [];
|
|
30
|
-
const width = Number(frame?.width) || 0;
|
|
31
|
-
const height = Number(frame?.height) || 0;
|
|
32
|
-
if (bySize.length === 0 || width <= 0 || height <= 0) {
|
|
33
|
-
const figure = Number(entry?.pixelsPerSec);
|
|
34
|
-
return Number.isFinite(figure) && figure > 0 ? figure : null;
|
|
35
|
-
}
|
|
36
|
-
const area = width * height;
|
|
37
|
-
const sorted = [...bySize].sort((left, right) => left.width * left.height - right.width * right.height);
|
|
38
|
-
const smallest = sorted[0];
|
|
39
|
-
const largest = sorted[sorted.length - 1];
|
|
40
|
-
if (area < smallest.width * smallest.height || area > largest.width * largest.height) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
for (let index = 0; index < sorted.length; index += 1) {
|
|
44
|
-
const reading = sorted[index];
|
|
45
|
-
const readArea = reading.width * reading.height;
|
|
46
|
-
if (readArea === area) {
|
|
47
|
-
return reading.pixelsPerSec;
|
|
48
|
-
}
|
|
49
|
-
if (readArea > area) {
|
|
50
|
-
const error = entry.interpolationError;
|
|
51
|
-
if (!(Number.isFinite(error) && error >= 0 && error < 1)) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const below = sorted[index - 1];
|
|
55
|
-
return interpolate(below, reading, area) * (1 - error);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Pixels per second at an area between two readings, straight in logarithms
|
|
63
|
-
* of both: throughput against frame area is a power law over any short
|
|
64
|
-
* stretch, and a straight line between two points of it is the least that
|
|
65
|
-
* can be said without inventing a shape.
|
|
66
|
-
*
|
|
67
|
-
* @param {SizeReading} below
|
|
68
|
-
* @param {SizeReading} above
|
|
69
|
-
* @param {number} area
|
|
70
|
-
* @returns {number}
|
|
71
|
-
*/
|
|
72
|
-
function interpolate(below, above, area) {
|
|
73
|
-
const lowArea = below.width * below.height;
|
|
74
|
-
const highArea = above.width * above.height;
|
|
75
|
-
const share = Math.log(area / lowArea) / Math.log(highArea / lowArea);
|
|
76
|
-
return Math.exp(Math.log(below.pixelsPerSec) + share * (Math.log(above.pixelsPerSec) - Math.log(below.pixelsPerSec)));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* How far an interpolation between readings of this mode was measured to be
|
|
81
|
-
* wrong: the largest relative error of a middle reading predicted from the two
|
|
82
|
-
* outside it.
|
|
83
|
-
*
|
|
84
|
-
* @param {SizeReading[]} bySize - Smallest first.
|
|
85
|
-
* @returns {number | null} Null with fewer than three readings.
|
|
86
|
-
*/
|
|
87
|
-
export function interpolationErrorOf(bySize) {
|
|
88
|
-
let largest = null;
|
|
89
|
-
for (let index = 1; index + 1 < bySize.length; index += 1) {
|
|
90
|
-
const middle = bySize[index];
|
|
91
|
-
const predicted = interpolate(bySize[index - 1], bySize[index + 1], middle.width * middle.height);
|
|
92
|
-
const error = Math.abs(predicted - middle.pixelsPerSec) / middle.pixelsPerSec;
|
|
93
|
-
largest = largest === null ? error : Math.max(largest, error);
|
|
94
|
-
}
|
|
95
|
-
return largest;
|
|
96
|
-
}
|
|
97
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file How fast a calibrated encoding mode runs at a given frame size, from
|
|
3
|
+
* the readings the startup calibration took (`encode/calibration.js`).
|
|
4
|
+
*
|
|
5
|
+
* Pure arithmetic over measured figures, kept apart from the calibration so
|
|
6
|
+
* that everything which prices an encode can read it without reaching the code
|
|
7
|
+
* that runs ffmpeg.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} SizeReading
|
|
12
|
+
* @property {number} width
|
|
13
|
+
* @property {number} height
|
|
14
|
+
* @property {number} pixelsPerSec
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The rate of pictures this mode would encode at this frame, in pixels per
|
|
19
|
+
* second, or null where this machine has not shown it can.
|
|
20
|
+
*
|
|
21
|
+
* An entry with no readings by size — built before sizes were read, or by a
|
|
22
|
+
* check that states one figure — answers with its one figure, as it always did.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ pixelsPerSec: number, bySize?: SizeReading[], interpolationError?: number | null }} entry
|
|
25
|
+
* @param {{ width: number, height: number } | null | undefined} frame
|
|
26
|
+
* @returns {number | null}
|
|
27
|
+
*/
|
|
28
|
+
export function throughputAt(entry, frame) {
|
|
29
|
+
const bySize = Array.isArray(entry?.bySize) ? entry.bySize : [];
|
|
30
|
+
const width = Number(frame?.width) || 0;
|
|
31
|
+
const height = Number(frame?.height) || 0;
|
|
32
|
+
if (bySize.length === 0 || width <= 0 || height <= 0) {
|
|
33
|
+
const figure = Number(entry?.pixelsPerSec);
|
|
34
|
+
return Number.isFinite(figure) && figure > 0 ? figure : null;
|
|
35
|
+
}
|
|
36
|
+
const area = width * height;
|
|
37
|
+
const sorted = [...bySize].sort((left, right) => left.width * left.height - right.width * right.height);
|
|
38
|
+
const smallest = sorted[0];
|
|
39
|
+
const largest = sorted[sorted.length - 1];
|
|
40
|
+
if (area < smallest.width * smallest.height || area > largest.width * largest.height) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
for (let index = 0; index < sorted.length; index += 1) {
|
|
44
|
+
const reading = sorted[index];
|
|
45
|
+
const readArea = reading.width * reading.height;
|
|
46
|
+
if (readArea === area) {
|
|
47
|
+
return reading.pixelsPerSec;
|
|
48
|
+
}
|
|
49
|
+
if (readArea > area) {
|
|
50
|
+
const error = entry.interpolationError;
|
|
51
|
+
if (!(Number.isFinite(error) && error >= 0 && error < 1)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const below = sorted[index - 1];
|
|
55
|
+
return interpolate(below, reading, area) * (1 - error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pixels per second at an area between two readings, straight in logarithms
|
|
63
|
+
* of both: throughput against frame area is a power law over any short
|
|
64
|
+
* stretch, and a straight line between two points of it is the least that
|
|
65
|
+
* can be said without inventing a shape.
|
|
66
|
+
*
|
|
67
|
+
* @param {SizeReading} below
|
|
68
|
+
* @param {SizeReading} above
|
|
69
|
+
* @param {number} area
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function interpolate(below, above, area) {
|
|
73
|
+
const lowArea = below.width * below.height;
|
|
74
|
+
const highArea = above.width * above.height;
|
|
75
|
+
const share = Math.log(area / lowArea) / Math.log(highArea / lowArea);
|
|
76
|
+
return Math.exp(Math.log(below.pixelsPerSec) + share * (Math.log(above.pixelsPerSec) - Math.log(below.pixelsPerSec)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How far an interpolation between readings of this mode was measured to be
|
|
81
|
+
* wrong: the largest relative error of a middle reading predicted from the two
|
|
82
|
+
* outside it.
|
|
83
|
+
*
|
|
84
|
+
* @param {SizeReading[]} bySize - Smallest first.
|
|
85
|
+
* @returns {number | null} Null with fewer than three readings.
|
|
86
|
+
*/
|
|
87
|
+
export function interpolationErrorOf(bySize) {
|
|
88
|
+
let largest = null;
|
|
89
|
+
for (let index = 1; index + 1 < bySize.length; index += 1) {
|
|
90
|
+
const middle = bySize[index];
|
|
91
|
+
const predicted = interpolate(bySize[index - 1], bySize[index + 1], middle.width * middle.height);
|
|
92
|
+
const error = Math.abs(predicted - middle.pixelsPerSec) / middle.pixelsPerSec;
|
|
93
|
+
largest = largest === null ? error : Math.max(largest, error);
|
|
94
|
+
}
|
|
95
|
+
return largest;
|
|
96
|
+
}
|
|
97
|
+
|