@torrent-tv/proxy 2.88.4 → 2.88.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2269 -2258
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/calibration/audio/NOTICE.md +19 -0
- package/assets/calibration/audio/aac.m4a +0 -0
- package/assets/calibration/audio/ac3.ac3 +0 -0
- package/assets/calibration/audio/alac.m4a +0 -0
- package/assets/calibration/audio/dts.dts +0 -0
- package/assets/calibration/audio/eac3.eac3 +0 -0
- package/assets/calibration/audio/flac.flac +0 -0
- package/assets/calibration/audio/manifest.json +98 -0
- package/assets/calibration/audio/mp2.mp2 +0 -0
- package/assets/calibration/audio/mp3.mp3 +0 -0
- package/assets/calibration/audio/opus.opus +0 -0
- package/assets/calibration/audio/pcm.wav +0 -0
- package/assets/calibration/audio/truehd.thd +0 -0
- package/assets/calibration/audio/vorbis.ogg +0 -0
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -411
- package/docs/browser-proxy-contract.md +155 -152
- 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/playback-start-forecast.md +56 -0
- 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 -178
- 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 -95
- 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/build-audio-samples.mjs +31 -0
- package/scripts/render-run-graph.js +173 -173
- package/server.js +730 -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 +1239 -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 -783
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2282
- 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 -418
- package/services/encode/audio-calibration.js +67 -0
- package/services/encode/audio-work-rate.js +15 -0
- 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 +24 -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 +1167 -1150
- 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 -349
- package/services/encode/quality/output-format.js +319 -319
- 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 +755 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +458 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +12 -1
- 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 +299 -298
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -609
- 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 -1344
- 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 -143
- 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 -233
- 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 +786 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +516 -708
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1020 -995
- 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 +1937 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1255 -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/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 +2688 -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-calibration.test.js +23 -0
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/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-presentation-clock.test.js +162 -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 -112
- 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 +273 -270
- 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 +483 -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 -1257
- 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 +352 -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 -105
- 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 -289
- package/test/source-file.test.js +133 -133
- 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 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,216 +1,216 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which output already on this proxy serves a viewer, instead of a new
|
|
3
|
-
* encode of the format they would otherwise be given.
|
|
4
|
-
*
|
|
5
|
-
* Decided with the user 2026-09-16, and made one rule for every path on
|
|
6
|
-
* 2026-09-23 (roadmap item 97, step 11). Quality is compared by the number of
|
|
7
|
-
* points in the picture. An output qualifies only if the piece at the viewer's
|
|
8
|
-
* position is already made — material somewhere else in the film spares them
|
|
9
|
-
* nothing — AND the viewer's own link admits its load (`link-budget.js`), in
|
|
10
|
-
* every mode and on every branch. A load whose figure is a bound (`known`) is
|
|
11
|
-
* taken before one whose figure is an average (`estimated`), whatever their
|
|
12
|
-
* sizes; an `unknown` load is admitted only while nothing has measured the
|
|
13
|
-
* link.
|
|
14
|
-
*
|
|
15
|
-
* AUTOMATIC:
|
|
16
|
-
* 1. at least the quality wanted — the nearest to it;
|
|
17
|
-
* 2. below the quality wanted but above the next rung down of this viewer's
|
|
18
|
-
* own ladder — the largest of those;
|
|
19
|
-
* 3. lower still only when producing the wanted format here risks the viewer
|
|
20
|
-
* waiting: this machine does not hold it at the speed the file needs — the
|
|
21
|
-
* largest of those.
|
|
22
|
-
*
|
|
23
|
-
* CHOSEN BY HAND: exactly the size chosen — of those, the one with the highest
|
|
24
|
-
* bitrate the link admits. Which encoder made it and at which speed setting do
|
|
25
|
-
* not matter to the viewer.
|
|
26
|
-
*
|
|
27
|
-
* No reading of disk or machine here: every figure arrives as a plain value.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { OutputSpec } from "../output/OutputSpec.js";
|
|
31
|
-
import { admissionRank } from "./link-budget.js";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @typedef {object} Candidate
|
|
35
|
-
* @property {string} key - The output's key.
|
|
36
|
-
* @property {OutputSpec} spec
|
|
37
|
-
* @property {number} width
|
|
38
|
-
* @property {number} height
|
|
39
|
-
* @property {boolean} readyHere - The piece at the viewer's position is made.
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {object} params
|
|
44
|
-
* @param {"auto" | "manual"} params.mode
|
|
45
|
-
* @param {{ width: number, height: number }} params.wanted - The format this
|
|
46
|
-
* viewer would otherwise be given.
|
|
47
|
-
* @param {number} params.nextLowerArea - Points in the next rung below `wanted`
|
|
48
|
-
* on this viewer's own ladder; zero when there is none.
|
|
49
|
-
* @param {boolean} params.atRisk - Producing `wanted` here is measured not to
|
|
50
|
-
* keep up with what the file needs. False where nothing measured it.
|
|
51
|
-
* @param {(candidate: Candidate) => { admitted: boolean, load: object }} params.judge -
|
|
52
|
-
* What this viewer's link does with the candidate's whole load.
|
|
53
|
-
* @param {Candidate[]} params.candidates
|
|
54
|
-
* @returns {{ key: string, answer: object } | null} The output to serve, or
|
|
55
|
-
* null to produce `wanted`.
|
|
56
|
-
*/
|
|
57
|
-
export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, judge, candidates }) {
|
|
58
|
-
const admitted = (candidates ?? [])
|
|
59
|
-
.filter((candidate) => candidate.readyHere === true)
|
|
60
|
-
.map((candidate) => ({ candidate, answer: judge(candidate) }))
|
|
61
|
-
.filter((one) => one.answer.admitted);
|
|
62
|
-
const areaOf = (one) => one.candidate.width * one.candidate.height;
|
|
63
|
-
const rateOf = (one) => one.answer.load.totalMbps ?? 0;
|
|
64
|
-
const byRank = (left, right) => admissionRank(right.answer) - admissionRank(left.answer);
|
|
65
|
-
const pick = (list) => (list.length > 0 ? { key: list[0].candidate.key, answer: list[0].answer } : null);
|
|
66
|
-
if (mode === "manual") {
|
|
67
|
-
return pick(
|
|
68
|
-
admitted
|
|
69
|
-
.filter((one) => one.candidate.width === wanted.width && one.candidate.height === wanted.height)
|
|
70
|
-
.sort((left, right) => byRank(left, right) || rateOf(right) - rateOf(left))
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
const wantedArea = wanted.width * wanted.height;
|
|
74
|
-
const atLeast = admitted
|
|
75
|
-
.filter((one) => areaOf(one) >= wantedArea)
|
|
76
|
-
.sort((left, right) => byRank(left, right) || areaOf(left) - areaOf(right) || rateOf(right) - rateOf(left));
|
|
77
|
-
if (atLeast.length > 0) {
|
|
78
|
-
return pick(atLeast);
|
|
79
|
-
}
|
|
80
|
-
const below = admitted
|
|
81
|
-
.filter((one) => areaOf(one) < wantedArea)
|
|
82
|
-
.sort((left, right) => byRank(left, right) || areaOf(right) - areaOf(left) || rateOf(right) - rateOf(left));
|
|
83
|
-
const close = below.filter((one) => areaOf(one) > nextLowerArea);
|
|
84
|
-
if (close.length > 0) {
|
|
85
|
-
return pick(close);
|
|
86
|
-
}
|
|
87
|
-
return atRisk ? pick(below) : null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Points in the next rung below a height on a viewer's own ladder.
|
|
92
|
-
*
|
|
93
|
-
* @param {{ width: number, height: number }[]} ladder - High to low.
|
|
94
|
-
* @param {number} height
|
|
95
|
-
* @returns {number} Zero when nothing on the ladder is lower.
|
|
96
|
-
*/
|
|
97
|
-
export function nextRungAreaBelow(ladder, height) {
|
|
98
|
-
const next = (ladder ?? []).find((rung) => rung.height < height);
|
|
99
|
-
return next ? next.width * next.height : 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The outputs that could stand in for `wanted`, read from their keys.
|
|
104
|
-
*
|
|
105
|
-
* One qualifies when it is the same picture of the same file in every respect
|
|
106
|
-
* but its quality: the same torrent, container, cut grid, soundtrack and what
|
|
107
|
-
* the output carries. A re-encode qualifies only if this machine's encoder
|
|
108
|
-
* made it, because serving it means going on producing it, and pieces of
|
|
109
|
-
* another encoder cannot share its header. A copy is the source's own picture.
|
|
110
|
-
*
|
|
111
|
-
* @param {object} params
|
|
112
|
-
* @param {OutputSpec} params.wanted
|
|
113
|
-
* @param {Iterable<string>} params.keys - Every output this proxy holds, live
|
|
114
|
-
* or only on disk. Repeats are fine.
|
|
115
|
-
* @param {{ width: number, height: number, megabitsPerSecond: number | null }} params.source
|
|
116
|
-
* @param {string} params.encoderName
|
|
117
|
-
* @param {(key: string) => boolean} params.readyAt - The piece at the viewer's
|
|
118
|
-
* position is made.
|
|
119
|
-
* @returns {Candidate[]}
|
|
120
|
-
*/
|
|
121
|
-
/**
|
|
122
|
-
* Whether two outputs are of the same MATERIAL — the same film, the same
|
|
123
|
-
* container, the same cuts, the same tracks — and therefore interchangeable as
|
|
124
|
-
* far as anything but picture size is concerned.
|
|
125
|
-
*
|
|
126
|
-
* Stated once and exported, because two places ask it: choosing an output by
|
|
127
|
-
* quality, and answering which output serves a height. A step of a picture is
|
|
128
|
-
* only a step of THAT picture if this holds; a 720p step in fMP4 handed to a
|
|
129
|
-
* picture cut for MPEG-TS is a segment name its player cannot even ask for.
|
|
130
|
-
*
|
|
131
|
-
* Picture SIZE is deliberately not in it: differing in size is what a step is.
|
|
132
|
-
*
|
|
133
|
-
* @param {OutputSpec} spec
|
|
134
|
-
* @param {OutputSpec} wanted
|
|
135
|
-
* @returns {boolean}
|
|
136
|
-
*/
|
|
137
|
-
export function isSameMaterial(spec, wanted) {
|
|
138
|
-
if (!spec || !wanted || !spec.video || !wanted.video) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
return (
|
|
142
|
-
spec.sourceKey === wanted.sourceKey &&
|
|
143
|
-
spec.segmentFormatId === wanted.segmentFormatId &&
|
|
144
|
-
spec.grid.toKey() === wanted.grid.toKey() &&
|
|
145
|
-
spec.carries === wanted.carries &&
|
|
146
|
-
spec.video.fileIndex === wanted.video.fileIndex &&
|
|
147
|
-
(spec.audio?.toKey() ?? "") === (wanted.audio?.toKey() ?? "")
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function servingCandidates({ wanted, keys, source, encoderName, readyAt }) {
|
|
152
|
-
const found = new Map();
|
|
153
|
-
for (const key of keys ?? []) {
|
|
154
|
-
if (found.has(key)) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const spec = OutputSpec.fromKey(key);
|
|
158
|
-
if (!spec || !spec.video || !wanted.video) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (!isSameMaterial(spec, wanted)) {
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
const encode = spec.video.encode;
|
|
165
|
-
if (encode && encode.encoder !== encoderName) {
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
const width = encode ? encode.width : source.width;
|
|
169
|
-
const height = encode ? encode.height : source.height;
|
|
170
|
-
if (!(width > 0 && height > 0)) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
found.set(key, {
|
|
174
|
-
key,
|
|
175
|
-
spec,
|
|
176
|
-
width,
|
|
177
|
-
height,
|
|
178
|
-
readyHere: readyAt(key)
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return [...found.values()];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Whether ONE output may serve this viewer: the same material as the picture
|
|
186
|
-
* it would stand in for, the size their mode requires, and a load their own
|
|
187
|
-
* link admits.
|
|
188
|
-
*
|
|
189
|
-
* THE ONE RULE EVERY PATH ASKS (roadmap item 97, step 11). The picture
|
|
190
|
-
* answering for its own height, an output found already producing a height,
|
|
191
|
-
* and the choice among outputs already here used to decide each for itself,
|
|
192
|
-
* and only the last asked about the viewer at all.
|
|
193
|
-
*
|
|
194
|
-
* @param {object} params
|
|
195
|
-
* @param {OutputSpec} params.spec - The output that would serve.
|
|
196
|
-
* @param {OutputSpec} params.pictureSpec - The picture it would stand in for.
|
|
197
|
-
* @param {"auto" | "manual"} params.mode
|
|
198
|
-
* @param {{ width: number, height: number } | null} params.size - The size the
|
|
199
|
-
* output produces; for a copy, the source's.
|
|
200
|
-
* @param {{ width: number, height: number } | null} params.wanted - The size a
|
|
201
|
-
* viewer who picked by hand must get exactly; ignored in AUTO.
|
|
202
|
-
* @param {(spec: OutputSpec) => { admitted: boolean }} params.judge
|
|
203
|
-
* @returns {{ suits: boolean, answer: object | null, reason: string }}
|
|
204
|
-
*/
|
|
205
|
-
export function outputSuits({ spec, pictureSpec, mode, size, wanted, judge }) {
|
|
206
|
-
if (!isSameMaterial(spec, pictureSpec)) {
|
|
207
|
-
return { suits: false, answer: null, reason: "another material" };
|
|
208
|
-
}
|
|
209
|
-
if (mode === "manual" && wanted && size && (size.width !== wanted.width || size.height !== wanted.height)) {
|
|
210
|
-
return { suits: false, answer: null, reason: "not the size picked" };
|
|
211
|
-
}
|
|
212
|
-
const answer = judge(spec);
|
|
213
|
-
return answer.admitted
|
|
214
|
-
? { suits: true, answer, reason: answer.verdict }
|
|
215
|
-
: { suits: false, answer, reason: answer.verdict };
|
|
216
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Which output already on this proxy serves a viewer, instead of a new
|
|
3
|
+
* encode of the format they would otherwise be given.
|
|
4
|
+
*
|
|
5
|
+
* Decided with the user 2026-09-16, and made one rule for every path on
|
|
6
|
+
* 2026-09-23 (roadmap item 97, step 11). Quality is compared by the number of
|
|
7
|
+
* points in the picture. An output qualifies only if the piece at the viewer's
|
|
8
|
+
* position is already made — material somewhere else in the film spares them
|
|
9
|
+
* nothing — AND the viewer's own link admits its load (`link-budget.js`), in
|
|
10
|
+
* every mode and on every branch. A load whose figure is a bound (`known`) is
|
|
11
|
+
* taken before one whose figure is an average (`estimated`), whatever their
|
|
12
|
+
* sizes; an `unknown` load is admitted only while nothing has measured the
|
|
13
|
+
* link.
|
|
14
|
+
*
|
|
15
|
+
* AUTOMATIC:
|
|
16
|
+
* 1. at least the quality wanted — the nearest to it;
|
|
17
|
+
* 2. below the quality wanted but above the next rung down of this viewer's
|
|
18
|
+
* own ladder — the largest of those;
|
|
19
|
+
* 3. lower still only when producing the wanted format here risks the viewer
|
|
20
|
+
* waiting: this machine does not hold it at the speed the file needs — the
|
|
21
|
+
* largest of those.
|
|
22
|
+
*
|
|
23
|
+
* CHOSEN BY HAND: exactly the size chosen — of those, the one with the highest
|
|
24
|
+
* bitrate the link admits. Which encoder made it and at which speed setting do
|
|
25
|
+
* not matter to the viewer.
|
|
26
|
+
*
|
|
27
|
+
* No reading of disk or machine here: every figure arrives as a plain value.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { OutputSpec } from "../output/OutputSpec.js";
|
|
31
|
+
import { admissionRank } from "./link-budget.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} Candidate
|
|
35
|
+
* @property {string} key - The output's key.
|
|
36
|
+
* @property {OutputSpec} spec
|
|
37
|
+
* @property {number} width
|
|
38
|
+
* @property {number} height
|
|
39
|
+
* @property {boolean} readyHere - The piece at the viewer's position is made.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} params
|
|
44
|
+
* @param {"auto" | "manual"} params.mode
|
|
45
|
+
* @param {{ width: number, height: number }} params.wanted - The format this
|
|
46
|
+
* viewer would otherwise be given.
|
|
47
|
+
* @param {number} params.nextLowerArea - Points in the next rung below `wanted`
|
|
48
|
+
* on this viewer's own ladder; zero when there is none.
|
|
49
|
+
* @param {boolean} params.atRisk - Producing `wanted` here is measured not to
|
|
50
|
+
* keep up with what the file needs. False where nothing measured it.
|
|
51
|
+
* @param {(candidate: Candidate) => { admitted: boolean, load: object }} params.judge -
|
|
52
|
+
* What this viewer's link does with the candidate's whole load.
|
|
53
|
+
* @param {Candidate[]} params.candidates
|
|
54
|
+
* @returns {{ key: string, answer: object } | null} The output to serve, or
|
|
55
|
+
* null to produce `wanted`.
|
|
56
|
+
*/
|
|
57
|
+
export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, judge, candidates }) {
|
|
58
|
+
const admitted = (candidates ?? [])
|
|
59
|
+
.filter((candidate) => candidate.readyHere === true)
|
|
60
|
+
.map((candidate) => ({ candidate, answer: judge(candidate) }))
|
|
61
|
+
.filter((one) => one.answer.admitted);
|
|
62
|
+
const areaOf = (one) => one.candidate.width * one.candidate.height;
|
|
63
|
+
const rateOf = (one) => one.answer.load.totalMbps ?? 0;
|
|
64
|
+
const byRank = (left, right) => admissionRank(right.answer) - admissionRank(left.answer);
|
|
65
|
+
const pick = (list) => (list.length > 0 ? { key: list[0].candidate.key, answer: list[0].answer } : null);
|
|
66
|
+
if (mode === "manual") {
|
|
67
|
+
return pick(
|
|
68
|
+
admitted
|
|
69
|
+
.filter((one) => one.candidate.width === wanted.width && one.candidate.height === wanted.height)
|
|
70
|
+
.sort((left, right) => byRank(left, right) || rateOf(right) - rateOf(left))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const wantedArea = wanted.width * wanted.height;
|
|
74
|
+
const atLeast = admitted
|
|
75
|
+
.filter((one) => areaOf(one) >= wantedArea)
|
|
76
|
+
.sort((left, right) => byRank(left, right) || areaOf(left) - areaOf(right) || rateOf(right) - rateOf(left));
|
|
77
|
+
if (atLeast.length > 0) {
|
|
78
|
+
return pick(atLeast);
|
|
79
|
+
}
|
|
80
|
+
const below = admitted
|
|
81
|
+
.filter((one) => areaOf(one) < wantedArea)
|
|
82
|
+
.sort((left, right) => byRank(left, right) || areaOf(right) - areaOf(left) || rateOf(right) - rateOf(left));
|
|
83
|
+
const close = below.filter((one) => areaOf(one) > nextLowerArea);
|
|
84
|
+
if (close.length > 0) {
|
|
85
|
+
return pick(close);
|
|
86
|
+
}
|
|
87
|
+
return atRisk ? pick(below) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Points in the next rung below a height on a viewer's own ladder.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ width: number, height: number }[]} ladder - High to low.
|
|
94
|
+
* @param {number} height
|
|
95
|
+
* @returns {number} Zero when nothing on the ladder is lower.
|
|
96
|
+
*/
|
|
97
|
+
export function nextRungAreaBelow(ladder, height) {
|
|
98
|
+
const next = (ladder ?? []).find((rung) => rung.height < height);
|
|
99
|
+
return next ? next.width * next.height : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The outputs that could stand in for `wanted`, read from their keys.
|
|
104
|
+
*
|
|
105
|
+
* One qualifies when it is the same picture of the same file in every respect
|
|
106
|
+
* but its quality: the same torrent, container, cut grid, soundtrack and what
|
|
107
|
+
* the output carries. A re-encode qualifies only if this machine's encoder
|
|
108
|
+
* made it, because serving it means going on producing it, and pieces of
|
|
109
|
+
* another encoder cannot share its header. A copy is the source's own picture.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} params
|
|
112
|
+
* @param {OutputSpec} params.wanted
|
|
113
|
+
* @param {Iterable<string>} params.keys - Every output this proxy holds, live
|
|
114
|
+
* or only on disk. Repeats are fine.
|
|
115
|
+
* @param {{ width: number, height: number, megabitsPerSecond: number | null }} params.source
|
|
116
|
+
* @param {string} params.encoderName
|
|
117
|
+
* @param {(key: string) => boolean} params.readyAt - The piece at the viewer's
|
|
118
|
+
* position is made.
|
|
119
|
+
* @returns {Candidate[]}
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* Whether two outputs are of the same MATERIAL — the same film, the same
|
|
123
|
+
* container, the same cuts, the same tracks — and therefore interchangeable as
|
|
124
|
+
* far as anything but picture size is concerned.
|
|
125
|
+
*
|
|
126
|
+
* Stated once and exported, because two places ask it: choosing an output by
|
|
127
|
+
* quality, and answering which output serves a height. A step of a picture is
|
|
128
|
+
* only a step of THAT picture if this holds; a 720p step in fMP4 handed to a
|
|
129
|
+
* picture cut for MPEG-TS is a segment name its player cannot even ask for.
|
|
130
|
+
*
|
|
131
|
+
* Picture SIZE is deliberately not in it: differing in size is what a step is.
|
|
132
|
+
*
|
|
133
|
+
* @param {OutputSpec} spec
|
|
134
|
+
* @param {OutputSpec} wanted
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
137
|
+
export function isSameMaterial(spec, wanted) {
|
|
138
|
+
if (!spec || !wanted || !spec.video || !wanted.video) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
spec.sourceKey === wanted.sourceKey &&
|
|
143
|
+
spec.segmentFormatId === wanted.segmentFormatId &&
|
|
144
|
+
spec.grid.toKey() === wanted.grid.toKey() &&
|
|
145
|
+
spec.carries === wanted.carries &&
|
|
146
|
+
spec.video.fileIndex === wanted.video.fileIndex &&
|
|
147
|
+
(spec.audio?.toKey() ?? "") === (wanted.audio?.toKey() ?? "")
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function servingCandidates({ wanted, keys, source, encoderName, readyAt }) {
|
|
152
|
+
const found = new Map();
|
|
153
|
+
for (const key of keys ?? []) {
|
|
154
|
+
if (found.has(key)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const spec = OutputSpec.fromKey(key);
|
|
158
|
+
if (!spec || !spec.video || !wanted.video) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!isSameMaterial(spec, wanted)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const encode = spec.video.encode;
|
|
165
|
+
if (encode && encode.encoder !== encoderName) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const width = encode ? encode.width : source.width;
|
|
169
|
+
const height = encode ? encode.height : source.height;
|
|
170
|
+
if (!(width > 0 && height > 0)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
found.set(key, {
|
|
174
|
+
key,
|
|
175
|
+
spec,
|
|
176
|
+
width,
|
|
177
|
+
height,
|
|
178
|
+
readyHere: readyAt(key)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return [...found.values()];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether ONE output may serve this viewer: the same material as the picture
|
|
186
|
+
* it would stand in for, the size their mode requires, and a load their own
|
|
187
|
+
* link admits.
|
|
188
|
+
*
|
|
189
|
+
* THE ONE RULE EVERY PATH ASKS (roadmap item 97, step 11). The picture
|
|
190
|
+
* answering for its own height, an output found already producing a height,
|
|
191
|
+
* and the choice among outputs already here used to decide each for itself,
|
|
192
|
+
* and only the last asked about the viewer at all.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} params
|
|
195
|
+
* @param {OutputSpec} params.spec - The output that would serve.
|
|
196
|
+
* @param {OutputSpec} params.pictureSpec - The picture it would stand in for.
|
|
197
|
+
* @param {"auto" | "manual"} params.mode
|
|
198
|
+
* @param {{ width: number, height: number } | null} params.size - The size the
|
|
199
|
+
* output produces; for a copy, the source's.
|
|
200
|
+
* @param {{ width: number, height: number } | null} params.wanted - The size a
|
|
201
|
+
* viewer who picked by hand must get exactly; ignored in AUTO.
|
|
202
|
+
* @param {(spec: OutputSpec) => { admitted: boolean }} params.judge
|
|
203
|
+
* @returns {{ suits: boolean, answer: object | null, reason: string }}
|
|
204
|
+
*/
|
|
205
|
+
export function outputSuits({ spec, pictureSpec, mode, size, wanted, judge }) {
|
|
206
|
+
if (!isSameMaterial(spec, pictureSpec)) {
|
|
207
|
+
return { suits: false, answer: null, reason: "another material" };
|
|
208
|
+
}
|
|
209
|
+
if (mode === "manual" && wanted && size && (size.width !== wanted.width || size.height !== wanted.height)) {
|
|
210
|
+
return { suits: false, answer: null, reason: "not the size picked" };
|
|
211
|
+
}
|
|
212
|
+
const answer = judge(spec);
|
|
213
|
+
return answer.admitted
|
|
214
|
+
? { suits: true, answer, reason: answer.verdict }
|
|
215
|
+
: { suits: false, answer, reason: answer.verdict };
|
|
216
|
+
}
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which rung of the ladder the picture a viewer sees asks for (roadmap
|
|
3
|
-
* item 98).
|
|
4
|
-
*
|
|
5
|
-
* THE RULE, stated by the user 2026-09-27: the smallest height of the ladder
|
|
6
|
-
* whose frame is not smaller than the visible picture — both its width and its
|
|
7
|
-
* height. The visible picture is what the viewer's page measured: the frame
|
|
8
|
-
* that would be shown without being enlarged, in physical pixels
|
|
9
|
-
* (`server/public/domain/visible-picture.js`). A frame smaller than that is
|
|
10
|
-
* enlarged on screen and loses detail; a larger one costs encoder and link for
|
|
11
|
-
* detail the screen cannot show.
|
|
12
|
-
*
|
|
13
|
-
* When no rung is large enough — a picture shown bigger than the source — the
|
|
14
|
-
* answer is the top rung, the source's own size: nothing is ever made larger
|
|
15
|
-
* than the source.
|
|
16
|
-
*
|
|
17
|
-
* It bounds only a RE-ENCODED picture. A copy of the source is not re-encoded
|
|
18
|
-
* because it is taller than the picture on screen; it is re-encoded only when
|
|
19
|
-
* the browser cannot play it or the viewer's link cannot carry it (the rule is
|
|
20
|
-
* in "Key decisions" of the project notes).
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { buildResolutionLadder } from "../hwaccel.js";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The rung of `ladder` the visible picture asks for.
|
|
27
|
-
*
|
|
28
|
-
* @param {Array<{ width: number, height: number }>} ladder - Highest first, as
|
|
29
|
-
* `buildResolutionLadder` gives it.
|
|
30
|
-
* @param {{ width: number, height: number } | null} visible
|
|
31
|
-
* @returns {{ width: number, height: number } | null} Null when nothing was
|
|
32
|
-
* measured or there is no ladder.
|
|
33
|
-
*/
|
|
34
|
-
export function rungForVisiblePicture(ladder, visible) {
|
|
35
|
-
if (!Array.isArray(ladder) || ladder.length === 0 || !(visible?.width > 0) || !(visible?.height > 0)) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
const large = ladder.filter((rung) => rung.width >= visible.width && rung.height >= visible.height);
|
|
39
|
-
return large.length > 0 ? large[large.length - 1] : ladder[0];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The height a re-encoded picture of this source is bounded by for a viewer
|
|
44
|
-
* who sees `visible`, or null when nothing bounds it.
|
|
45
|
-
*
|
|
46
|
-
* @param {number} sourceWidth
|
|
47
|
-
* @param {number} sourceHeight
|
|
48
|
-
* @param {{ width: number, height: number } | null} visible
|
|
49
|
-
* @returns {number | null}
|
|
50
|
-
*/
|
|
51
|
-
export function visibleHeightCap(sourceWidth, sourceHeight, visible) {
|
|
52
|
-
const rung = rungForVisiblePicture(
|
|
53
|
-
buildResolutionLadder(Math.round(Number(sourceWidth) || 0), Math.round(Number(sourceHeight) || 0)),
|
|
54
|
-
visible
|
|
55
|
-
);
|
|
56
|
-
return rung ? rung.height : null;
|
|
57
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Which rung of the ladder the picture a viewer sees asks for (roadmap
|
|
3
|
+
* item 98).
|
|
4
|
+
*
|
|
5
|
+
* THE RULE, stated by the user 2026-09-27: the smallest height of the ladder
|
|
6
|
+
* whose frame is not smaller than the visible picture — both its width and its
|
|
7
|
+
* height. The visible picture is what the viewer's page measured: the frame
|
|
8
|
+
* that would be shown without being enlarged, in physical pixels
|
|
9
|
+
* (`server/public/domain/visible-picture.js`). A frame smaller than that is
|
|
10
|
+
* enlarged on screen and loses detail; a larger one costs encoder and link for
|
|
11
|
+
* detail the screen cannot show.
|
|
12
|
+
*
|
|
13
|
+
* When no rung is large enough — a picture shown bigger than the source — the
|
|
14
|
+
* answer is the top rung, the source's own size: nothing is ever made larger
|
|
15
|
+
* than the source.
|
|
16
|
+
*
|
|
17
|
+
* It bounds only a RE-ENCODED picture. A copy of the source is not re-encoded
|
|
18
|
+
* because it is taller than the picture on screen; it is re-encoded only when
|
|
19
|
+
* the browser cannot play it or the viewer's link cannot carry it (the rule is
|
|
20
|
+
* in "Key decisions" of the project notes).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { buildResolutionLadder } from "../hwaccel.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The rung of `ladder` the visible picture asks for.
|
|
27
|
+
*
|
|
28
|
+
* @param {Array<{ width: number, height: number }>} ladder - Highest first, as
|
|
29
|
+
* `buildResolutionLadder` gives it.
|
|
30
|
+
* @param {{ width: number, height: number } | null} visible
|
|
31
|
+
* @returns {{ width: number, height: number } | null} Null when nothing was
|
|
32
|
+
* measured or there is no ladder.
|
|
33
|
+
*/
|
|
34
|
+
export function rungForVisiblePicture(ladder, visible) {
|
|
35
|
+
if (!Array.isArray(ladder) || ladder.length === 0 || !(visible?.width > 0) || !(visible?.height > 0)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const large = ladder.filter((rung) => rung.width >= visible.width && rung.height >= visible.height);
|
|
39
|
+
return large.length > 0 ? large[large.length - 1] : ladder[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The height a re-encoded picture of this source is bounded by for a viewer
|
|
44
|
+
* who sees `visible`, or null when nothing bounds it.
|
|
45
|
+
*
|
|
46
|
+
* @param {number} sourceWidth
|
|
47
|
+
* @param {number} sourceHeight
|
|
48
|
+
* @param {{ width: number, height: number } | null} visible
|
|
49
|
+
* @returns {number | null}
|
|
50
|
+
*/
|
|
51
|
+
export function visibleHeightCap(sourceWidth, sourceHeight, visible) {
|
|
52
|
+
const rung = rungForVisiblePicture(
|
|
53
|
+
buildResolutionLadder(Math.round(Number(sourceWidth) || 0), Math.round(Number(sourceHeight) || 0)),
|
|
54
|
+
visible
|
|
55
|
+
);
|
|
56
|
+
return rung ? rung.height : null;
|
|
57
|
+
}
|