@torrent-tv/proxy 2.87.4 → 2.87.6
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 +8 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +94 -94
- 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/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +212 -212
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +36 -36
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2 -2
- 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/pool-adapter.js +1 -1
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/data-channel-handler.js +1356 -1356
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -73
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +176 -176
- 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 +35 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -0
- 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/{services/torrent → utils}/torrent-source-key.js +38 -38
|
@@ -1,322 +1,322 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which configuration a measurement of encoding was taken on (roadmap
|
|
3
|
-
* item 97, step 14).
|
|
4
|
-
*
|
|
5
|
-
* A figure about encoding is a figure about ONE configuration: this ffmpeg
|
|
6
|
-
* build, this libx264, this processor with this many threads, or this device
|
|
7
|
-
* with this driver. The same film encoded on another build or another driver
|
|
8
|
-
* is not the same measurement, and a figure kept across such a change would be
|
|
9
|
-
* applied to something it was never taken on.
|
|
10
|
-
*
|
|
11
|
-
* So every figure this proxy keeps between runs is filed under the key of the
|
|
12
|
-
* configuration it describes, and a figure whose key no longer matches is not
|
|
13
|
-
* used — not converted, not scaled, simply not matched.
|
|
14
|
-
*
|
|
15
|
-
* The key is per ENCODER, not per host. A host whose graphics driver changes
|
|
16
|
-
* keeps what it knows about its software encoder: that encoder has not
|
|
17
|
-
* changed, so nothing it was measured doing has changed either.
|
|
18
|
-
*
|
|
19
|
-
* WHAT IS READ, and where. None of it is chosen:
|
|
20
|
-
*
|
|
21
|
-
* 1. the ffmpeg build — the first line of `ffmpeg -version`;
|
|
22
|
-
* 2. the libx264 build — the version string x264 writes into every stream it
|
|
23
|
-
* produces (`x264 - core N rNNNN hash`), read from one encoded frame;
|
|
24
|
-
* 3. the processor model and the thread count the encoder is given
|
|
25
|
-
* (`CPU_THREADS`, which is the core count);
|
|
26
|
-
* 4. for a device-backed encoder, the device model and its driver: NVIDIA's
|
|
27
|
-
* own tool where it exists, and otherwise what the kernel publishes under
|
|
28
|
-
* `/sys` for the render node or the video device.
|
|
29
|
-
*
|
|
30
|
-
* A part that cannot be read is recorded as `unknown`. That is still a key:
|
|
31
|
-
* two runs that both could not read it are told apart by everything else, and
|
|
32
|
-
* a run that later can read it has a different key and starts afresh.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { spawn } from "node:child_process";
|
|
36
|
-
import { readdirSync, readFileSync, readlinkSync } from "node:fs";
|
|
37
|
-
import os from "node:os";
|
|
38
|
-
import path from "node:path";
|
|
39
|
-
import { CPU_THREADS } from "./args.js";
|
|
40
|
-
|
|
41
|
-
/** What an unreadable part of a fingerprint is written as. */
|
|
42
|
-
export const UNKNOWN = "unknown";
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The ffmpeg build, from the first line `ffmpeg -version` prints.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} text - Standard output of `ffmpeg -version`.
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
50
|
-
export function parseFfmpegVersion(text) {
|
|
51
|
-
const match = /^ffmpeg version (\S+)/m.exec(String(text ?? ""));
|
|
52
|
-
return match ? match[1] : UNKNOWN;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The libx264 build, from the text x264 writes into the stream it produces.
|
|
57
|
-
*
|
|
58
|
-
* @param {Buffer | string} bytes - An encoded H.264 stream.
|
|
59
|
-
* @returns {string}
|
|
60
|
-
*/
|
|
61
|
-
export function parseX264Version(bytes) {
|
|
62
|
-
const text = Buffer.isBuffer(bytes) ? bytes.toString("latin1") : String(bytes ?? "");
|
|
63
|
-
const match = /x264 - core (\d+)(?: (r\d+ [0-9a-f]+))?/.exec(text);
|
|
64
|
-
if (!match) {
|
|
65
|
-
return UNKNOWN;
|
|
66
|
-
}
|
|
67
|
-
return match[2] ? `core ${match[1]} ${match[2]}` : `core ${match[1]}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* NVIDIA's own answer about the device and its driver.
|
|
72
|
-
*
|
|
73
|
-
* @param {string} text - `nvidia-smi --query-gpu=name,driver_version --format=csv,noheader`.
|
|
74
|
-
* @returns {{ model: string, driver: string }}
|
|
75
|
-
*/
|
|
76
|
-
export function parseNvidiaSmi(text) {
|
|
77
|
-
const line = String(text ?? "").split(/\r?\n/).find((row) => row.includes(","));
|
|
78
|
-
if (!line) {
|
|
79
|
-
return { model: UNKNOWN, driver: UNKNOWN };
|
|
80
|
-
}
|
|
81
|
-
const [model, driver] = line.split(",").map((value) => value.trim());
|
|
82
|
-
return { model: model || UNKNOWN, driver: driver || UNKNOWN };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** ffmpeg's name for each encoder kind, as the kinds state it (`encode/*Encoder.js`). */
|
|
86
|
-
const KIND_BY_NAME = Object.freeze({
|
|
87
|
-
libx264: "software",
|
|
88
|
-
h264_nvenc: "nvenc",
|
|
89
|
-
h264_qsv: "qsv",
|
|
90
|
-
h264_vaapi: "vaapi",
|
|
91
|
-
h264_v4l2m2m: "v4l2m2m"
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Which kind an encoder of this name is, as an output's key names it.
|
|
96
|
-
*
|
|
97
|
-
* @param {string} name
|
|
98
|
-
* @returns {string}
|
|
99
|
-
*/
|
|
100
|
-
export function kindOfEncoderName(name) {
|
|
101
|
-
return KIND_BY_NAME[name] ?? UNKNOWN;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* The part of a fingerprint that decides whether a figure about THIS encoder
|
|
106
|
-
* still applies. Stable text, so it can be compared and stored.
|
|
107
|
-
*
|
|
108
|
-
* @param {HostFingerprint} fingerprint
|
|
109
|
-
* @param {{ name: string, kind: string }} encoder
|
|
110
|
-
* @returns {string}
|
|
111
|
-
*/
|
|
112
|
-
export function configurationKeyOf(fingerprint, encoder) {
|
|
113
|
-
const common = [`ffmpeg=${fingerprint?.ffmpeg ?? UNKNOWN}`, `encoder=${encoder?.name ?? UNKNOWN}`];
|
|
114
|
-
if (encoder?.kind === "software") {
|
|
115
|
-
return [
|
|
116
|
-
...common,
|
|
117
|
-
`x264=${fingerprint?.x264 ?? UNKNOWN}`,
|
|
118
|
-
`cpu=${fingerprint?.cpu ?? UNKNOWN}`,
|
|
119
|
-
`threads=${fingerprint?.threads ?? UNKNOWN}`
|
|
120
|
-
].join(";");
|
|
121
|
-
}
|
|
122
|
-
const device = fingerprint?.devices?.[encoder?.kind] ?? null;
|
|
123
|
-
return [
|
|
124
|
-
...common,
|
|
125
|
-
`device=${device?.model ?? UNKNOWN}`,
|
|
126
|
-
`driver=${device?.driver ?? UNKNOWN}`
|
|
127
|
-
].join(";");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* @typedef {object} DeviceFingerprint
|
|
132
|
-
* @property {string} model
|
|
133
|
-
* @property {string} driver
|
|
134
|
-
*
|
|
135
|
-
* @typedef {object} HostFingerprint
|
|
136
|
-
* @property {string} ffmpeg
|
|
137
|
-
* @property {string} x264
|
|
138
|
-
* @property {string} cpu
|
|
139
|
-
* @property {number} threads
|
|
140
|
-
* @property {Record<string, DeviceFingerprint>} devices - By encoder kind.
|
|
141
|
-
*/
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Run a program and hand back what it wrote to standard output.
|
|
145
|
-
*
|
|
146
|
-
* @param {string} command
|
|
147
|
-
* @param {string[]} args
|
|
148
|
-
* @param {number} timeoutMs
|
|
149
|
-
* @returns {Promise<Buffer | null>} Null when it could not be run or failed.
|
|
150
|
-
*/
|
|
151
|
-
function capture(command, args, timeoutMs) {
|
|
152
|
-
return new Promise((resolve) => {
|
|
153
|
-
/** @type {Buffer[]} */
|
|
154
|
-
const chunks = [];
|
|
155
|
-
let settled = false;
|
|
156
|
-
let child;
|
|
157
|
-
const finish = (value) => {
|
|
158
|
-
if (settled) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
settled = true;
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
resolve(value);
|
|
164
|
-
};
|
|
165
|
-
const timer = setTimeout(() => {
|
|
166
|
-
try {
|
|
167
|
-
child?.kill("SIGKILL");
|
|
168
|
-
} catch {
|
|
169
|
-
// already gone
|
|
170
|
-
}
|
|
171
|
-
finish(null);
|
|
172
|
-
}, timeoutMs);
|
|
173
|
-
try {
|
|
174
|
-
child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
175
|
-
} catch {
|
|
176
|
-
finish(null);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
180
|
-
child.on("error", () => finish(null));
|
|
181
|
-
child.on("close", (code) => finish(code === 0 ? Buffer.concat(chunks) : null));
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Read a small text file, or `unknown`.
|
|
187
|
-
*
|
|
188
|
-
* @param {string} filePath
|
|
189
|
-
* @returns {string}
|
|
190
|
-
*/
|
|
191
|
-
function readText(filePath) {
|
|
192
|
-
try {
|
|
193
|
-
const text = readFileSync(filePath, "utf8").trim();
|
|
194
|
-
return text.length > 0 ? text : UNKNOWN;
|
|
195
|
-
} catch {
|
|
196
|
-
return UNKNOWN;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* The name of the kernel driver bound to a device under `/sys`.
|
|
202
|
-
*
|
|
203
|
-
* @param {string} sysDevice - e.g. `/sys/class/drm/renderD128/device`.
|
|
204
|
-
* @returns {string}
|
|
205
|
-
*/
|
|
206
|
-
function driverOf(sysDevice) {
|
|
207
|
-
try {
|
|
208
|
-
return path.basename(readlinkSync(path.join(sysDevice, "driver")));
|
|
209
|
-
} catch {
|
|
210
|
-
return UNKNOWN;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* The version of a kernel driver. A driver built into the kernel publishes no
|
|
216
|
-
* version of its own, and then the kernel release IS its version.
|
|
217
|
-
*
|
|
218
|
-
* @param {string} driver
|
|
219
|
-
* @returns {string}
|
|
220
|
-
*/
|
|
221
|
-
function driverVersionOf(driver) {
|
|
222
|
-
if (driver === UNKNOWN) {
|
|
223
|
-
return UNKNOWN;
|
|
224
|
-
}
|
|
225
|
-
const own = readText(`/sys/module/${driver}/version`);
|
|
226
|
-
return own !== UNKNOWN ? `${driver} ${own}` : `${driver} (kernel ${os.release()})`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* A render node's device: the PCI vendor and device ids, and its driver.
|
|
231
|
-
*
|
|
232
|
-
* @param {string | null} renderNode - e.g. `/dev/dri/renderD128`.
|
|
233
|
-
* @returns {DeviceFingerprint}
|
|
234
|
-
*/
|
|
235
|
-
function renderNodeDevice(renderNode) {
|
|
236
|
-
if (!renderNode) {
|
|
237
|
-
return { model: UNKNOWN, driver: UNKNOWN };
|
|
238
|
-
}
|
|
239
|
-
const sysDevice = `/sys/class/drm/${path.basename(renderNode)}/device`;
|
|
240
|
-
const vendor = readText(path.join(sysDevice, "vendor"));
|
|
241
|
-
const device = readText(path.join(sysDevice, "device"));
|
|
242
|
-
const model = vendor === UNKNOWN && device === UNKNOWN ? UNKNOWN : `${vendor}:${device}`;
|
|
243
|
-
return { model, driver: driverVersionOf(driverOf(sysDevice)) };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* The memory-to-memory video devices, by the names the kernel gives them.
|
|
248
|
-
*
|
|
249
|
-
* @returns {DeviceFingerprint}
|
|
250
|
-
*/
|
|
251
|
-
function videoDevices() {
|
|
252
|
-
let names = [];
|
|
253
|
-
try {
|
|
254
|
-
names = readdirSync("/sys/class/video4linux")
|
|
255
|
-
.sort()
|
|
256
|
-
.map((entry) => readText(`/sys/class/video4linux/${entry}/name`))
|
|
257
|
-
.filter((name) => name !== UNKNOWN);
|
|
258
|
-
} catch {
|
|
259
|
-
names = [];
|
|
260
|
-
}
|
|
261
|
-
let driver = UNKNOWN;
|
|
262
|
-
try {
|
|
263
|
-
const first = readdirSync("/sys/class/video4linux").sort()[0];
|
|
264
|
-
driver = first ? driverVersionOf(driverOf(`/sys/class/video4linux/${first}/device`)) : UNKNOWN;
|
|
265
|
-
} catch {
|
|
266
|
-
driver = UNKNOWN;
|
|
267
|
-
}
|
|
268
|
-
return { model: names.length > 0 ? [...new Set(names)].join("+") : UNKNOWN, driver };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* NVIDIA's device and driver, from its own tool and otherwise from the file
|
|
273
|
-
* its kernel module publishes.
|
|
274
|
-
*
|
|
275
|
-
* @returns {Promise<DeviceFingerprint>}
|
|
276
|
-
*/
|
|
277
|
-
async function nvidiaDevice() {
|
|
278
|
-
const smi = await capture("nvidia-smi", ["--query-gpu=name,driver_version", "--format=csv,noheader"], 5000);
|
|
279
|
-
if (smi) {
|
|
280
|
-
return parseNvidiaSmi(smi.toString("utf8"));
|
|
281
|
-
}
|
|
282
|
-
const version = readText("/proc/driver/nvidia/version");
|
|
283
|
-
const driver = /Kernel Module\s+(\S+)/.exec(version)?.[1] ?? UNKNOWN;
|
|
284
|
-
return { model: UNKNOWN, driver };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Read this host's fingerprint. Always resolves: an unreadable part is
|
|
289
|
-
* `unknown`, never a refusal to start.
|
|
290
|
-
*
|
|
291
|
-
* @param {{ ffmpegBin: string, encoder?: { kind: string, device: string | null } | null }} params
|
|
292
|
-
* @returns {Promise<HostFingerprint>}
|
|
293
|
-
*/
|
|
294
|
-
export async function readHostFingerprint({ ffmpegBin, encoder = null }) {
|
|
295
|
-
const versionText = await capture(ffmpegBin, ["-hide_banner", "-version"], 5000);
|
|
296
|
-
// `-hide_banner` does not hide the version line itself: it is the answer.
|
|
297
|
-
const ffmpeg = parseFfmpegVersion(versionText ? versionText.toString("utf8") : "");
|
|
298
|
-
// One frame through libx264, written to standard output: the stream carries
|
|
299
|
-
// x264's own statement of its build.
|
|
300
|
-
const oneFrame = await capture(ffmpegBin, [
|
|
301
|
-
"-hide_banner", "-loglevel", "error",
|
|
302
|
-
"-f", "lavfi", "-i", "testsrc2=s=64x64:r=1:d=1",
|
|
303
|
-
"-frames:v", "1", "-c:v", "libx264", "-f", "h264", "-"
|
|
304
|
-
], 10000);
|
|
305
|
-
const x264 = oneFrame ? parseX264Version(oneFrame) : UNKNOWN;
|
|
306
|
-
/** @type {Record<string, DeviceFingerprint>} */
|
|
307
|
-
const devices = {};
|
|
308
|
-
if (encoder?.kind === "nvenc") {
|
|
309
|
-
devices.nvenc = await nvidiaDevice();
|
|
310
|
-
} else if (encoder?.kind === "vaapi" || encoder?.kind === "qsv") {
|
|
311
|
-
devices[encoder.kind] = renderNodeDevice(encoder.device);
|
|
312
|
-
} else if (encoder?.kind === "v4l2m2m") {
|
|
313
|
-
devices.v4l2m2m = videoDevices();
|
|
314
|
-
}
|
|
315
|
-
return {
|
|
316
|
-
ffmpeg,
|
|
317
|
-
x264,
|
|
318
|
-
cpu: os.cpus()[0]?.model?.trim() || UNKNOWN,
|
|
319
|
-
threads: CPU_THREADS,
|
|
320
|
-
devices
|
|
321
|
-
};
|
|
322
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Which configuration a measurement of encoding was taken on (roadmap
|
|
3
|
+
* item 97, step 14).
|
|
4
|
+
*
|
|
5
|
+
* A figure about encoding is a figure about ONE configuration: this ffmpeg
|
|
6
|
+
* build, this libx264, this processor with this many threads, or this device
|
|
7
|
+
* with this driver. The same film encoded on another build or another driver
|
|
8
|
+
* is not the same measurement, and a figure kept across such a change would be
|
|
9
|
+
* applied to something it was never taken on.
|
|
10
|
+
*
|
|
11
|
+
* So every figure this proxy keeps between runs is filed under the key of the
|
|
12
|
+
* configuration it describes, and a figure whose key no longer matches is not
|
|
13
|
+
* used — not converted, not scaled, simply not matched.
|
|
14
|
+
*
|
|
15
|
+
* The key is per ENCODER, not per host. A host whose graphics driver changes
|
|
16
|
+
* keeps what it knows about its software encoder: that encoder has not
|
|
17
|
+
* changed, so nothing it was measured doing has changed either.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IS READ, and where. None of it is chosen:
|
|
20
|
+
*
|
|
21
|
+
* 1. the ffmpeg build — the first line of `ffmpeg -version`;
|
|
22
|
+
* 2. the libx264 build — the version string x264 writes into every stream it
|
|
23
|
+
* produces (`x264 - core N rNNNN hash`), read from one encoded frame;
|
|
24
|
+
* 3. the processor model and the thread count the encoder is given
|
|
25
|
+
* (`CPU_THREADS`, which is the core count);
|
|
26
|
+
* 4. for a device-backed encoder, the device model and its driver: NVIDIA's
|
|
27
|
+
* own tool where it exists, and otherwise what the kernel publishes under
|
|
28
|
+
* `/sys` for the render node or the video device.
|
|
29
|
+
*
|
|
30
|
+
* A part that cannot be read is recorded as `unknown`. That is still a key:
|
|
31
|
+
* two runs that both could not read it are told apart by everything else, and
|
|
32
|
+
* a run that later can read it has a different key and starts afresh.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { spawn } from "node:child_process";
|
|
36
|
+
import { readdirSync, readFileSync, readlinkSync } from "node:fs";
|
|
37
|
+
import os from "node:os";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
import { CPU_THREADS } from "./args.js";
|
|
40
|
+
|
|
41
|
+
/** What an unreadable part of a fingerprint is written as. */
|
|
42
|
+
export const UNKNOWN = "unknown";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The ffmpeg build, from the first line `ffmpeg -version` prints.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} text - Standard output of `ffmpeg -version`.
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function parseFfmpegVersion(text) {
|
|
51
|
+
const match = /^ffmpeg version (\S+)/m.exec(String(text ?? ""));
|
|
52
|
+
return match ? match[1] : UNKNOWN;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The libx264 build, from the text x264 writes into the stream it produces.
|
|
57
|
+
*
|
|
58
|
+
* @param {Buffer | string} bytes - An encoded H.264 stream.
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function parseX264Version(bytes) {
|
|
62
|
+
const text = Buffer.isBuffer(bytes) ? bytes.toString("latin1") : String(bytes ?? "");
|
|
63
|
+
const match = /x264 - core (\d+)(?: (r\d+ [0-9a-f]+))?/.exec(text);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return UNKNOWN;
|
|
66
|
+
}
|
|
67
|
+
return match[2] ? `core ${match[1]} ${match[2]}` : `core ${match[1]}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* NVIDIA's own answer about the device and its driver.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} text - `nvidia-smi --query-gpu=name,driver_version --format=csv,noheader`.
|
|
74
|
+
* @returns {{ model: string, driver: string }}
|
|
75
|
+
*/
|
|
76
|
+
export function parseNvidiaSmi(text) {
|
|
77
|
+
const line = String(text ?? "").split(/\r?\n/).find((row) => row.includes(","));
|
|
78
|
+
if (!line) {
|
|
79
|
+
return { model: UNKNOWN, driver: UNKNOWN };
|
|
80
|
+
}
|
|
81
|
+
const [model, driver] = line.split(",").map((value) => value.trim());
|
|
82
|
+
return { model: model || UNKNOWN, driver: driver || UNKNOWN };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** ffmpeg's name for each encoder kind, as the kinds state it (`encode/*Encoder.js`). */
|
|
86
|
+
const KIND_BY_NAME = Object.freeze({
|
|
87
|
+
libx264: "software",
|
|
88
|
+
h264_nvenc: "nvenc",
|
|
89
|
+
h264_qsv: "qsv",
|
|
90
|
+
h264_vaapi: "vaapi",
|
|
91
|
+
h264_v4l2m2m: "v4l2m2m"
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Which kind an encoder of this name is, as an output's key names it.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} name
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
export function kindOfEncoderName(name) {
|
|
101
|
+
return KIND_BY_NAME[name] ?? UNKNOWN;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The part of a fingerprint that decides whether a figure about THIS encoder
|
|
106
|
+
* still applies. Stable text, so it can be compared and stored.
|
|
107
|
+
*
|
|
108
|
+
* @param {HostFingerprint} fingerprint
|
|
109
|
+
* @param {{ name: string, kind: string }} encoder
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
export function configurationKeyOf(fingerprint, encoder) {
|
|
113
|
+
const common = [`ffmpeg=${fingerprint?.ffmpeg ?? UNKNOWN}`, `encoder=${encoder?.name ?? UNKNOWN}`];
|
|
114
|
+
if (encoder?.kind === "software") {
|
|
115
|
+
return [
|
|
116
|
+
...common,
|
|
117
|
+
`x264=${fingerprint?.x264 ?? UNKNOWN}`,
|
|
118
|
+
`cpu=${fingerprint?.cpu ?? UNKNOWN}`,
|
|
119
|
+
`threads=${fingerprint?.threads ?? UNKNOWN}`
|
|
120
|
+
].join(";");
|
|
121
|
+
}
|
|
122
|
+
const device = fingerprint?.devices?.[encoder?.kind] ?? null;
|
|
123
|
+
return [
|
|
124
|
+
...common,
|
|
125
|
+
`device=${device?.model ?? UNKNOWN}`,
|
|
126
|
+
`driver=${device?.driver ?? UNKNOWN}`
|
|
127
|
+
].join(";");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @typedef {object} DeviceFingerprint
|
|
132
|
+
* @property {string} model
|
|
133
|
+
* @property {string} driver
|
|
134
|
+
*
|
|
135
|
+
* @typedef {object} HostFingerprint
|
|
136
|
+
* @property {string} ffmpeg
|
|
137
|
+
* @property {string} x264
|
|
138
|
+
* @property {string} cpu
|
|
139
|
+
* @property {number} threads
|
|
140
|
+
* @property {Record<string, DeviceFingerprint>} devices - By encoder kind.
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Run a program and hand back what it wrote to standard output.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} command
|
|
147
|
+
* @param {string[]} args
|
|
148
|
+
* @param {number} timeoutMs
|
|
149
|
+
* @returns {Promise<Buffer | null>} Null when it could not be run or failed.
|
|
150
|
+
*/
|
|
151
|
+
function capture(command, args, timeoutMs) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
/** @type {Buffer[]} */
|
|
154
|
+
const chunks = [];
|
|
155
|
+
let settled = false;
|
|
156
|
+
let child;
|
|
157
|
+
const finish = (value) => {
|
|
158
|
+
if (settled) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
settled = true;
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
resolve(value);
|
|
164
|
+
};
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
try {
|
|
167
|
+
child?.kill("SIGKILL");
|
|
168
|
+
} catch {
|
|
169
|
+
// already gone
|
|
170
|
+
}
|
|
171
|
+
finish(null);
|
|
172
|
+
}, timeoutMs);
|
|
173
|
+
try {
|
|
174
|
+
child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
175
|
+
} catch {
|
|
176
|
+
finish(null);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
180
|
+
child.on("error", () => finish(null));
|
|
181
|
+
child.on("close", (code) => finish(code === 0 ? Buffer.concat(chunks) : null));
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read a small text file, or `unknown`.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} filePath
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
function readText(filePath) {
|
|
192
|
+
try {
|
|
193
|
+
const text = readFileSync(filePath, "utf8").trim();
|
|
194
|
+
return text.length > 0 ? text : UNKNOWN;
|
|
195
|
+
} catch {
|
|
196
|
+
return UNKNOWN;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The name of the kernel driver bound to a device under `/sys`.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} sysDevice - e.g. `/sys/class/drm/renderD128/device`.
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
function driverOf(sysDevice) {
|
|
207
|
+
try {
|
|
208
|
+
return path.basename(readlinkSync(path.join(sysDevice, "driver")));
|
|
209
|
+
} catch {
|
|
210
|
+
return UNKNOWN;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The version of a kernel driver. A driver built into the kernel publishes no
|
|
216
|
+
* version of its own, and then the kernel release IS its version.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} driver
|
|
219
|
+
* @returns {string}
|
|
220
|
+
*/
|
|
221
|
+
function driverVersionOf(driver) {
|
|
222
|
+
if (driver === UNKNOWN) {
|
|
223
|
+
return UNKNOWN;
|
|
224
|
+
}
|
|
225
|
+
const own = readText(`/sys/module/${driver}/version`);
|
|
226
|
+
return own !== UNKNOWN ? `${driver} ${own}` : `${driver} (kernel ${os.release()})`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A render node's device: the PCI vendor and device ids, and its driver.
|
|
231
|
+
*
|
|
232
|
+
* @param {string | null} renderNode - e.g. `/dev/dri/renderD128`.
|
|
233
|
+
* @returns {DeviceFingerprint}
|
|
234
|
+
*/
|
|
235
|
+
function renderNodeDevice(renderNode) {
|
|
236
|
+
if (!renderNode) {
|
|
237
|
+
return { model: UNKNOWN, driver: UNKNOWN };
|
|
238
|
+
}
|
|
239
|
+
const sysDevice = `/sys/class/drm/${path.basename(renderNode)}/device`;
|
|
240
|
+
const vendor = readText(path.join(sysDevice, "vendor"));
|
|
241
|
+
const device = readText(path.join(sysDevice, "device"));
|
|
242
|
+
const model = vendor === UNKNOWN && device === UNKNOWN ? UNKNOWN : `${vendor}:${device}`;
|
|
243
|
+
return { model, driver: driverVersionOf(driverOf(sysDevice)) };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The memory-to-memory video devices, by the names the kernel gives them.
|
|
248
|
+
*
|
|
249
|
+
* @returns {DeviceFingerprint}
|
|
250
|
+
*/
|
|
251
|
+
function videoDevices() {
|
|
252
|
+
let names = [];
|
|
253
|
+
try {
|
|
254
|
+
names = readdirSync("/sys/class/video4linux")
|
|
255
|
+
.sort()
|
|
256
|
+
.map((entry) => readText(`/sys/class/video4linux/${entry}/name`))
|
|
257
|
+
.filter((name) => name !== UNKNOWN);
|
|
258
|
+
} catch {
|
|
259
|
+
names = [];
|
|
260
|
+
}
|
|
261
|
+
let driver = UNKNOWN;
|
|
262
|
+
try {
|
|
263
|
+
const first = readdirSync("/sys/class/video4linux").sort()[0];
|
|
264
|
+
driver = first ? driverVersionOf(driverOf(`/sys/class/video4linux/${first}/device`)) : UNKNOWN;
|
|
265
|
+
} catch {
|
|
266
|
+
driver = UNKNOWN;
|
|
267
|
+
}
|
|
268
|
+
return { model: names.length > 0 ? [...new Set(names)].join("+") : UNKNOWN, driver };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* NVIDIA's device and driver, from its own tool and otherwise from the file
|
|
273
|
+
* its kernel module publishes.
|
|
274
|
+
*
|
|
275
|
+
* @returns {Promise<DeviceFingerprint>}
|
|
276
|
+
*/
|
|
277
|
+
async function nvidiaDevice() {
|
|
278
|
+
const smi = await capture("nvidia-smi", ["--query-gpu=name,driver_version", "--format=csv,noheader"], 5000);
|
|
279
|
+
if (smi) {
|
|
280
|
+
return parseNvidiaSmi(smi.toString("utf8"));
|
|
281
|
+
}
|
|
282
|
+
const version = readText("/proc/driver/nvidia/version");
|
|
283
|
+
const driver = /Kernel Module\s+(\S+)/.exec(version)?.[1] ?? UNKNOWN;
|
|
284
|
+
return { model: UNKNOWN, driver };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Read this host's fingerprint. Always resolves: an unreadable part is
|
|
289
|
+
* `unknown`, never a refusal to start.
|
|
290
|
+
*
|
|
291
|
+
* @param {{ ffmpegBin: string, encoder?: { kind: string, device: string | null } | null }} params
|
|
292
|
+
* @returns {Promise<HostFingerprint>}
|
|
293
|
+
*/
|
|
294
|
+
export async function readHostFingerprint({ ffmpegBin, encoder = null }) {
|
|
295
|
+
const versionText = await capture(ffmpegBin, ["-hide_banner", "-version"], 5000);
|
|
296
|
+
// `-hide_banner` does not hide the version line itself: it is the answer.
|
|
297
|
+
const ffmpeg = parseFfmpegVersion(versionText ? versionText.toString("utf8") : "");
|
|
298
|
+
// One frame through libx264, written to standard output: the stream carries
|
|
299
|
+
// x264's own statement of its build.
|
|
300
|
+
const oneFrame = await capture(ffmpegBin, [
|
|
301
|
+
"-hide_banner", "-loglevel", "error",
|
|
302
|
+
"-f", "lavfi", "-i", "testsrc2=s=64x64:r=1:d=1",
|
|
303
|
+
"-frames:v", "1", "-c:v", "libx264", "-f", "h264", "-"
|
|
304
|
+
], 10000);
|
|
305
|
+
const x264 = oneFrame ? parseX264Version(oneFrame) : UNKNOWN;
|
|
306
|
+
/** @type {Record<string, DeviceFingerprint>} */
|
|
307
|
+
const devices = {};
|
|
308
|
+
if (encoder?.kind === "nvenc") {
|
|
309
|
+
devices.nvenc = await nvidiaDevice();
|
|
310
|
+
} else if (encoder?.kind === "vaapi" || encoder?.kind === "qsv") {
|
|
311
|
+
devices[encoder.kind] = renderNodeDevice(encoder.device);
|
|
312
|
+
} else if (encoder?.kind === "v4l2m2m") {
|
|
313
|
+
devices.v4l2m2m = videoDevices();
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
ffmpeg,
|
|
317
|
+
x264,
|
|
318
|
+
cpu: os.cpus()[0]?.model?.trim() || UNKNOWN,
|
|
319
|
+
threads: CPU_THREADS,
|
|
320
|
+
devices
|
|
321
|
+
};
|
|
322
|
+
}
|