@torrent-tv/proxy 2.87.10 → 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 -2181
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +124 -101
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/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 -931
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +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 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +323 -0
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,273 +1,273 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Step 14 of roadmap item 97: which modes this machine may encode with,
|
|
3
|
-
* what each costs at every size, and what the proxy keeps between runs about
|
|
4
|
-
* its own admitted encodes — all on fakes. Nothing here runs ffmpeg, starts a
|
|
5
|
-
* torrent client or touches a swarm; the one file written is in a temporary
|
|
6
|
-
* directory.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
-
import os from "node:os";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
configurationKeyOf,
|
|
16
|
-
kindOfEncoderName,
|
|
17
|
-
parseFfmpegVersion,
|
|
18
|
-
parseNvidiaSmi,
|
|
19
|
-
parseX264Version,
|
|
20
|
-
UNKNOWN
|
|
21
|
-
} from "../services/encode/fingerprint.js";
|
|
22
|
-
import { interpolationErrorOf, throughputAt } from "../services/encode/throughput.js";
|
|
23
|
-
import { calibrateEncoder, HostCalibration } from "../services/encode/calibration.js";
|
|
24
|
-
import { canSustainOutput, pickSoftwarePreset } from "../services/encode/hwaccel.js";
|
|
25
|
-
import { contentOf, LocalObservations } from "../services/encode/LocalObservations.js";
|
|
26
|
-
import { OutputSpec, VideoOutput, CutGrid } from "../services/encode/output/index.js";
|
|
27
|
-
import { PEAK_CLASS, videoLoadOfSpec } from "../services/encode/quality/link-budget.js";
|
|
28
|
-
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
29
|
-
import { NvencEncoder } from "../services/encode/NvencEncoder.js";
|
|
30
|
-
|
|
31
|
-
const FINGERPRINT = Object.freeze({
|
|
32
|
-
ffmpeg: "8.1.2",
|
|
33
|
-
x264: "core 164 r3108 31e19f9",
|
|
34
|
-
cpu: "Cortex-A72",
|
|
35
|
-
threads: 4,
|
|
36
|
-
devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.6.31)" } }
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
/** A re-encoded picture's spec, the way `OutputOpening` builds one. */
|
|
40
|
-
function specAt({ encoder = "libx264", width = 1280, height = 720, fps = 24, preset = "veryfast", rateControl = null } = {}) {
|
|
41
|
-
return new OutputSpec({
|
|
42
|
-
sourceKey: "src",
|
|
43
|
-
segmentFormatId: "fmp4",
|
|
44
|
-
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
45
|
-
video: new VideoOutput({ fileIndex: 0, encode: { encoder, width, height, fps, preset, tonemap: false, rateControl } }),
|
|
46
|
-
audio: null
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// --- The configuration key -------------------------------------------------
|
|
51
|
-
|
|
52
|
-
test("the builds and devices are read out of what the tools print", () => {
|
|
53
|
-
assert.equal(parseFfmpegVersion("ffmpeg version 8.1.2 Copyright (c) 2000-2025\nbuilt with gcc"), "8.1.2");
|
|
54
|
-
assert.equal(parseFfmpegVersion("garbage"), UNKNOWN);
|
|
55
|
-
const stream = Buffer.from("\0\0\x01\x06\x05x264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec", "latin1");
|
|
56
|
-
assert.equal(parseX264Version(stream), "core 164 r3108 31e19f9");
|
|
57
|
-
assert.equal(parseX264Version(Buffer.from("no version here")), UNKNOWN);
|
|
58
|
-
assert.deepEqual(parseNvidiaSmi("NVIDIA RTX 4000 SFF Ada Generation, 550.54.14\n"), {
|
|
59
|
-
model: "NVIDIA RTX 4000 SFF Ada Generation",
|
|
60
|
-
driver: "550.54.14"
|
|
61
|
-
});
|
|
62
|
-
assert.equal(kindOfEncoderName("h264_nvenc"), "nvenc");
|
|
63
|
-
assert.equal(kindOfEncoderName("libx264"), "software");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("a driver update invalidates what a device was seen doing, and nothing about software", () => {
|
|
67
|
-
const vaapi = { name: "h264_vaapi", kind: "vaapi" };
|
|
68
|
-
const software = { name: "libx264", kind: "software" };
|
|
69
|
-
const updated = { ...FINGERPRINT, devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.8.0)" } } };
|
|
70
|
-
assert.notEqual(configurationKeyOf(FINGERPRINT, vaapi), configurationKeyOf(updated, vaapi));
|
|
71
|
-
assert.equal(configurationKeyOf(FINGERPRINT, software), configurationKeyOf(updated, software));
|
|
72
|
-
// Another x264 build or another thread count is another software configuration.
|
|
73
|
-
assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, threads: 14 }, software));
|
|
74
|
-
assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, x264: "core 165" }, software));
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// --- Throughput by size ----------------------------------------------------
|
|
78
|
-
|
|
79
|
-
const READINGS = [
|
|
80
|
-
{ width: 256, height: 144, pixelsPerSec: 40e6 },
|
|
81
|
-
{ width: 640, height: 360, pixelsPerSec: 36e6 },
|
|
82
|
-
{ width: 1280, height: 720, pixelsPerSec: 30e6 }
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
test("a size is priced from the readings around it, lowered by the error measured for interpolating", () => {
|
|
86
|
-
const error = interpolationErrorOf(READINGS);
|
|
87
|
-
assert.ok(error !== null && error >= 0, "three readings give a measured bound");
|
|
88
|
-
const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: error };
|
|
89
|
-
assert.equal(throughputAt(entry, { width: 640, height: 360 }), 36e6, "a size read is its own reading");
|
|
90
|
-
const between = throughputAt(entry, { width: 854, height: 480 });
|
|
91
|
-
assert.ok(between < 36e6 && between > 30e6 * (1 - error), `between the two readings, got ${between}`);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("a size beyond what was read is not a size this machine has shown it can do", () => {
|
|
95
|
-
const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 };
|
|
96
|
-
assert.equal(throughputAt(entry, { width: 1920, height: 1080 }), null);
|
|
97
|
-
assert.equal(throughputAt(entry, { width: 128, height: 72 }), null);
|
|
98
|
-
// Two readings measure no interpolation error: only the sizes read are known.
|
|
99
|
-
const two = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS.slice(0, 2), interpolationError: null };
|
|
100
|
-
assert.equal(throughputAt(two, { width: 426, height: 240 }), null);
|
|
101
|
-
assert.equal(throughputAt(two, { width: 640, height: 360 }), 36e6);
|
|
102
|
-
// An entry with no readings by size answers with its one figure, as before.
|
|
103
|
-
assert.equal(throughputAt({ preset: "fast", pixelsPerSec: 5e6 }, { width: 1920, height: 1080 }), 5e6);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("the offer does not keep a rung at a size no mode was measured at", () => {
|
|
107
|
-
const benchmark = [{ preset: "ultrafast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 }];
|
|
108
|
-
const at1080 = canSustainOutput({ benchmark, outputPixelsPerSec: 1920 * 1080 * 24, frame: { width: 1920, height: 1080 } });
|
|
109
|
-
assert.deepEqual(at1080, { speed: null, sustainable: false });
|
|
110
|
-
// The same mode at a size it WAS read at, with decoding priced: 30 Mpx/s
|
|
111
|
-
// against 1280x720x24 is 1.36x, and a decode at 10x leaves 1.19x.
|
|
112
|
-
const at720 = canSustainOutput({
|
|
113
|
-
benchmark,
|
|
114
|
-
outputPixelsPerSec: 1280 * 720 * 24,
|
|
115
|
-
frame: { width: 1280, height: 720 },
|
|
116
|
-
observedDecodeCostSec: 0.1
|
|
117
|
-
});
|
|
118
|
-
assert.equal(at720.sustainable, true);
|
|
119
|
-
assert.ok(Math.abs(at720.speed - 1 / (0.1 + (1280 * 720 * 24) / 30e6)) < 1e-9);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("a preset not measured at a size is not chosen there", () => {
|
|
123
|
-
const benchmark = [
|
|
124
|
-
{ preset: "fast", pixelsPerSec: 20e6, bySize: READINGS.slice(0, 2), interpolationError: null },
|
|
125
|
-
{ preset: "ultrafast", pixelsPerSec: 60e6, bySize: READINGS, interpolationError: 0.05 }
|
|
126
|
-
];
|
|
127
|
-
assert.equal(pickSoftwarePreset(benchmark, 1280 * 720 * 24, {}, { width: 1280, height: 720 }), "ultrafast");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// --- The calibration walk --------------------------------------------------
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* A calibration with every reading replaced: `speeds` maps "rung@WxH" to how
|
|
134
|
-
* many times realtime that mode encodes there, `failing` the rungs whose
|
|
135
|
-
* segments do not decode.
|
|
136
|
-
*/
|
|
137
|
-
async function calibrateWith({ encoder, speeds, failing = [] }) {
|
|
138
|
-
const asked = [];
|
|
139
|
-
const result = await calibrateEncoder({
|
|
140
|
-
ffmpegBin: "never-run",
|
|
141
|
-
encoder,
|
|
142
|
-
measure: {
|
|
143
|
-
check: async ({ rung }) => (failing.includes(rung)
|
|
144
|
-
? { ok: false, reason: "a segment does not decode", segmentKbps: null }
|
|
145
|
-
: { ok: true, reason: "", segmentKbps: { averageKbps: 900, peakKbps: 1400 } }),
|
|
146
|
-
rawFrames: async (frame) => `raw-${frame.width}x${frame.height}`,
|
|
147
|
-
speed: async (rung, rawPath, frame) => {
|
|
148
|
-
asked.push(`${rung}@${frame.width}x${frame.height}`);
|
|
149
|
-
return speeds[`${rung}@${frame.width}x${frame.height}`] ?? null;
|
|
150
|
-
},
|
|
151
|
-
release: () => {}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
return { ...result, asked };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
test("a mode whose segments do not decode is not used at all", async () => {
|
|
158
|
-
const encoder = new SoftwareEncoder();
|
|
159
|
-
const speeds = {};
|
|
160
|
-
for (const rung of encoder.selectableRungs) {
|
|
161
|
-
for (const size of ["256x144", "640x360", "1280x720", "1920x1080", "3840x2160"]) {
|
|
162
|
-
speeds[`${rung}@${size}`] = 2;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const { modes, refused } = await calibrateWith({ encoder, speeds, failing: ["faster"] });
|
|
166
|
-
assert.ok(!modes.some((mode) => mode.preset === "faster"));
|
|
167
|
-
assert.ok(refused.some((one) => one.preset === "faster" && /decode/.test(one.reason)));
|
|
168
|
-
assert.equal(modes.length, encoder.selectableRungs.length - 1);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("slower modes are not read where a faster one fell behind, nor larger sizes where the fastest did", async () => {
|
|
172
|
-
const encoder = new SoftwareEncoder();
|
|
173
|
-
const speeds = {
|
|
174
|
-
"ultrafast@256x144": 20, "superfast@256x144": 15, "veryfast@256x144": 10, "faster@256x144": 8, "fast@256x144": 6,
|
|
175
|
-
"ultrafast@640x360": 6, "superfast@640x360": 4, "veryfast@640x360": 0.8,
|
|
176
|
-
"ultrafast@1280x720": 0.9
|
|
177
|
-
};
|
|
178
|
-
const { modes, asked } = await calibrateWith({ encoder, speeds });
|
|
179
|
-
assert.ok(!asked.includes("faster@640x360"), "a mode slower than one that fell behind is not read");
|
|
180
|
-
assert.ok(!asked.some((one) => one.endsWith("@1920x1080")), "a size larger than one the fastest could not hold is not read");
|
|
181
|
-
const ultrafast = modes.find((mode) => mode.preset === "ultrafast");
|
|
182
|
-
assert.deepEqual(ultrafast.bySize.map((reading) => `${reading.width}x${reading.height}`), ["256x144", "640x360", "1280x720"]);
|
|
183
|
-
assert.equal(ultrafast.pixelsPerSec, 640 * 360 * 24 * 6, "the reference figure is the 640x360 reading");
|
|
184
|
-
const fast = modes.find((mode) => mode.preset === "fast");
|
|
185
|
-
assert.equal(throughputAt(fast, { width: 640, height: 360 }), null, "fast was never read at 640x360");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("a device is calibrated only at the one setting its arguments pass", () => {
|
|
189
|
-
assert.deepEqual(new NvencEncoder().selectableRungs, ["p4"]);
|
|
190
|
-
assert.ok(new NvencEncoder().buildVideoArgs({ targetWidth: 1280, targetHeight: 720, segmentDurationSec: 4 }).includes("p4"));
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("the modes of the encoder in use are what prices, and software is kept for the fallback", () => {
|
|
194
|
-
const calibration = new HostCalibration({ byKind: { vaapi: [{ preset: "h264_vaapi", pixelsPerSec: 1 }], software: [] }, fingerprint: FINGERPRINT });
|
|
195
|
-
assert.equal(calibration.modesFor("vaapi").length, 1);
|
|
196
|
-
assert.deepEqual(calibration.modesFor("software"), [], "calibrated, nothing qualified");
|
|
197
|
-
assert.equal(calibration.modesFor("nvenc"), null, "not calibrated at all");
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// --- Local observations ----------------------------------------------------
|
|
201
|
-
|
|
202
|
-
test("what is kept applies only on the configuration it was seen on, and survives a restart on it", () => {
|
|
203
|
-
const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
|
|
204
|
-
const filePath = path.join(directory, "local-observations.json");
|
|
205
|
-
try {
|
|
206
|
-
const first = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
207
|
-
const content = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 };
|
|
208
|
-
first.noteEncode({ spec: specAt(), content, aloneSpeedX: 1.7, segmentKbps: { averageKbps: 2100, peakKbps: 3900 } });
|
|
209
|
-
first.notePreparation({ spec: specAt(), seconds: 6.5, bufferedSec: 41 });
|
|
210
|
-
|
|
211
|
-
const software = configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" });
|
|
212
|
-
const again = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
213
|
-
again.load([software]);
|
|
214
|
-
assert.equal(again.slowestAloneSpeed(specAt(), content), 1.7);
|
|
215
|
-
assert.equal(again.peakMbps(specAt()), 3.9);
|
|
216
|
-
assert.equal(again.longestPreparationSec(specAt()), 6.5);
|
|
217
|
-
|
|
218
|
-
// Another libx264 build: nothing kept matches, and the file is rewritten without it.
|
|
219
|
-
const rebuilt = { ...FINGERPRINT, x264: "core 165 r3200 abc" };
|
|
220
|
-
const afterUpdate = new LocalObservations({ fingerprint: rebuilt, filePath });
|
|
221
|
-
afterUpdate.load([configurationKeyOf(rebuilt, { name: "libx264", kind: "software" })]);
|
|
222
|
-
assert.equal(afterUpdate.slowestAloneSpeed(specAt(), content), null);
|
|
223
|
-
assert.equal(afterUpdate.peakMbps(specAt()), null);
|
|
224
|
-
assert.equal(JSON.parse(readFileSync(filePath, "utf8")).entries.speed && Object.keys(JSON.parse(readFileSync(filePath, "utf8")).entries.speed).length, 0);
|
|
225
|
-
} finally {
|
|
226
|
-
rmSync(directory, { recursive: true, force: true });
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("a speed seen on easier material is not applied to harder material, and the slowest comparable one is used", () => {
|
|
231
|
-
const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath: "" });
|
|
232
|
-
const light = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 4 };
|
|
233
|
-
const heavy = { ...light, megabitsPerSecond: 12 };
|
|
234
|
-
observations.noteEncode({ spec: specAt(), content: light, aloneSpeedX: 2.4, segmentKbps: null });
|
|
235
|
-
assert.equal(observations.slowestAloneSpeed(specAt(), heavy), null, "a lighter source says nothing about a heavier one");
|
|
236
|
-
observations.noteEncode({ spec: specAt(), content: heavy, aloneSpeedX: 1.3, segmentKbps: null });
|
|
237
|
-
assert.equal(observations.slowestAloneSpeed(specAt(), light), 1.3, "both apply to the lighter source, and the slower wins");
|
|
238
|
-
assert.equal(observations.slowestAloneSpeed(specAt(), { ...light, codec: "h264" }), null, "another codec is not comparable");
|
|
239
|
-
assert.equal(observations.slowestAloneSpeed(specAt({ preset: "fast" }), light), null, "another mode is not this one");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("an encoder with no bound of its own takes the peak seen, as an estimate and never as a bound", () => {
|
|
243
|
-
const hardware = specAt({ encoder: "h264_vaapi", preset: null });
|
|
244
|
-
assert.equal(videoLoadOfSpec(hardware, null).peakClass, PEAK_CLASS.UNKNOWN);
|
|
245
|
-
const seen = videoLoadOfSpec(hardware, null, 5.2);
|
|
246
|
-
assert.equal(seen.peakClass, PEAK_CLASS.ESTIMATED);
|
|
247
|
-
assert.equal(seen.mbps, 5.2);
|
|
248
|
-
// A bound the encoder is held to is never replaced by what was seen.
|
|
249
|
-
const bounded = specAt({ rateControl: { maxrateKbps: 3900, bufsizeKbps: 4500, level: "3.1" } });
|
|
250
|
-
assert.equal(videoLoadOfSpec(bounded, null, 9.9).peakClass, PEAK_CLASS.KNOWN);
|
|
251
|
-
assert.equal(videoLoadOfSpec(bounded, null, 9.9).mbps, 3.9);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("a file of another shape is not read as observations", () => {
|
|
255
|
-
const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
|
|
256
|
-
const filePath = path.join(directory, "local-observations.json");
|
|
257
|
-
try {
|
|
258
|
-
writeFileSync(filePath, JSON.stringify({ version: 0, entries: { speed: { x: [1] } } }));
|
|
259
|
-
const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
260
|
-
observations.load([configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" })]);
|
|
261
|
-
assert.equal(observations.peakMbps(specAt()), null);
|
|
262
|
-
} finally {
|
|
263
|
-
rmSync(directory, { recursive: true, force: true });
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test("what an encode was made from is described by the file's own facts", () => {
|
|
268
|
-
assert.deepEqual(
|
|
269
|
-
contentOf({ width: 1920, height: 1080, decode: { codec: "hevc", bitDepth: 10, megabitsPerSecond: 8, megapixelsPerSecond: 50 } }),
|
|
270
|
-
{ codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 }
|
|
271
|
-
);
|
|
272
|
-
assert.equal(contentOf({ width: 0, height: 0, decode: null }), null);
|
|
273
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Step 14 of roadmap item 97: which modes this machine may encode with,
|
|
3
|
+
* what each costs at every size, and what the proxy keeps between runs about
|
|
4
|
+
* its own admitted encodes — all on fakes. Nothing here runs ffmpeg, starts a
|
|
5
|
+
* torrent client or touches a swarm; the one file written is in a temporary
|
|
6
|
+
* directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
configurationKeyOf,
|
|
16
|
+
kindOfEncoderName,
|
|
17
|
+
parseFfmpegVersion,
|
|
18
|
+
parseNvidiaSmi,
|
|
19
|
+
parseX264Version,
|
|
20
|
+
UNKNOWN
|
|
21
|
+
} from "../services/encode/fingerprint.js";
|
|
22
|
+
import { interpolationErrorOf, throughputAt } from "../services/encode/throughput.js";
|
|
23
|
+
import { calibrateEncoder, HostCalibration } from "../services/encode/calibration.js";
|
|
24
|
+
import { canSustainOutput, pickSoftwarePreset } from "../services/encode/hwaccel.js";
|
|
25
|
+
import { contentOf, LocalObservations } from "../services/encode/LocalObservations.js";
|
|
26
|
+
import { OutputSpec, VideoOutput, CutGrid } from "../services/encode/output/index.js";
|
|
27
|
+
import { PEAK_CLASS, videoLoadOfSpec } from "../services/encode/quality/link-budget.js";
|
|
28
|
+
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
29
|
+
import { NvencEncoder } from "../services/encode/NvencEncoder.js";
|
|
30
|
+
|
|
31
|
+
const FINGERPRINT = Object.freeze({
|
|
32
|
+
ffmpeg: "8.1.2",
|
|
33
|
+
x264: "core 164 r3108 31e19f9",
|
|
34
|
+
cpu: "Cortex-A72",
|
|
35
|
+
threads: 4,
|
|
36
|
+
devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.6.31)" } }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** A re-encoded picture's spec, the way `OutputOpening` builds one. */
|
|
40
|
+
function specAt({ encoder = "libx264", width = 1280, height = 720, fps = 24, preset = "veryfast", rateControl = null } = {}) {
|
|
41
|
+
return new OutputSpec({
|
|
42
|
+
sourceKey: "src",
|
|
43
|
+
segmentFormatId: "fmp4",
|
|
44
|
+
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
45
|
+
video: new VideoOutput({ fileIndex: 0, encode: { encoder, width, height, fps, preset, tonemap: false, rateControl } }),
|
|
46
|
+
audio: null
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- The configuration key -------------------------------------------------
|
|
51
|
+
|
|
52
|
+
test("the builds and devices are read out of what the tools print", () => {
|
|
53
|
+
assert.equal(parseFfmpegVersion("ffmpeg version 8.1.2 Copyright (c) 2000-2025\nbuilt with gcc"), "8.1.2");
|
|
54
|
+
assert.equal(parseFfmpegVersion("garbage"), UNKNOWN);
|
|
55
|
+
const stream = Buffer.from("\0\0\x01\x06\x05x264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec", "latin1");
|
|
56
|
+
assert.equal(parseX264Version(stream), "core 164 r3108 31e19f9");
|
|
57
|
+
assert.equal(parseX264Version(Buffer.from("no version here")), UNKNOWN);
|
|
58
|
+
assert.deepEqual(parseNvidiaSmi("NVIDIA RTX 4000 SFF Ada Generation, 550.54.14\n"), {
|
|
59
|
+
model: "NVIDIA RTX 4000 SFF Ada Generation",
|
|
60
|
+
driver: "550.54.14"
|
|
61
|
+
});
|
|
62
|
+
assert.equal(kindOfEncoderName("h264_nvenc"), "nvenc");
|
|
63
|
+
assert.equal(kindOfEncoderName("libx264"), "software");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a driver update invalidates what a device was seen doing, and nothing about software", () => {
|
|
67
|
+
const vaapi = { name: "h264_vaapi", kind: "vaapi" };
|
|
68
|
+
const software = { name: "libx264", kind: "software" };
|
|
69
|
+
const updated = { ...FINGERPRINT, devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.8.0)" } } };
|
|
70
|
+
assert.notEqual(configurationKeyOf(FINGERPRINT, vaapi), configurationKeyOf(updated, vaapi));
|
|
71
|
+
assert.equal(configurationKeyOf(FINGERPRINT, software), configurationKeyOf(updated, software));
|
|
72
|
+
// Another x264 build or another thread count is another software configuration.
|
|
73
|
+
assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, threads: 14 }, software));
|
|
74
|
+
assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, x264: "core 165" }, software));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// --- Throughput by size ----------------------------------------------------
|
|
78
|
+
|
|
79
|
+
const READINGS = [
|
|
80
|
+
{ width: 256, height: 144, pixelsPerSec: 40e6 },
|
|
81
|
+
{ width: 640, height: 360, pixelsPerSec: 36e6 },
|
|
82
|
+
{ width: 1280, height: 720, pixelsPerSec: 30e6 }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
test("a size is priced from the readings around it, lowered by the error measured for interpolating", () => {
|
|
86
|
+
const error = interpolationErrorOf(READINGS);
|
|
87
|
+
assert.ok(error !== null && error >= 0, "three readings give a measured bound");
|
|
88
|
+
const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: error };
|
|
89
|
+
assert.equal(throughputAt(entry, { width: 640, height: 360 }), 36e6, "a size read is its own reading");
|
|
90
|
+
const between = throughputAt(entry, { width: 854, height: 480 });
|
|
91
|
+
assert.ok(between < 36e6 && between > 30e6 * (1 - error), `between the two readings, got ${between}`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a size beyond what was read is not a size this machine has shown it can do", () => {
|
|
95
|
+
const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 };
|
|
96
|
+
assert.equal(throughputAt(entry, { width: 1920, height: 1080 }), null);
|
|
97
|
+
assert.equal(throughputAt(entry, { width: 128, height: 72 }), null);
|
|
98
|
+
// Two readings measure no interpolation error: only the sizes read are known.
|
|
99
|
+
const two = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS.slice(0, 2), interpolationError: null };
|
|
100
|
+
assert.equal(throughputAt(two, { width: 426, height: 240 }), null);
|
|
101
|
+
assert.equal(throughputAt(two, { width: 640, height: 360 }), 36e6);
|
|
102
|
+
// An entry with no readings by size answers with its one figure, as before.
|
|
103
|
+
assert.equal(throughputAt({ preset: "fast", pixelsPerSec: 5e6 }, { width: 1920, height: 1080 }), 5e6);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("the offer does not keep a rung at a size no mode was measured at", () => {
|
|
107
|
+
const benchmark = [{ preset: "ultrafast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 }];
|
|
108
|
+
const at1080 = canSustainOutput({ benchmark, outputPixelsPerSec: 1920 * 1080 * 24, frame: { width: 1920, height: 1080 } });
|
|
109
|
+
assert.deepEqual(at1080, { speed: null, sustainable: false });
|
|
110
|
+
// The same mode at a size it WAS read at, with decoding priced: 30 Mpx/s
|
|
111
|
+
// against 1280x720x24 is 1.36x, and a decode at 10x leaves 1.19x.
|
|
112
|
+
const at720 = canSustainOutput({
|
|
113
|
+
benchmark,
|
|
114
|
+
outputPixelsPerSec: 1280 * 720 * 24,
|
|
115
|
+
frame: { width: 1280, height: 720 },
|
|
116
|
+
observedDecodeCostSec: 0.1
|
|
117
|
+
});
|
|
118
|
+
assert.equal(at720.sustainable, true);
|
|
119
|
+
assert.ok(Math.abs(at720.speed - 1 / (0.1 + (1280 * 720 * 24) / 30e6)) < 1e-9);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a preset not measured at a size is not chosen there", () => {
|
|
123
|
+
const benchmark = [
|
|
124
|
+
{ preset: "fast", pixelsPerSec: 20e6, bySize: READINGS.slice(0, 2), interpolationError: null },
|
|
125
|
+
{ preset: "ultrafast", pixelsPerSec: 60e6, bySize: READINGS, interpolationError: 0.05 }
|
|
126
|
+
];
|
|
127
|
+
assert.equal(pickSoftwarePreset(benchmark, 1280 * 720 * 24, {}, { width: 1280, height: 720 }), "ultrafast");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// --- The calibration walk --------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A calibration with every reading replaced: `speeds` maps "rung@WxH" to how
|
|
134
|
+
* many times realtime that mode encodes there, `failing` the rungs whose
|
|
135
|
+
* segments do not decode.
|
|
136
|
+
*/
|
|
137
|
+
async function calibrateWith({ encoder, speeds, failing = [] }) {
|
|
138
|
+
const asked = [];
|
|
139
|
+
const result = await calibrateEncoder({
|
|
140
|
+
ffmpegBin: "never-run",
|
|
141
|
+
encoder,
|
|
142
|
+
measure: {
|
|
143
|
+
check: async ({ rung }) => (failing.includes(rung)
|
|
144
|
+
? { ok: false, reason: "a segment does not decode", segmentKbps: null }
|
|
145
|
+
: { ok: true, reason: "", segmentKbps: { averageKbps: 900, peakKbps: 1400 } }),
|
|
146
|
+
rawFrames: async (frame) => `raw-${frame.width}x${frame.height}`,
|
|
147
|
+
speed: async (rung, rawPath, frame) => {
|
|
148
|
+
asked.push(`${rung}@${frame.width}x${frame.height}`);
|
|
149
|
+
return speeds[`${rung}@${frame.width}x${frame.height}`] ?? null;
|
|
150
|
+
},
|
|
151
|
+
release: () => {}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return { ...result, asked };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
test("a mode whose segments do not decode is not used at all", async () => {
|
|
158
|
+
const encoder = new SoftwareEncoder();
|
|
159
|
+
const speeds = {};
|
|
160
|
+
for (const rung of encoder.selectableRungs) {
|
|
161
|
+
for (const size of ["256x144", "640x360", "1280x720", "1920x1080", "3840x2160"]) {
|
|
162
|
+
speeds[`${rung}@${size}`] = 2;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const { modes, refused } = await calibrateWith({ encoder, speeds, failing: ["faster"] });
|
|
166
|
+
assert.ok(!modes.some((mode) => mode.preset === "faster"));
|
|
167
|
+
assert.ok(refused.some((one) => one.preset === "faster" && /decode/.test(one.reason)));
|
|
168
|
+
assert.equal(modes.length, encoder.selectableRungs.length - 1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("slower modes are not read where a faster one fell behind, nor larger sizes where the fastest did", async () => {
|
|
172
|
+
const encoder = new SoftwareEncoder();
|
|
173
|
+
const speeds = {
|
|
174
|
+
"ultrafast@256x144": 20, "superfast@256x144": 15, "veryfast@256x144": 10, "faster@256x144": 8, "fast@256x144": 6,
|
|
175
|
+
"ultrafast@640x360": 6, "superfast@640x360": 4, "veryfast@640x360": 0.8,
|
|
176
|
+
"ultrafast@1280x720": 0.9
|
|
177
|
+
};
|
|
178
|
+
const { modes, asked } = await calibrateWith({ encoder, speeds });
|
|
179
|
+
assert.ok(!asked.includes("faster@640x360"), "a mode slower than one that fell behind is not read");
|
|
180
|
+
assert.ok(!asked.some((one) => one.endsWith("@1920x1080")), "a size larger than one the fastest could not hold is not read");
|
|
181
|
+
const ultrafast = modes.find((mode) => mode.preset === "ultrafast");
|
|
182
|
+
assert.deepEqual(ultrafast.bySize.map((reading) => `${reading.width}x${reading.height}`), ["256x144", "640x360", "1280x720"]);
|
|
183
|
+
assert.equal(ultrafast.pixelsPerSec, 640 * 360 * 24 * 6, "the reference figure is the 640x360 reading");
|
|
184
|
+
const fast = modes.find((mode) => mode.preset === "fast");
|
|
185
|
+
assert.equal(throughputAt(fast, { width: 640, height: 360 }), null, "fast was never read at 640x360");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("a device is calibrated only at the one setting its arguments pass", () => {
|
|
189
|
+
assert.deepEqual(new NvencEncoder().selectableRungs, ["p4"]);
|
|
190
|
+
assert.ok(new NvencEncoder().buildVideoArgs({ targetWidth: 1280, targetHeight: 720, segmentDurationSec: 4 }).includes("p4"));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("the modes of the encoder in use are what prices, and software is kept for the fallback", () => {
|
|
194
|
+
const calibration = new HostCalibration({ byKind: { vaapi: [{ preset: "h264_vaapi", pixelsPerSec: 1 }], software: [] }, fingerprint: FINGERPRINT });
|
|
195
|
+
assert.equal(calibration.modesFor("vaapi").length, 1);
|
|
196
|
+
assert.deepEqual(calibration.modesFor("software"), [], "calibrated, nothing qualified");
|
|
197
|
+
assert.equal(calibration.modesFor("nvenc"), null, "not calibrated at all");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// --- Local observations ----------------------------------------------------
|
|
201
|
+
|
|
202
|
+
test("what is kept applies only on the configuration it was seen on, and survives a restart on it", () => {
|
|
203
|
+
const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
|
|
204
|
+
const filePath = path.join(directory, "local-observations.json");
|
|
205
|
+
try {
|
|
206
|
+
const first = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
207
|
+
const content = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 };
|
|
208
|
+
first.noteEncode({ spec: specAt(), content, aloneSpeedX: 1.7, segmentKbps: { averageKbps: 2100, peakKbps: 3900 } });
|
|
209
|
+
first.notePreparation({ spec: specAt(), seconds: 6.5, bufferedSec: 41 });
|
|
210
|
+
|
|
211
|
+
const software = configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" });
|
|
212
|
+
const again = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
213
|
+
again.load([software]);
|
|
214
|
+
assert.equal(again.slowestAloneSpeed(specAt(), content), 1.7);
|
|
215
|
+
assert.equal(again.peakMbps(specAt()), 3.9);
|
|
216
|
+
assert.equal(again.longestPreparationSec(specAt()), 6.5);
|
|
217
|
+
|
|
218
|
+
// Another libx264 build: nothing kept matches, and the file is rewritten without it.
|
|
219
|
+
const rebuilt = { ...FINGERPRINT, x264: "core 165 r3200 abc" };
|
|
220
|
+
const afterUpdate = new LocalObservations({ fingerprint: rebuilt, filePath });
|
|
221
|
+
afterUpdate.load([configurationKeyOf(rebuilt, { name: "libx264", kind: "software" })]);
|
|
222
|
+
assert.equal(afterUpdate.slowestAloneSpeed(specAt(), content), null);
|
|
223
|
+
assert.equal(afterUpdate.peakMbps(specAt()), null);
|
|
224
|
+
assert.equal(JSON.parse(readFileSync(filePath, "utf8")).entries.speed && Object.keys(JSON.parse(readFileSync(filePath, "utf8")).entries.speed).length, 0);
|
|
225
|
+
} finally {
|
|
226
|
+
rmSync(directory, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("a speed seen on easier material is not applied to harder material, and the slowest comparable one is used", () => {
|
|
231
|
+
const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath: "" });
|
|
232
|
+
const light = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 4 };
|
|
233
|
+
const heavy = { ...light, megabitsPerSecond: 12 };
|
|
234
|
+
observations.noteEncode({ spec: specAt(), content: light, aloneSpeedX: 2.4, segmentKbps: null });
|
|
235
|
+
assert.equal(observations.slowestAloneSpeed(specAt(), heavy), null, "a lighter source says nothing about a heavier one");
|
|
236
|
+
observations.noteEncode({ spec: specAt(), content: heavy, aloneSpeedX: 1.3, segmentKbps: null });
|
|
237
|
+
assert.equal(observations.slowestAloneSpeed(specAt(), light), 1.3, "both apply to the lighter source, and the slower wins");
|
|
238
|
+
assert.equal(observations.slowestAloneSpeed(specAt(), { ...light, codec: "h264" }), null, "another codec is not comparable");
|
|
239
|
+
assert.equal(observations.slowestAloneSpeed(specAt({ preset: "fast" }), light), null, "another mode is not this one");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("an encoder with no bound of its own takes the peak seen, as an estimate and never as a bound", () => {
|
|
243
|
+
const hardware = specAt({ encoder: "h264_vaapi", preset: null });
|
|
244
|
+
assert.equal(videoLoadOfSpec(hardware, null).peakClass, PEAK_CLASS.UNKNOWN);
|
|
245
|
+
const seen = videoLoadOfSpec(hardware, null, 5.2);
|
|
246
|
+
assert.equal(seen.peakClass, PEAK_CLASS.ESTIMATED);
|
|
247
|
+
assert.equal(seen.mbps, 5.2);
|
|
248
|
+
// A bound the encoder is held to is never replaced by what was seen.
|
|
249
|
+
const bounded = specAt({ rateControl: { maxrateKbps: 3900, bufsizeKbps: 4500, level: "3.1" } });
|
|
250
|
+
assert.equal(videoLoadOfSpec(bounded, null, 9.9).peakClass, PEAK_CLASS.KNOWN);
|
|
251
|
+
assert.equal(videoLoadOfSpec(bounded, null, 9.9).mbps, 3.9);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("a file of another shape is not read as observations", () => {
|
|
255
|
+
const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
|
|
256
|
+
const filePath = path.join(directory, "local-observations.json");
|
|
257
|
+
try {
|
|
258
|
+
writeFileSync(filePath, JSON.stringify({ version: 0, entries: { speed: { x: [1] } } }));
|
|
259
|
+
const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
260
|
+
observations.load([configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" })]);
|
|
261
|
+
assert.equal(observations.peakMbps(specAt()), null);
|
|
262
|
+
} finally {
|
|
263
|
+
rmSync(directory, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("what an encode was made from is described by the file's own facts", () => {
|
|
268
|
+
assert.deepEqual(
|
|
269
|
+
contentOf({ width: 1920, height: 1080, decode: { codec: "hevc", bitDepth: 10, megabitsPerSecond: 8, megapixelsPerSecond: 50 } }),
|
|
270
|
+
{ codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 }
|
|
271
|
+
);
|
|
272
|
+
assert.equal(contentOf({ width: 0, height: 0, decode: null }), null);
|
|
273
|
+
});
|