@torrent-tv/proxy 2.88.3 → 2.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2259 -2250
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +23 -4
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1150 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -254
- package/services/encode/quality/output-format.js +319 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +298 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1244
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -0
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +995 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +70 -70
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -131
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/audio-frame-boundary.json +1 -1
- package/test/fmp4-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +270 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -0
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
package/services/encode/args.js
CHANGED
|
@@ -1,416 +1,418 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The arithmetic every encoder kind is built from: the output frame rate,
|
|
3
|
-
* the bitrate ladder and its caps, the frame box, and the keyframe arguments.
|
|
4
|
-
*
|
|
5
|
-
* Taken out of `encode/hwaccel.js` so the kind classes beside this file do not have to
|
|
6
|
-
* import the detection and benchmarking that happen to live there. The
|
|
7
|
-
* dependency runs one way — `encode/hwaccel.js` imports this, never the reverse — and
|
|
8
|
-
* everything here is a calculation, with no process, no filesystem and no clock
|
|
9
|
-
* behind it.
|
|
10
|
-
*
|
|
11
|
-
* Moved verbatim on 2026-09-04. Every comment is the reasoning it was written
|
|
12
|
-
* with and every field case it cites is unchanged.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import os from "node:os";
|
|
16
|
-
import { LADDER_HEIGHTS } from "./output/ladder.js";
|
|
17
|
-
|
|
18
|
-
export const SOFTWARE_PRESET = "ultrafast";
|
|
19
|
-
export const SOFTWARE_CRF = "24";
|
|
20
|
-
// HDR→SDR tone-map chain (software). Converts a BT.2020 PQ/HLG source to BT.709
|
|
21
|
-
// 8-bit SDR so the re-encode is not washed-out/desaturated. Requires the
|
|
22
|
-
// `zscale` (libzimg) and `tonemap` filters — gated by detectTonemapSupport;
|
|
23
|
-
// when unavailable the encode falls back to a plain 8-bit convert (no tonemap).
|
|
24
|
-
// npl=100 targets ~100-nit SDR; hable is a well-behaved tone-mapping operator.
|
|
25
|
-
export const TONEMAP_FILTER_CHAIN =
|
|
26
|
-
"zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709," +
|
|
27
|
-
"tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p";
|
|
28
|
-
// Default output frame rate when the source rate is unknown, and the rate used
|
|
29
|
-
// by the synthetic startup test-encode / preset benchmark. The real encode
|
|
30
|
-
// inherits the source rate (rounded to an integer, capped) — see
|
|
31
|
-
// chooseOutputFps — so 25/30 fps content no longer plays resampled to 24.
|
|
32
|
-
export const TRANSCODE_FPS = 24;
|
|
33
|
-
// Upper bound on the output frame rate: 50/60 fps sources are halved-in-effort
|
|
34
|
-
// by capping to 30, protecting the realtime encode budget on weak hosts.
|
|
35
|
-
export const MAX_OUTPUT_FPS = 30;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Choose an INTEGER output frame rate from the (possibly fractional) source
|
|
39
|
-
* rate, for the frame-count-GOP encoders ONLY (software libx264, v4l2m2m).
|
|
40
|
-
* Those place keyframes with `-g = segmentDur × fps` (frame count), so the
|
|
41
|
-
* `fps=` filter value must be an integer that makes seg×fps an exact whole
|
|
42
|
-
* number of frames per segment — otherwise segments drift off the synthetic
|
|
43
|
-
* playlist's uniform grid and seek accuracy degrades over a long file. Film
|
|
44
|
-
* rates (23.976) round to 24, 25 stays 25, 29.97 rounds to 30; the cap clamps
|
|
45
|
-
* high rates (the cap is a SPEED guard for the weak software/v4l2m2m path).
|
|
46
|
-
*
|
|
47
|
-
* Time-based-keyframe encoders (nvenc, vaapi, qsv) do NOT use this — they
|
|
48
|
-
* inherit the exact source rate untouched (their keyframes are forced by
|
|
49
|
-
* output time, so any rate segments correctly).
|
|
50
|
-
*
|
|
51
|
-
* @param {number | null | undefined} sourceFps
|
|
52
|
-
* @param {number} [cap=MAX_OUTPUT_FPS]
|
|
53
|
-
* @returns {number}
|
|
54
|
-
*/
|
|
55
|
-
export function chooseOutputFps(sourceFps, cap = MAX_OUTPUT_FPS) {
|
|
56
|
-
if (!Number.isFinite(sourceFps) || sourceFps <= 0) {
|
|
57
|
-
return TRANSCODE_FPS;
|
|
58
|
-
}
|
|
59
|
-
const rounded = Math.round(sourceFps);
|
|
60
|
-
if (rounded < 1) {
|
|
61
|
-
return TRANSCODE_FPS;
|
|
62
|
-
}
|
|
63
|
-
return Math.min(cap, rounded);
|
|
64
|
-
}
|
|
65
|
-
// Software x264 on weak ARM hosts is the transcode bottleneck — use all cores.
|
|
66
|
-
export const CPU_THREADS = Math.max(1, os.cpus().length);
|
|
67
|
-
|
|
68
|
-
// Bitrate caps (constrained CRF). CRF stays the quality driver; -maxrate/
|
|
69
|
-
// -bufsize only bound the peaks. Field evidence (iPhone on cellular,
|
|
70
|
-
// 2026-07-10): uncapped complex scenes produced 4 s segments of ~18 Mbit/s
|
|
71
|
-
// against a 1-6 Mbit/s viewer link — 45 s prebuffer, draining buffer.
|
|
72
|
-
// Nominal H.264 rates per row; multipliers from webtor's production ladder
|
|
73
|
-
// (content-transcoder): maxrate = 1.3x nominal, bufsize = 1.5x. The nominal
|
|
74
|
-
// figures themselves came with no source when they were written (2026-07-10,
|
|
75
|
-
// openspec `adaptive-bitrate`) and stand as an assumption until roadmap item
|
|
76
|
-
// 97, step 14 measures them.
|
|
77
|
-
const NOMINAL_KBPS_BY_ROW = new Map([
|
|
78
|
-
[1080, 5000],
|
|
79
|
-
[720, 2800],
|
|
80
|
-
[480, 1400],
|
|
81
|
-
[360, 800],
|
|
82
|
-
[240, 400]
|
|
83
|
-
]);
|
|
84
|
-
// The picture every row is named after: a 16:9 frame of the row's height, as
|
|
85
|
-
// the encode itself would size one (`computeOutputDimensions` from a 16:9
|
|
86
|
-
// source), so the reference is the frame the product makes and not a width
|
|
87
|
-
// written down beside it.
|
|
88
|
-
const ROW_REFERENCE_SOURCE = Object.freeze({ width: 3840, height: 2160 });
|
|
89
|
-
/**
|
|
90
|
-
* The rate a re-encoded soundtrack is
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
ROW_REFERENCE_SOURCE.width,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* @
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
["1
|
|
217
|
-
["1.
|
|
218
|
-
["2",
|
|
219
|
-
["
|
|
220
|
-
["2
|
|
221
|
-
["
|
|
222
|
-
["
|
|
223
|
-
["3
|
|
224
|
-
["
|
|
225
|
-
["
|
|
226
|
-
["4
|
|
227
|
-
["
|
|
228
|
-
["
|
|
229
|
-
["5
|
|
230
|
-
["
|
|
231
|
-
["
|
|
232
|
-
["6
|
|
233
|
-
]
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* the
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* the
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* @
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
* @
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* @
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* @param {number
|
|
399
|
-
* @param {number
|
|
400
|
-
* @
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
w
|
|
414
|
-
h
|
|
415
|
-
|
|
416
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file The arithmetic every encoder kind is built from: the output frame rate,
|
|
3
|
+
* the bitrate ladder and its caps, the frame box, and the keyframe arguments.
|
|
4
|
+
*
|
|
5
|
+
* Taken out of `encode/hwaccel.js` so the kind classes beside this file do not have to
|
|
6
|
+
* import the detection and benchmarking that happen to live there. The
|
|
7
|
+
* dependency runs one way — `encode/hwaccel.js` imports this, never the reverse — and
|
|
8
|
+
* everything here is a calculation, with no process, no filesystem and no clock
|
|
9
|
+
* behind it.
|
|
10
|
+
*
|
|
11
|
+
* Moved verbatim on 2026-09-04. Every comment is the reasoning it was written
|
|
12
|
+
* with and every field case it cites is unchanged.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import { LADDER_HEIGHTS } from "./output/ladder.js";
|
|
17
|
+
|
|
18
|
+
export const SOFTWARE_PRESET = "ultrafast";
|
|
19
|
+
export const SOFTWARE_CRF = "24";
|
|
20
|
+
// HDR→SDR tone-map chain (software). Converts a BT.2020 PQ/HLG source to BT.709
|
|
21
|
+
// 8-bit SDR so the re-encode is not washed-out/desaturated. Requires the
|
|
22
|
+
// `zscale` (libzimg) and `tonemap` filters — gated by detectTonemapSupport;
|
|
23
|
+
// when unavailable the encode falls back to a plain 8-bit convert (no tonemap).
|
|
24
|
+
// npl=100 targets ~100-nit SDR; hable is a well-behaved tone-mapping operator.
|
|
25
|
+
export const TONEMAP_FILTER_CHAIN =
|
|
26
|
+
"zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709," +
|
|
27
|
+
"tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p";
|
|
28
|
+
// Default output frame rate when the source rate is unknown, and the rate used
|
|
29
|
+
// by the synthetic startup test-encode / preset benchmark. The real encode
|
|
30
|
+
// inherits the source rate (rounded to an integer, capped) — see
|
|
31
|
+
// chooseOutputFps — so 25/30 fps content no longer plays resampled to 24.
|
|
32
|
+
export const TRANSCODE_FPS = 24;
|
|
33
|
+
// Upper bound on the output frame rate: 50/60 fps sources are halved-in-effort
|
|
34
|
+
// by capping to 30, protecting the realtime encode budget on weak hosts.
|
|
35
|
+
export const MAX_OUTPUT_FPS = 30;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Choose an INTEGER output frame rate from the (possibly fractional) source
|
|
39
|
+
* rate, for the frame-count-GOP encoders ONLY (software libx264, v4l2m2m).
|
|
40
|
+
* Those place keyframes with `-g = segmentDur × fps` (frame count), so the
|
|
41
|
+
* `fps=` filter value must be an integer that makes seg×fps an exact whole
|
|
42
|
+
* number of frames per segment — otherwise segments drift off the synthetic
|
|
43
|
+
* playlist's uniform grid and seek accuracy degrades over a long file. Film
|
|
44
|
+
* rates (23.976) round to 24, 25 stays 25, 29.97 rounds to 30; the cap clamps
|
|
45
|
+
* high rates (the cap is a SPEED guard for the weak software/v4l2m2m path).
|
|
46
|
+
*
|
|
47
|
+
* Time-based-keyframe encoders (nvenc, vaapi, qsv) do NOT use this — they
|
|
48
|
+
* inherit the exact source rate untouched (their keyframes are forced by
|
|
49
|
+
* output time, so any rate segments correctly).
|
|
50
|
+
*
|
|
51
|
+
* @param {number | null | undefined} sourceFps
|
|
52
|
+
* @param {number} [cap=MAX_OUTPUT_FPS]
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function chooseOutputFps(sourceFps, cap = MAX_OUTPUT_FPS) {
|
|
56
|
+
if (!Number.isFinite(sourceFps) || sourceFps <= 0) {
|
|
57
|
+
return TRANSCODE_FPS;
|
|
58
|
+
}
|
|
59
|
+
const rounded = Math.round(sourceFps);
|
|
60
|
+
if (rounded < 1) {
|
|
61
|
+
return TRANSCODE_FPS;
|
|
62
|
+
}
|
|
63
|
+
return Math.min(cap, rounded);
|
|
64
|
+
}
|
|
65
|
+
// Software x264 on weak ARM hosts is the transcode bottleneck — use all cores.
|
|
66
|
+
export const CPU_THREADS = Math.max(1, os.cpus().length);
|
|
67
|
+
|
|
68
|
+
// Bitrate caps (constrained CRF). CRF stays the quality driver; -maxrate/
|
|
69
|
+
// -bufsize only bound the peaks. Field evidence (iPhone on cellular,
|
|
70
|
+
// 2026-07-10): uncapped complex scenes produced 4 s segments of ~18 Mbit/s
|
|
71
|
+
// against a 1-6 Mbit/s viewer link — 45 s prebuffer, draining buffer.
|
|
72
|
+
// Nominal H.264 rates per row; multipliers from webtor's production ladder
|
|
73
|
+
// (content-transcoder): maxrate = 1.3x nominal, bufsize = 1.5x. The nominal
|
|
74
|
+
// figures themselves came with no source when they were written (2026-07-10,
|
|
75
|
+
// openspec `adaptive-bitrate`) and stand as an assumption until roadmap item
|
|
76
|
+
// 97, step 14 measures them.
|
|
77
|
+
const NOMINAL_KBPS_BY_ROW = new Map([
|
|
78
|
+
[1080, 5000],
|
|
79
|
+
[720, 2800],
|
|
80
|
+
[480, 1400],
|
|
81
|
+
[360, 800],
|
|
82
|
+
[240, 400]
|
|
83
|
+
]);
|
|
84
|
+
// The picture every row is named after: a 16:9 frame of the row's height, as
|
|
85
|
+
// the encode itself would size one (`computeOutputDimensions` from a 16:9
|
|
86
|
+
// source), so the reference is the frame the product makes and not a width
|
|
87
|
+
// written down beside it.
|
|
88
|
+
const ROW_REFERENCE_SOURCE = Object.freeze({ width: 3840, height: 2160 });
|
|
89
|
+
/**
|
|
90
|
+
* The rate a re-encoded soundtrack is asked for, in kbit/s: stereo AAC. A
|
|
91
|
+
* TARGET, not a bound — ffmpeg's AAC encoder lets single frames run above it
|
|
92
|
+
* from its bit reservoir. Stated once because two places need the same figure —
|
|
93
|
+
* the ffmpeg arguments that ask for it, and the viewer's link that has to carry
|
|
94
|
+
* it, which counts it as an estimate (`quality/link-budget.js`).
|
|
95
|
+
*/
|
|
96
|
+
export const AUDIO_TRANSCODE_KBPS = 128;
|
|
97
|
+
const CAP_MAXRATE_FACTOR = 1.3;
|
|
98
|
+
const CAP_BUFSIZE_FACTOR = 1.5;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The rows of limits, one per height of the ladder, each with the area of the
|
|
102
|
+
* frame it is named after.
|
|
103
|
+
*
|
|
104
|
+
* @type {{ height: number, area: number }[] | null}
|
|
105
|
+
*/
|
|
106
|
+
let limitRows = null;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @returns {{ height: number, area: number }[]}
|
|
110
|
+
*/
|
|
111
|
+
function rows() {
|
|
112
|
+
if (limitRows === null) {
|
|
113
|
+
limitRows = LADDER_HEIGHTS.map((height) => {
|
|
114
|
+
const frame = computeOutputDimensions(
|
|
115
|
+
ROW_REFERENCE_SOURCE.width,
|
|
116
|
+
height,
|
|
117
|
+
ROW_REFERENCE_SOURCE.width,
|
|
118
|
+
ROW_REFERENCE_SOURCE.height
|
|
119
|
+
);
|
|
120
|
+
return { height, area: frame.w * frame.h };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return limitRows;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The row whose frame area is nearest to this one, measured as a ratio rather
|
|
128
|
+
* than a difference (decided with the user 2026-09-24): a rate grows with the
|
|
129
|
+
* number of points multiplicatively, and the rows stand at uneven distances, so
|
|
130
|
+
* the border between two rows is the geometric mean of their areas. On the
|
|
131
|
+
* border itself the row with the larger area wins, so the choice has one answer.
|
|
132
|
+
*
|
|
133
|
+
* Keyed by area and not by height because a height alone names the wrong
|
|
134
|
+
* frame for any picture that is not 16:9: a 2.4:1 film in a 1080 box is
|
|
135
|
+
* encoded at about 1920x800, 1.54 million points, which a height puts in the
|
|
136
|
+
* 720 row (0.92 million) although it is nearer the 1080 row (2.07 million).
|
|
137
|
+
*
|
|
138
|
+
* @param {{ width: number, height: number }} frame
|
|
139
|
+
* @returns {number} The height the row is named after.
|
|
140
|
+
*/
|
|
141
|
+
export function limitRowFor({ width, height }) {
|
|
142
|
+
const area = Number(width) * Number(height);
|
|
143
|
+
if (!(Number.isFinite(area) && area > 0)) {
|
|
144
|
+
throw new RangeError(`a limit row is chosen for a frame, not for ${width}x${height}`);
|
|
145
|
+
}
|
|
146
|
+
return nearestByArea(rows(), area).height;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {{ height: number, area: number }[]} candidates
|
|
151
|
+
* @param {number} area
|
|
152
|
+
* @returns {{ height: number, area: number }}
|
|
153
|
+
*/
|
|
154
|
+
function nearestByArea(candidates, area) {
|
|
155
|
+
// The larger area over the smaller: the same order as the distance of the
|
|
156
|
+
// logarithms, without a logarithm. Taken as log(a / b) the two sides of a
|
|
157
|
+
// border come out one last digit apart — log(2/3) is not exactly -log(3/2) —
|
|
158
|
+
// and the tie rule below would never be reached.
|
|
159
|
+
const ratioTo = (rowArea) => Math.max(area, rowArea) / Math.min(area, rowArea);
|
|
160
|
+
let best = candidates[0];
|
|
161
|
+
let bestDistance = ratioTo(best.area);
|
|
162
|
+
for (const row of candidates.slice(1)) {
|
|
163
|
+
const distance = ratioTo(row.area);
|
|
164
|
+
if (distance < bestDistance || (distance === bestDistance && row.area > best.area)) {
|
|
165
|
+
best = row;
|
|
166
|
+
bestDistance = distance;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return best;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Nominal kbps for a frame: the nominal of the row its area falls in. A row
|
|
174
|
+
* that has no nominal of its own yet (540, 1440 and 2160 today) takes the one
|
|
175
|
+
* of the nearest row by area that has one, by the same rule, until step 14
|
|
176
|
+
* measures it.
|
|
177
|
+
*
|
|
178
|
+
* @param {{ width: number, height: number }} frame
|
|
179
|
+
* @returns {number}
|
|
180
|
+
*/
|
|
181
|
+
export function nominalKbpsFor(frame) {
|
|
182
|
+
const row = limitRowFor(frame);
|
|
183
|
+
const own = NOMINAL_KBPS_BY_ROW.get(row);
|
|
184
|
+
if (own !== undefined) {
|
|
185
|
+
return own;
|
|
186
|
+
}
|
|
187
|
+
const named = rows().filter((candidate) => NOMINAL_KBPS_BY_ROW.has(candidate.height));
|
|
188
|
+
const rowArea = rows().find((candidate) => candidate.height === row).area;
|
|
189
|
+
return NOMINAL_KBPS_BY_ROW.get(nearestByArea(named, rowArea).height);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The peak this encode may reach, in kbit/s, for a nominal rate.
|
|
194
|
+
*
|
|
195
|
+
* Exported because the same figure answers a second question: whether a rung
|
|
196
|
+
* fits the viewer's measured link. The budget compares the link against what
|
|
197
|
+
* the encode is ALLOWED to peak at rather than against what it happened to
|
|
198
|
+
* produce in the last few segments, so a rung is judged by the bound we impose
|
|
199
|
+
* on it and not by a quiet stretch of the film.
|
|
200
|
+
*
|
|
201
|
+
* @param {number} nominalKbps
|
|
202
|
+
* @returns {number}
|
|
203
|
+
*/
|
|
204
|
+
export function maxrateKbpsFor(nominalKbps) {
|
|
205
|
+
return Math.round(nominalKbps * CAP_MAXRATE_FACTOR);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* H.264 levels, Table A-1 of ITU-T H.264: the level name, then MaxMBPS
|
|
210
|
+
* (macroblocks per second), MaxFS (macroblocks per frame), MaxBR (kbit/s) and
|
|
211
|
+
* MaxCPB (kbit). The bit rate and buffer limits are the Baseline/Main figures;
|
|
212
|
+
* the High profile, which libx264 produces for 8-bit 4:2:0, allows 1.25 times
|
|
213
|
+
* both (Table A-2, `cpbBrVclFactor` 1250 against 1000).
|
|
214
|
+
*/
|
|
215
|
+
const H264_LEVELS = [
|
|
216
|
+
["1", 1485, 99, 64, 175],
|
|
217
|
+
["1.1", 3000, 396, 192, 500],
|
|
218
|
+
["1.2", 6000, 396, 384, 1000],
|
|
219
|
+
["1.3", 11880, 396, 768, 2000],
|
|
220
|
+
["2", 11880, 396, 2000, 2000],
|
|
221
|
+
["2.1", 19800, 792, 4000, 4000],
|
|
222
|
+
["2.2", 20250, 1620, 4000, 4000],
|
|
223
|
+
["3", 40500, 1620, 10000, 10000],
|
|
224
|
+
["3.1", 108000, 3600, 14000, 14000],
|
|
225
|
+
["3.2", 216000, 5120, 20000, 20000],
|
|
226
|
+
["4", 245760, 8192, 20000, 25000],
|
|
227
|
+
["4.1", 245760, 8192, 50000, 62500],
|
|
228
|
+
["4.2", 522240, 8704, 50000, 62500],
|
|
229
|
+
["5", 589824, 22080, 135000, 135000],
|
|
230
|
+
["5.1", 983040, 36864, 240000, 240000],
|
|
231
|
+
["5.2", 2073600, 36864, 240000, 240000],
|
|
232
|
+
["6", 4177920, 139264, 240000, 240000],
|
|
233
|
+
["6.1", 8355840, 139264, 480000, 480000],
|
|
234
|
+
["6.2", 16711680, 139264, 800000, 800000]
|
|
235
|
+
];
|
|
236
|
+
const HIGH_PROFILE_RATE_FACTOR = 1.25;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The lowest H.264 level whose limits hold this picture and this rate control,
|
|
240
|
+
* or null when none does.
|
|
241
|
+
*
|
|
242
|
+
* What the DPB needs is not checked here: it depends on the reference count,
|
|
243
|
+
* which the speed setting decides, and at the presets this proxy runs (one or
|
|
244
|
+
* two references) no size it produces reaches that limit. Whether this answer
|
|
245
|
+
* is the level x264 would pick by itself is checked before a release
|
|
246
|
+
* (`stand/segment-compat/level-check.mjs`), which is the check that would catch
|
|
247
|
+
* the DPB, or anything else, deciding differently.
|
|
248
|
+
*
|
|
249
|
+
* @param {{ width: number, height: number, fps: number, maxrateKbps: number, bufsizeKbps: number }} picture
|
|
250
|
+
* @returns {string | null}
|
|
251
|
+
*/
|
|
252
|
+
export function h264LevelFor({ width, height, fps, maxrateKbps, bufsizeKbps }) {
|
|
253
|
+
const frameMbs = Math.ceil(width / 16) * Math.ceil(height / 16);
|
|
254
|
+
for (const [name, maxMbps, maxFs, maxBr, maxCpb] of H264_LEVELS) {
|
|
255
|
+
if (
|
|
256
|
+
frameMbs <= maxFs &&
|
|
257
|
+
frameMbs * fps <= maxMbps &&
|
|
258
|
+
maxrateKbps <= maxBr * HIGH_PROFILE_RATE_FACTOR &&
|
|
259
|
+
bufsizeKbps <= maxCpb * HIGH_PROFILE_RATE_FACTOR
|
|
260
|
+
) {
|
|
261
|
+
return name;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The rate control of a software encode at this size, and the level it is
|
|
269
|
+
* declared at.
|
|
270
|
+
*
|
|
271
|
+
* THE LEVEL BELONGS TO THE NOMINAL OUTPUT OF THIS SIZE, not to the limit asked
|
|
272
|
+
* for (decided with the user 2026-09-23). A level is a ceiling a decoder must
|
|
273
|
+
* be able to handle, not a description of the stream, so a stream held below
|
|
274
|
+
* the nominal limit is correctly declared at the nominal output's level. What
|
|
275
|
+
* that buys is the whole point: every limit at one size then writes the same
|
|
276
|
+
* header, so moving a viewer between two of them is serving another output's
|
|
277
|
+
* pieces under the address they already play, with nothing asked of the
|
|
278
|
+
* player. Declared by the stream's own limit, a lower limit could land on a
|
|
279
|
+
* lower level, the header would differ, and the move would need a new address.
|
|
280
|
+
*
|
|
281
|
+
* A limit ABOVE the nominal one is refused rather than lowered: it would need a
|
|
282
|
+
* higher level than the one every other output of this size declares, and the
|
|
283
|
+
* request would then be answered with something other than what it asked for.
|
|
284
|
+
*
|
|
285
|
+
* @param {{ width: number, height: number, fps: number, capKbps?: number | null }} params
|
|
286
|
+
* `capKbps` is the nominal rate asked for; absent, the size's own.
|
|
287
|
+
* @returns {{ maxrateKbps: number, bufsizeKbps: number, level: string | null }}
|
|
288
|
+
*/
|
|
289
|
+
export function softwareRateControlFor({ width, height, fps, capKbps = null }) {
|
|
290
|
+
const nominal = nominalKbpsFor({ width, height });
|
|
291
|
+
const asked = capKbps === null || capKbps === undefined ? nominal : Number(capKbps);
|
|
292
|
+
if (!(Number.isFinite(asked) && asked > 0)) {
|
|
293
|
+
throw new RangeError(`a bitrate limit must be a positive number of kbit/s, not ${capKbps}`);
|
|
294
|
+
}
|
|
295
|
+
if (asked > nominal) {
|
|
296
|
+
throw new RangeError(
|
|
297
|
+
`a bitrate limit of ${asked}kbps is above the ${nominal}kbps a ${width}x${height} picture is sized for, ` +
|
|
298
|
+
"and would need a higher level than every other output of this size declares"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
maxrateKbps: maxrateKbpsFor(asked),
|
|
303
|
+
bufsizeKbps: bufsizeKbpsFor(asked),
|
|
304
|
+
level: h264LevelFor({
|
|
305
|
+
width,
|
|
306
|
+
height,
|
|
307
|
+
fps,
|
|
308
|
+
maxrateKbps: maxrateKbpsFor(nominal),
|
|
309
|
+
bufsizeKbps: bufsizeKbpsFor(nominal)
|
|
310
|
+
})
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The buffer an encode may fill, in kbit, for a nominal rate.
|
|
316
|
+
*
|
|
317
|
+
* @param {number} nominalKbps
|
|
318
|
+
* @returns {number}
|
|
319
|
+
*/
|
|
320
|
+
export function bufsizeKbpsFor(nominalKbps) {
|
|
321
|
+
return Math.round(nominalKbps * CAP_BUFSIZE_FACTOR);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* `-maxrate`/`-bufsize`/`-level` for an encode, exactly as its output states
|
|
326
|
+
* them (constrained CRF: CRF drives quality, these bound the peaks).
|
|
327
|
+
*
|
|
328
|
+
* Nothing is computed here: the figures are part of what the output IS, and
|
|
329
|
+
* its key names them, so the arguments are read off the output rather than
|
|
330
|
+
* worked out again beside it.
|
|
331
|
+
*
|
|
332
|
+
* @param {{ maxrateKbps: number, bufsizeKbps: number, level: string | null } | null} rateControl
|
|
333
|
+
* @returns {string[]}
|
|
334
|
+
*/
|
|
335
|
+
export function rateControlArgs(rateControl) {
|
|
336
|
+
if (!rateControl) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
return [
|
|
340
|
+
"-maxrate", `${rateControl.maxrateKbps}k`,
|
|
341
|
+
"-bufsize", `${rateControl.bufsizeKbps}k`,
|
|
342
|
+
...(rateControl.level ? ["-level:v", rateControl.level] : [])
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @param {number} targetWidth
|
|
348
|
+
* @param {number} targetHeight
|
|
349
|
+
* @returns {{ w: number, h: number }}
|
|
350
|
+
*/
|
|
351
|
+
export function safeDimensions(targetWidth, targetHeight) {
|
|
352
|
+
const w = Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : 1280;
|
|
353
|
+
const h = Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : 720;
|
|
354
|
+
return { w, h };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Force a keyframe on every segment boundary so each HLS segment is
|
|
359
|
+
* independently decodable.
|
|
360
|
+
*
|
|
361
|
+
* Two grids exist. The usual one is even — a keyframe every
|
|
362
|
+
* `segmentDurationSec` — and the encoder is free to place them because it is
|
|
363
|
+
* producing every frame anyway. The other is the SOURCE's own keyframe times,
|
|
364
|
+
* used when this encode has to be interchangeable with a stream that is
|
|
365
|
+
* COPIED: a copy can only be cut where the source already has a keyframe, so a
|
|
366
|
+
* rung meant to splice into it must be cut at exactly those times and nowhere
|
|
367
|
+
* else. Then the times are given outright.
|
|
368
|
+
*
|
|
369
|
+
* @param {number} segmentDurationSec
|
|
370
|
+
* @param {number[] | null} [forcedTimes] - Run-relative seconds, ascending.
|
|
371
|
+
* @returns {string[]}
|
|
372
|
+
*/
|
|
373
|
+
export function keyFrameArgs(segmentDurationSec, forcedTimes = null) {
|
|
374
|
+
if (Array.isArray(forcedTimes) && forcedTimes.length > 0) {
|
|
375
|
+
return ["-force_key_frames", forcedTimes.join(",")];
|
|
376
|
+
}
|
|
377
|
+
return ["-force_key_frames", `expr:gte(t,n_forced*${segmentDurationSec})`];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Whether an explicit cut list was supplied.
|
|
382
|
+
*
|
|
383
|
+
* @param {number[] | null | undefined} forcedTimes
|
|
384
|
+
* @returns {boolean}
|
|
385
|
+
*/
|
|
386
|
+
export function hasForcedTimes(forcedTimes) {
|
|
387
|
+
return Array.isArray(forcedTimes) && forcedTimes.length > 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Compute the actual output resolution ffmpeg will produce: the target box
|
|
392
|
+
* capped to the source (never upscaled), preserving aspect, divisible by 2.
|
|
393
|
+
* Mirrors the `scale='min(w,iw)':'min(h,ih)':force_original_aspect_ratio=decrease`
|
|
394
|
+
* filter built below, which is why it lives beside it: two statements of one
|
|
395
|
+
* rule, and the day they disagree the quality offer prices a picture ffmpeg is
|
|
396
|
+
* not making. Returns `null` when the source size is unknown.
|
|
397
|
+
*
|
|
398
|
+
* @param {number} targetWidth
|
|
399
|
+
* @param {number} targetHeight
|
|
400
|
+
* @param {number | null} sourceWidth
|
|
401
|
+
* @param {number | null} sourceHeight
|
|
402
|
+
* @returns {{ w: number, h: number } | null}
|
|
403
|
+
*/
|
|
404
|
+
export function computeOutputDimensions(targetWidth, targetHeight, sourceWidth, sourceHeight) {
|
|
405
|
+
const sw = Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : 0;
|
|
406
|
+
const sh = Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : 0;
|
|
407
|
+
if (!sw || !sh) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const tw = Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : sw;
|
|
411
|
+
const th = Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : sh;
|
|
412
|
+
const scale = Math.min(tw / sw, th / sh, 1);
|
|
413
|
+
let w = Math.round(sw * scale);
|
|
414
|
+
let h = Math.round(sh * scale);
|
|
415
|
+
w -= w % 2;
|
|
416
|
+
h -= h % 2;
|
|
417
|
+
return { w: Math.max(2, w), h: Math.max(2, h) };
|
|
418
|
+
}
|