@torrent-tv/proxy 2.87.4 → 2.87.5
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 +4 -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-source-key.js +38 -38
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/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
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Progress reported by one encoder run, placed on the source timeline.
|
|
3
|
-
*
|
|
4
|
-
* ffmpeg reports time relative to the start of each run on both the re-encode
|
|
5
|
-
* and copy branches. `-output_ts_offset` and `-copyts` change muxed timestamps,
|
|
6
|
-
* not the `-progress` clock. Every reading is therefore rebased by this run's
|
|
7
|
-
* own start, never by an output-wide value shared with another run.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
function parseTimestamp(value) {
|
|
11
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
const parts = value.split(":");
|
|
15
|
-
if (parts.length !== 3) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
const [hours, minutes, seconds] = parts.map(Number);
|
|
19
|
-
return [hours, minutes, seconds].every(Number.isFinite)
|
|
20
|
-
? hours * 3600 + minutes * 60 + seconds
|
|
21
|
-
: null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class RunProgress {
|
|
25
|
-
constructor({ startSeconds = 0, totalSeconds = null, now = Date.now } = {}) {
|
|
26
|
-
this.startPositionSeconds = Number.isFinite(startSeconds) ? Math.max(0, startSeconds) : 0;
|
|
27
|
-
this.processedSeconds = this.startPositionSeconds;
|
|
28
|
-
this.totalSeconds = Number.isFinite(totalSeconds) && totalSeconds > 0 ? totalSeconds : null;
|
|
29
|
-
this.percent = null;
|
|
30
|
-
this.remainingSeconds = this.totalSeconds;
|
|
31
|
-
this.speed = "";
|
|
32
|
-
this.updatedAt = now();
|
|
33
|
-
this.lastLoggedAt = 0;
|
|
34
|
-
this.now = now;
|
|
35
|
-
this.#derive();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
note({ processedSeconds = null, outTime, speed = null }) {
|
|
39
|
-
const relative = Number.isFinite(processedSeconds)
|
|
40
|
-
? processedSeconds
|
|
41
|
-
: parseTimestamp(outTime);
|
|
42
|
-
if (relative !== null) {
|
|
43
|
-
this.processedSeconds = this.startPositionSeconds + Math.max(0, relative);
|
|
44
|
-
}
|
|
45
|
-
if (typeof speed === "string") {
|
|
46
|
-
this.speed = speed;
|
|
47
|
-
}
|
|
48
|
-
this.updatedAt = this.now();
|
|
49
|
-
this.#derive();
|
|
50
|
-
return this.snapshot();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
shouldLog(everyMs) {
|
|
54
|
-
if (this.percent === null || this.updatedAt - this.lastLoggedAt < everyMs) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
this.lastLoggedAt = this.updatedAt;
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
snapshot() {
|
|
62
|
-
return {
|
|
63
|
-
processedSeconds: this.processedSeconds,
|
|
64
|
-
startPositionSeconds: this.startPositionSeconds,
|
|
65
|
-
totalSeconds: this.totalSeconds,
|
|
66
|
-
percent: this.percent,
|
|
67
|
-
remainingSeconds: this.remainingSeconds,
|
|
68
|
-
speed: this.speed,
|
|
69
|
-
updatedAt: this.updatedAt
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
#derive() {
|
|
74
|
-
if (this.totalSeconds === null) {
|
|
75
|
-
this.percent = null;
|
|
76
|
-
this.remainingSeconds = null;
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const span = Math.max(1, this.totalSeconds - this.startPositionSeconds);
|
|
80
|
-
const processed = Math.max(0, this.processedSeconds - this.startPositionSeconds);
|
|
81
|
-
this.percent = Math.max(0, Math.min(100, (processed / span) * 100));
|
|
82
|
-
this.remainingSeconds = Math.max(0, this.totalSeconds - this.processedSeconds);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Progress reported by one encoder run, placed on the source timeline.
|
|
3
|
+
*
|
|
4
|
+
* ffmpeg reports time relative to the start of each run on both the re-encode
|
|
5
|
+
* and copy branches. `-output_ts_offset` and `-copyts` change muxed timestamps,
|
|
6
|
+
* not the `-progress` clock. Every reading is therefore rebased by this run's
|
|
7
|
+
* own start, never by an output-wide value shared with another run.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function parseTimestamp(value) {
|
|
11
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const parts = value.split(":");
|
|
15
|
+
if (parts.length !== 3) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const [hours, minutes, seconds] = parts.map(Number);
|
|
19
|
+
return [hours, minutes, seconds].every(Number.isFinite)
|
|
20
|
+
? hours * 3600 + minutes * 60 + seconds
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class RunProgress {
|
|
25
|
+
constructor({ startSeconds = 0, totalSeconds = null, now = Date.now } = {}) {
|
|
26
|
+
this.startPositionSeconds = Number.isFinite(startSeconds) ? Math.max(0, startSeconds) : 0;
|
|
27
|
+
this.processedSeconds = this.startPositionSeconds;
|
|
28
|
+
this.totalSeconds = Number.isFinite(totalSeconds) && totalSeconds > 0 ? totalSeconds : null;
|
|
29
|
+
this.percent = null;
|
|
30
|
+
this.remainingSeconds = this.totalSeconds;
|
|
31
|
+
this.speed = "";
|
|
32
|
+
this.updatedAt = now();
|
|
33
|
+
this.lastLoggedAt = 0;
|
|
34
|
+
this.now = now;
|
|
35
|
+
this.#derive();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
note({ processedSeconds = null, outTime, speed = null }) {
|
|
39
|
+
const relative = Number.isFinite(processedSeconds)
|
|
40
|
+
? processedSeconds
|
|
41
|
+
: parseTimestamp(outTime);
|
|
42
|
+
if (relative !== null) {
|
|
43
|
+
this.processedSeconds = this.startPositionSeconds + Math.max(0, relative);
|
|
44
|
+
}
|
|
45
|
+
if (typeof speed === "string") {
|
|
46
|
+
this.speed = speed;
|
|
47
|
+
}
|
|
48
|
+
this.updatedAt = this.now();
|
|
49
|
+
this.#derive();
|
|
50
|
+
return this.snapshot();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
shouldLog(everyMs) {
|
|
54
|
+
if (this.percent === null || this.updatedAt - this.lastLoggedAt < everyMs) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
this.lastLoggedAt = this.updatedAt;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
snapshot() {
|
|
62
|
+
return {
|
|
63
|
+
processedSeconds: this.processedSeconds,
|
|
64
|
+
startPositionSeconds: this.startPositionSeconds,
|
|
65
|
+
totalSeconds: this.totalSeconds,
|
|
66
|
+
percent: this.percent,
|
|
67
|
+
remainingSeconds: this.remainingSeconds,
|
|
68
|
+
speed: this.speed,
|
|
69
|
+
updatedAt: this.updatedAt
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#derive() {
|
|
74
|
+
if (this.totalSeconds === null) {
|
|
75
|
+
this.percent = null;
|
|
76
|
+
this.remainingSeconds = null;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const span = Math.max(1, this.totalSeconds - this.startPositionSeconds);
|
|
80
|
+
const processed = Math.max(0, this.processedSeconds - this.startPositionSeconds);
|
|
81
|
+
this.percent = Math.max(0, Math.min(100, (processed / span) * 100));
|
|
82
|
+
this.remainingSeconds = Math.max(0, this.totalSeconds - this.processedSeconds);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What is wanted of one output, in ITS OWN segment numbers.
|
|
3
|
-
*
|
|
4
|
-
* One map per output and nothing else. The map itself is built once, per film,
|
|
5
|
-
* by the layer that knows where the viewers are; this holds the copy converted
|
|
6
|
-
* into an output's own numbering, because two outputs of one film are cut
|
|
7
|
-
* independently — 454 pieces against 401 on the field file — so the same second
|
|
8
|
-
* is a different number in each.
|
|
9
|
-
*
|
|
10
|
-
* **No viewer appears here.** It used to hold a window per viewer per band and
|
|
11
|
-
* merge them itself, which was the priority layer's work done a second time in
|
|
12
|
-
* the wrong place, and it carried the viewer's name as the key of a claim —
|
|
13
|
-
* against the rule that the encoding and the viewer are not connected at all.
|
|
14
|
-
* What arrives now is already merged and already anonymous.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export class SegmentDemand {
|
|
18
|
-
/** Output address to what is wanted of it. @type {Map<string, object[]>} */
|
|
19
|
-
#maps = new Map();
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* What is wanted of one output, replacing whatever was wanted before.
|
|
23
|
-
*
|
|
24
|
-
* An EMPTY map is a statement and is stored as one: it says nobody is coming
|
|
25
|
-
* anywhere in this output, which is what stops the encoders on it.
|
|
26
|
-
*
|
|
27
|
-
* @param {string} address
|
|
28
|
-
* @param {{ from: number, to: number, priority: number, withinSeconds: number }[]} zones
|
|
29
|
-
*/
|
|
30
|
-
state(address, zones) {
|
|
31
|
-
this.#maps.set(address, Array.isArray(zones) ? zones : []);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* An output nobody has anything to say about any more.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} address
|
|
38
|
-
*/
|
|
39
|
-
forget(address) {
|
|
40
|
-
this.#maps.delete(address);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Every output anything is wanted of.
|
|
45
|
-
*
|
|
46
|
-
* @returns {string[]}
|
|
47
|
-
*/
|
|
48
|
-
addresses() {
|
|
49
|
-
return [...this.#maps.keys()];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* What is wanted of one output.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} address
|
|
56
|
-
* @returns {{ from: number, to: number, priority: number, withinSeconds: number }[]}
|
|
57
|
-
*/
|
|
58
|
-
mapOn(address) {
|
|
59
|
-
return this.#maps.get(address) ?? [];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* What the map says about one segment, and about the map it sits in.
|
|
64
|
-
*
|
|
65
|
-
* Asked by whoever MEASURES how well the map is being served: a wait means
|
|
66
|
-
* one thing at the top rank and another at the bottom, and telling them apart
|
|
67
|
-
* needs both the segment's own rank and the highest rank stated — a rank is a
|
|
68
|
-
* position in this map, not an absolute number.
|
|
69
|
-
*
|
|
70
|
-
* AND AN EMPTY MAP IS NOT AN ANSWER, which is why the top rank comes back even
|
|
71
|
-
* when the segment's own is zero. A top rank of zero says the map has not been
|
|
72
|
-
* built yet — a session created a moment ago, before the first pass — and that
|
|
73
|
-
* is a different thing from "nobody is coming". Conflated, every request
|
|
74
|
-
* behind the run reads as absent for as long as a fresh session has no map.
|
|
75
|
-
*
|
|
76
|
-
* @param {string} address
|
|
77
|
-
* @param {number} index
|
|
78
|
-
* @returns {{ rank: number, topRank: number }} Rank zero for a number in
|
|
79
|
-
* nobody's zone, which is a statement: nothing is coming for it.
|
|
80
|
-
*/
|
|
81
|
-
rankOf(address, index) {
|
|
82
|
-
const zones = this.mapOn(address);
|
|
83
|
-
let rank = 0;
|
|
84
|
-
let topRank = 0;
|
|
85
|
-
for (const zone of zones) {
|
|
86
|
-
if (zone.priority > topRank) {
|
|
87
|
-
topRank = zone.priority;
|
|
88
|
-
}
|
|
89
|
-
if (index >= zone.from && index <= zone.to && zone.priority > rank) {
|
|
90
|
-
rank = zone.priority;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { rank, topRank };
|
|
94
|
-
}
|
|
95
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file What is wanted of one output, in ITS OWN segment numbers.
|
|
3
|
+
*
|
|
4
|
+
* One map per output and nothing else. The map itself is built once, per film,
|
|
5
|
+
* by the layer that knows where the viewers are; this holds the copy converted
|
|
6
|
+
* into an output's own numbering, because two outputs of one film are cut
|
|
7
|
+
* independently — 454 pieces against 401 on the field file — so the same second
|
|
8
|
+
* is a different number in each.
|
|
9
|
+
*
|
|
10
|
+
* **No viewer appears here.** It used to hold a window per viewer per band and
|
|
11
|
+
* merge them itself, which was the priority layer's work done a second time in
|
|
12
|
+
* the wrong place, and it carried the viewer's name as the key of a claim —
|
|
13
|
+
* against the rule that the encoding and the viewer are not connected at all.
|
|
14
|
+
* What arrives now is already merged and already anonymous.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class SegmentDemand {
|
|
18
|
+
/** Output address to what is wanted of it. @type {Map<string, object[]>} */
|
|
19
|
+
#maps = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What is wanted of one output, replacing whatever was wanted before.
|
|
23
|
+
*
|
|
24
|
+
* An EMPTY map is a statement and is stored as one: it says nobody is coming
|
|
25
|
+
* anywhere in this output, which is what stops the encoders on it.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} address
|
|
28
|
+
* @param {{ from: number, to: number, priority: number, withinSeconds: number }[]} zones
|
|
29
|
+
*/
|
|
30
|
+
state(address, zones) {
|
|
31
|
+
this.#maps.set(address, Array.isArray(zones) ? zones : []);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* An output nobody has anything to say about any more.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} address
|
|
38
|
+
*/
|
|
39
|
+
forget(address) {
|
|
40
|
+
this.#maps.delete(address);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every output anything is wanted of.
|
|
45
|
+
*
|
|
46
|
+
* @returns {string[]}
|
|
47
|
+
*/
|
|
48
|
+
addresses() {
|
|
49
|
+
return [...this.#maps.keys()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What is wanted of one output.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} address
|
|
56
|
+
* @returns {{ from: number, to: number, priority: number, withinSeconds: number }[]}
|
|
57
|
+
*/
|
|
58
|
+
mapOn(address) {
|
|
59
|
+
return this.#maps.get(address) ?? [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What the map says about one segment, and about the map it sits in.
|
|
64
|
+
*
|
|
65
|
+
* Asked by whoever MEASURES how well the map is being served: a wait means
|
|
66
|
+
* one thing at the top rank and another at the bottom, and telling them apart
|
|
67
|
+
* needs both the segment's own rank and the highest rank stated — a rank is a
|
|
68
|
+
* position in this map, not an absolute number.
|
|
69
|
+
*
|
|
70
|
+
* AND AN EMPTY MAP IS NOT AN ANSWER, which is why the top rank comes back even
|
|
71
|
+
* when the segment's own is zero. A top rank of zero says the map has not been
|
|
72
|
+
* built yet — a session created a moment ago, before the first pass — and that
|
|
73
|
+
* is a different thing from "nobody is coming". Conflated, every request
|
|
74
|
+
* behind the run reads as absent for as long as a fresh session has no map.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} address
|
|
77
|
+
* @param {number} index
|
|
78
|
+
* @returns {{ rank: number, topRank: number }} Rank zero for a number in
|
|
79
|
+
* nobody's zone, which is a statement: nothing is coming for it.
|
|
80
|
+
*/
|
|
81
|
+
rankOf(address, index) {
|
|
82
|
+
const zones = this.mapOn(address);
|
|
83
|
+
let rank = 0;
|
|
84
|
+
let topRank = 0;
|
|
85
|
+
for (const zone of zones) {
|
|
86
|
+
if (zone.priority > topRank) {
|
|
87
|
+
topRank = zone.priority;
|
|
88
|
+
}
|
|
89
|
+
if (index >= zone.from && index <= zone.to && zone.priority > rank) {
|
|
90
|
+
rank = zone.priority;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { rank, topRank };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file libx264 — the encoder every host has and the only one whose ladder of
|
|
3
|
-
* speed settings is measured at startup.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Encoder } from "./Encoder.js";
|
|
7
|
-
import {
|
|
8
|
-
CPU_THREADS,
|
|
9
|
-
hasForcedTimes,
|
|
10
|
-
keyFrameArgs,
|
|
11
|
-
rateControlArgs,
|
|
12
|
-
safeDimensions,
|
|
13
|
-
SOFTWARE_CRF,
|
|
14
|
-
SOFTWARE_PRESET,
|
|
15
|
-
TONEMAP_FILTER_CHAIN,
|
|
16
|
-
TRANSCODE_FPS
|
|
17
|
-
} from "./args.js";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The presets the startup benchmark walks, slowest first.
|
|
21
|
-
*
|
|
22
|
-
* The same list `benchmarkSoftwarePresets` measures and `pickSoftwarePreset`
|
|
23
|
-
* chooses from; it is stated here because it is a property of this kind, and
|
|
24
|
-
* the four hardware kinds beside it show what its absence looks like.
|
|
25
|
-
*/
|
|
26
|
-
const PRESETS = ["fast", "faster", "veryfast", "superfast", "ultrafast"];
|
|
27
|
-
|
|
28
|
-
export class SoftwareEncoder extends Encoder {
|
|
29
|
-
constructor() {
|
|
30
|
-
super({ name: "libx264", kind: "software", device: null, inputArgs: [] });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** @returns {import("./Encoder.js").SpeedLadder} */
|
|
34
|
-
get speedLadder() {
|
|
35
|
-
return {
|
|
36
|
-
flag: "-preset",
|
|
37
|
-
values: PRESETS,
|
|
38
|
-
measured: true,
|
|
39
|
-
note:
|
|
40
|
-
"Measured on every host at startup by `benchmarkSoftwarePresets`, in " +
|
|
41
|
-
"pixels per second per preset, and chosen from by `pickSoftwarePreset`."
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Every preset: `buildVideoArgs` passes whichever the offer chose. */
|
|
46
|
-
get selectableRungs() {
|
|
47
|
-
return PRESETS;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** @param {string | null} rung @returns {string[]} */
|
|
51
|
-
benchmarkArgs(rung = null) {
|
|
52
|
-
return ["-c:v", "libx264", "-preset", rung ?? SOFTWARE_PRESET, "-crf", SOFTWARE_CRF, "-pix_fmt", "yuv420p"];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
buildVideoArgs({
|
|
56
|
-
targetWidth,
|
|
57
|
-
targetHeight,
|
|
58
|
-
segmentDurationSec,
|
|
59
|
-
preset,
|
|
60
|
-
fps,
|
|
61
|
-
tonemap,
|
|
62
|
-
forcedKeyframeTimes,
|
|
63
|
-
rateControl = null
|
|
64
|
-
}) {
|
|
65
|
-
const { w, h } = safeDimensions(targetWidth, targetHeight);
|
|
66
|
-
const chosenPreset = typeof preset === "string" && preset.length > 0 ? preset : SOFTWARE_PRESET;
|
|
67
|
-
// Output frame rate: inherited from the source (rounded/capped) by the
|
|
68
|
-
// session manager, TRANSCODE_FPS by default. MUST be an integer and MUST
|
|
69
|
-
// equal the value used in the GOP below, or keyframes drift off the grid.
|
|
70
|
-
const outFps = Number.isInteger(fps) && fps > 0 ? fps : TRANSCODE_FPS;
|
|
71
|
-
// HDR→SDR tone-map, inserted AFTER the downscale so it runs on the smaller
|
|
72
|
-
// frame (cheaper on ARM); only when the source is HDR and the filters are
|
|
73
|
-
// present (session manager gates on both).
|
|
74
|
-
const tonemapPart = tonemap === true ? `,${TONEMAP_FILTER_CHAIN}` : "";
|
|
75
|
-
return [
|
|
76
|
-
// Never upscale: cap the target box to the source size (min with
|
|
77
|
-
// iw/ih), so a small source (e.g. 720x400) is encoded at its own
|
|
78
|
-
// resolution instead of being scaled up to the viewport — far fewer
|
|
79
|
-
// pixels, much faster on ARM. force_original_aspect_ratio keeps aspect.
|
|
80
|
-
"-vf",
|
|
81
|
-
`scale='min(${w},iw)':'min(${h},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2${tonemapPart},fps=${outFps}`,
|
|
82
|
-
"-c:v", "libx264",
|
|
83
|
-
// Preset is chosen per stream by the session manager from the startup
|
|
84
|
-
// benchmark (highest quality that still encodes the source resolution
|
|
85
|
-
// faster than realtime); falls back to the static default.
|
|
86
|
-
"-preset", chosenPreset,
|
|
87
|
-
"-crf", SOFTWARE_CRF,
|
|
88
|
-
// Constrained CRF: bound peak bitrate so a complex scene cannot produce
|
|
89
|
-
// segments a thin viewer link (cellular) can't download in time. The
|
|
90
|
-
// figures and the level are the OUTPUT's own, named by its key
|
|
91
|
-
// (`OutputSpec`), and are not worked out again here.
|
|
92
|
-
...rateControlArgs(rateControl),
|
|
93
|
-
"-threads", String(CPU_THREADS),
|
|
94
|
-
"-pix_fmt", "yuv420p",
|
|
95
|
-
// Fixed GOP: a keyframe exactly every (segmentDurationSec × fps) frames,
|
|
96
|
-
// scene-cut keyframes disabled. This is frame-count based, so it is
|
|
97
|
-
// independent of the PTS offset used on seek-restart — every HLS segment
|
|
98
|
-
// is exactly segmentDurationSec long and starts on a keyframe, so segment
|
|
99
|
-
// boundaries line up with the synthetic playlist with no gaps. (The old
|
|
100
|
-
// the OLD `expr:` form of -force_key_frames broke after a seek, because
|
|
101
|
-
// the `t` it reads is shifted by `-output_ts_offset`.)
|
|
102
|
-
//
|
|
103
|
-
// An explicit cut LIST is a different thing and does work: verified by
|
|
104
|
-
// running it, its times are on the run's own timeline — the same one
|
|
105
|
-
// `-segment_times` is measured on — so both are given one list and
|
|
106
|
-
// cannot drift apart. It replaces the frame-count GOP, which cannot
|
|
107
|
-
// describe the source's keyframes because they are not evenly spaced.
|
|
108
|
-
// `-g` stays as an upper bound on the interval: an extra keyframe
|
|
109
|
-
// inside a segment costs a little bitrate and cuts nothing, while
|
|
110
|
-
// leaving the interval unbounded means a driver that ignores the list
|
|
111
|
-
// produces one enormous segment instead of a wrong but cut one.
|
|
112
|
-
// `-keyint_min` goes, since a MINIMUM interval is the one thing that
|
|
113
|
-
// could argue with a forced keyframe.
|
|
114
|
-
"-g", String(segmentDurationSec * outFps),
|
|
115
|
-
...(hasForcedTimes(forcedKeyframeTimes)
|
|
116
|
-
? keyFrameArgs(segmentDurationSec, forcedKeyframeTimes)
|
|
117
|
-
: ["-keyint_min", String(segmentDurationSec * outFps)]),
|
|
118
|
-
"-sc_threshold", "0"
|
|
119
|
-
];
|
|
120
|
-
}
|
|
121
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file libx264 — the encoder every host has and the only one whose ladder of
|
|
3
|
+
* speed settings is measured at startup.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Encoder } from "./Encoder.js";
|
|
7
|
+
import {
|
|
8
|
+
CPU_THREADS,
|
|
9
|
+
hasForcedTimes,
|
|
10
|
+
keyFrameArgs,
|
|
11
|
+
rateControlArgs,
|
|
12
|
+
safeDimensions,
|
|
13
|
+
SOFTWARE_CRF,
|
|
14
|
+
SOFTWARE_PRESET,
|
|
15
|
+
TONEMAP_FILTER_CHAIN,
|
|
16
|
+
TRANSCODE_FPS
|
|
17
|
+
} from "./args.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The presets the startup benchmark walks, slowest first.
|
|
21
|
+
*
|
|
22
|
+
* The same list `benchmarkSoftwarePresets` measures and `pickSoftwarePreset`
|
|
23
|
+
* chooses from; it is stated here because it is a property of this kind, and
|
|
24
|
+
* the four hardware kinds beside it show what its absence looks like.
|
|
25
|
+
*/
|
|
26
|
+
const PRESETS = ["fast", "faster", "veryfast", "superfast", "ultrafast"];
|
|
27
|
+
|
|
28
|
+
export class SoftwareEncoder extends Encoder {
|
|
29
|
+
constructor() {
|
|
30
|
+
super({ name: "libx264", kind: "software", device: null, inputArgs: [] });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @returns {import("./Encoder.js").SpeedLadder} */
|
|
34
|
+
get speedLadder() {
|
|
35
|
+
return {
|
|
36
|
+
flag: "-preset",
|
|
37
|
+
values: PRESETS,
|
|
38
|
+
measured: true,
|
|
39
|
+
note:
|
|
40
|
+
"Measured on every host at startup by `benchmarkSoftwarePresets`, in " +
|
|
41
|
+
"pixels per second per preset, and chosen from by `pickSoftwarePreset`."
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Every preset: `buildVideoArgs` passes whichever the offer chose. */
|
|
46
|
+
get selectableRungs() {
|
|
47
|
+
return PRESETS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {string | null} rung @returns {string[]} */
|
|
51
|
+
benchmarkArgs(rung = null) {
|
|
52
|
+
return ["-c:v", "libx264", "-preset", rung ?? SOFTWARE_PRESET, "-crf", SOFTWARE_CRF, "-pix_fmt", "yuv420p"];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
buildVideoArgs({
|
|
56
|
+
targetWidth,
|
|
57
|
+
targetHeight,
|
|
58
|
+
segmentDurationSec,
|
|
59
|
+
preset,
|
|
60
|
+
fps,
|
|
61
|
+
tonemap,
|
|
62
|
+
forcedKeyframeTimes,
|
|
63
|
+
rateControl = null
|
|
64
|
+
}) {
|
|
65
|
+
const { w, h } = safeDimensions(targetWidth, targetHeight);
|
|
66
|
+
const chosenPreset = typeof preset === "string" && preset.length > 0 ? preset : SOFTWARE_PRESET;
|
|
67
|
+
// Output frame rate: inherited from the source (rounded/capped) by the
|
|
68
|
+
// session manager, TRANSCODE_FPS by default. MUST be an integer and MUST
|
|
69
|
+
// equal the value used in the GOP below, or keyframes drift off the grid.
|
|
70
|
+
const outFps = Number.isInteger(fps) && fps > 0 ? fps : TRANSCODE_FPS;
|
|
71
|
+
// HDR→SDR tone-map, inserted AFTER the downscale so it runs on the smaller
|
|
72
|
+
// frame (cheaper on ARM); only when the source is HDR and the filters are
|
|
73
|
+
// present (session manager gates on both).
|
|
74
|
+
const tonemapPart = tonemap === true ? `,${TONEMAP_FILTER_CHAIN}` : "";
|
|
75
|
+
return [
|
|
76
|
+
// Never upscale: cap the target box to the source size (min with
|
|
77
|
+
// iw/ih), so a small source (e.g. 720x400) is encoded at its own
|
|
78
|
+
// resolution instead of being scaled up to the viewport — far fewer
|
|
79
|
+
// pixels, much faster on ARM. force_original_aspect_ratio keeps aspect.
|
|
80
|
+
"-vf",
|
|
81
|
+
`scale='min(${w},iw)':'min(${h},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2${tonemapPart},fps=${outFps}`,
|
|
82
|
+
"-c:v", "libx264",
|
|
83
|
+
// Preset is chosen per stream by the session manager from the startup
|
|
84
|
+
// benchmark (highest quality that still encodes the source resolution
|
|
85
|
+
// faster than realtime); falls back to the static default.
|
|
86
|
+
"-preset", chosenPreset,
|
|
87
|
+
"-crf", SOFTWARE_CRF,
|
|
88
|
+
// Constrained CRF: bound peak bitrate so a complex scene cannot produce
|
|
89
|
+
// segments a thin viewer link (cellular) can't download in time. The
|
|
90
|
+
// figures and the level are the OUTPUT's own, named by its key
|
|
91
|
+
// (`OutputSpec`), and are not worked out again here.
|
|
92
|
+
...rateControlArgs(rateControl),
|
|
93
|
+
"-threads", String(CPU_THREADS),
|
|
94
|
+
"-pix_fmt", "yuv420p",
|
|
95
|
+
// Fixed GOP: a keyframe exactly every (segmentDurationSec × fps) frames,
|
|
96
|
+
// scene-cut keyframes disabled. This is frame-count based, so it is
|
|
97
|
+
// independent of the PTS offset used on seek-restart — every HLS segment
|
|
98
|
+
// is exactly segmentDurationSec long and starts on a keyframe, so segment
|
|
99
|
+
// boundaries line up with the synthetic playlist with no gaps. (The old
|
|
100
|
+
// the OLD `expr:` form of -force_key_frames broke after a seek, because
|
|
101
|
+
// the `t` it reads is shifted by `-output_ts_offset`.)
|
|
102
|
+
//
|
|
103
|
+
// An explicit cut LIST is a different thing and does work: verified by
|
|
104
|
+
// running it, its times are on the run's own timeline — the same one
|
|
105
|
+
// `-segment_times` is measured on — so both are given one list and
|
|
106
|
+
// cannot drift apart. It replaces the frame-count GOP, which cannot
|
|
107
|
+
// describe the source's keyframes because they are not evenly spaced.
|
|
108
|
+
// `-g` stays as an upper bound on the interval: an extra keyframe
|
|
109
|
+
// inside a segment costs a little bitrate and cuts nothing, while
|
|
110
|
+
// leaving the interval unbounded means a driver that ignores the list
|
|
111
|
+
// produces one enormous segment instead of a wrong but cut one.
|
|
112
|
+
// `-keyint_min` goes, since a MINIMUM interval is the one thing that
|
|
113
|
+
// could argue with a forced keyframe.
|
|
114
|
+
"-g", String(segmentDurationSec * outFps),
|
|
115
|
+
...(hasForcedTimes(forcedKeyframeTimes)
|
|
116
|
+
? keyFrameArgs(segmentDurationSec, forcedKeyframeTimes)
|
|
117
|
+
: ["-keyint_min", String(segmentDurationSec * outFps)]),
|
|
118
|
+
"-sc_threshold", "0"
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
}
|