@torrent-tv/proxy 2.87.4 → 2.87.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +8 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +94 -94
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1083 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +212 -212
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +36 -36
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +983 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2 -2
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +1 -1
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/data-channel-handler.js +1356 -1356
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +657 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -73
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +176 -176
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -0
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/{services/torrent → utils}/torrent-source-key.js +38 -38
|
@@ -1,298 +1,298 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One file of one torrent, as this proxy reads it.
|
|
3
|
-
*
|
|
4
|
-
* Which torrent, which file inside it, what it is called, what a probe of it
|
|
5
|
-
* said, and the address this proxy's own HTTP serves it at. Every one of those
|
|
6
|
-
* is a fact about the FILE: it is the same for the picture and for every
|
|
7
|
-
* quality step of it, the same for one viewer and for five, and it does not
|
|
8
|
-
* change while anybody is watching.
|
|
9
|
-
*
|
|
10
|
-
* They used to be nine fields copied onto every session, and the copying was
|
|
11
|
-
* visible in the code that read them back: twenty places in the session manager
|
|
12
|
-
* assembled `${sourceKey}:${fileIndex}` by hand, through five different
|
|
13
|
-
* spellings of the same two values, to key the caches that are keyed by a file —
|
|
14
|
-
* the host timings, the download stats, the decode cost, the cut table. A file
|
|
15
|
-
* that knows its own key removes all twenty.
|
|
16
|
-
*
|
|
17
|
-
* **What this is NOT.** It is not the container: a container answers for what
|
|
18
|
-
* the format states about itself and needs bytes to do it. It is not the track:
|
|
19
|
-
* a track is one stream inside a container. This is the file as a HANDLE — how
|
|
20
|
-
* it is named, addressed and identified — plus the facts a probe of it
|
|
21
|
-
* returned, which arrive together and are cheapest to keep where the handle is.
|
|
22
|
-
*
|
|
23
|
-
* It is not the keyframe table either, and that one was here until 2026-09-15.
|
|
24
|
-
* A table has two states a bag of probe results cannot tell apart — read and
|
|
25
|
-
* empty, which makes every picture of this file a re-encode for ever, against
|
|
26
|
-
* not read yet, which is a shortage of bytes off the swarm — so it is an object
|
|
27
|
-
* of its own, `container/KeyframeTable.js`, owned by the layer that reads what a
|
|
28
|
-
* file states about itself.
|
|
29
|
-
*
|
|
30
|
-
* Nothing here reaches outward, the address included: the proxy's own base URL
|
|
31
|
-
* is passed in, because where this proxy listens is not a property of a film.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* What decoding this source costs per second of it, and which measurement of
|
|
36
|
-
* this host applies.
|
|
37
|
-
*
|
|
38
|
-
* The codec and bit depth travel with the rates because they decide WHICH
|
|
39
|
-
* measurement applies: the model is fitted per codec family, and a video that
|
|
40
|
-
* has to be re-encoded is by definition one the browser could not play — HEVC,
|
|
41
|
-
* 10-bit — which is exactly where H.264 constants are wrong.
|
|
42
|
-
*
|
|
43
|
-
* @param {{ width: number | null, height: number | null, fps: number | null, bitrateKbps: number | null, codec?: string | null, bitDepth?: number | null } | null} mediaInfo
|
|
44
|
-
* @returns {{ megapixelsPerSecond: number, megabitsPerSecond: number, codec: string, bitDepth: number | null } | null}
|
|
45
|
-
*/
|
|
46
|
-
export function sourceDecodeCharacteristics(mediaInfo) {
|
|
47
|
-
const width = Number(mediaInfo?.width);
|
|
48
|
-
const height = Number(mediaInfo?.height);
|
|
49
|
-
const fps = Number(mediaInfo?.fps);
|
|
50
|
-
const kbps = Number(mediaInfo?.bitrateKbps);
|
|
51
|
-
if (!(width > 0) || !(height > 0) || !(fps > 0) || !(kbps > 0)) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
const depth = Number(mediaInfo?.bitDepth);
|
|
55
|
-
return {
|
|
56
|
-
megapixelsPerSecond: (width * height * fps) / 1e6,
|
|
57
|
-
megabitsPerSecond: kbps / 1000,
|
|
58
|
-
codec: typeof mediaInfo?.codec === "string" ? mediaInfo.codec : "",
|
|
59
|
-
bitDepth: Number.isFinite(depth) && depth > 0 ? depth : null
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export class SourceFile {
|
|
64
|
-
/**
|
|
65
|
-
* @param {object} params
|
|
66
|
-
* @param {string} params.sourceKey - The registry's key for the torrent.
|
|
67
|
-
* @param {number} params.fileIndex - Zero-based index of the file in it.
|
|
68
|
-
* @param {string} [params.name] - The file's own name, for log lines.
|
|
69
|
-
*/
|
|
70
|
-
constructor({ sourceKey, fileIndex, name = "" }) {
|
|
71
|
-
this.sourceKey = String(sourceKey ?? "");
|
|
72
|
-
this.fileIndex = Number.isFinite(fileIndex) ? Number(fileIndex) : 0;
|
|
73
|
-
this.givenName = typeof name === "string" ? name.trim() : "";
|
|
74
|
-
/**
|
|
75
|
-
* What a probe of this file said. Null until one has answered — a cold
|
|
76
|
-
* torrent has no header yet, and "not read" is not the same as "zero".
|
|
77
|
-
*
|
|
78
|
-
* @type {object | null}
|
|
79
|
-
*/
|
|
80
|
-
this.media = null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* The key every cache about this file is keyed by.
|
|
85
|
-
*
|
|
86
|
-
* @returns {string}
|
|
87
|
-
*/
|
|
88
|
-
get key() {
|
|
89
|
-
return `${this.sourceKey}:${this.fileIndex}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* What to call this file in a log line. A file with no name is named by its
|
|
94
|
-
* index, so a line is never about a nameless thing.
|
|
95
|
-
*
|
|
96
|
-
* @returns {string}
|
|
97
|
-
*/
|
|
98
|
-
get name() {
|
|
99
|
-
return this.givenName.length > 0 ? this.givenName : `file#${this.fileIndex}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Take in what a reading of this file returned.
|
|
104
|
-
*
|
|
105
|
-
* Readings are MERGED rather than replaced, because there is more than one
|
|
106
|
-
* reader and they answer about different things: ffmpeg's banner states the
|
|
107
|
-
* frame size, the frame rate and the bitrate, while the container's own
|
|
108
|
-
* header states where its timeline begins — and a sidecar soundtrack is read
|
|
109
|
-
* for that one field alone. Replacing would mean the second reader erasing
|
|
110
|
-
* what the first found.
|
|
111
|
-
*
|
|
112
|
-
* Called again whenever a fresher reading arrives: on a cold torrent the
|
|
113
|
-
* first answer can be missing the duration, and the second is what has it.
|
|
114
|
-
*
|
|
115
|
-
* @param {object | null} mediaInfo
|
|
116
|
-
* @returns {this}
|
|
117
|
-
*/
|
|
118
|
-
learn(mediaInfo) {
|
|
119
|
-
if (mediaInfo && typeof mediaInfo === "object") {
|
|
120
|
-
this.media = { ...(this.media ?? {}), ...mediaInfo };
|
|
121
|
-
}
|
|
122
|
-
return this;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** @returns {number | null} */
|
|
126
|
-
get width() {
|
|
127
|
-
const value = Number(this.media?.width);
|
|
128
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** @returns {number | null} */
|
|
132
|
-
get height() {
|
|
133
|
-
const value = Number(this.media?.height);
|
|
134
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** @returns {number | null} */
|
|
138
|
-
get fps() {
|
|
139
|
-
const value = Number(this.media?.fps);
|
|
140
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** @returns {boolean} */
|
|
144
|
-
get isHdr() {
|
|
145
|
-
return this.media?.isHdr === true;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Where this file's own timeline begins. Zero when nothing said otherwise —
|
|
150
|
-
* which is what a container that states no start time means.
|
|
151
|
-
*
|
|
152
|
-
* @returns {number}
|
|
153
|
-
*/
|
|
154
|
-
get startTime() {
|
|
155
|
-
const value = Number(this.media?.startTime);
|
|
156
|
-
return Number.isFinite(value) ? value : 0;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** @returns {number | null} */
|
|
160
|
-
get durationSeconds() {
|
|
161
|
-
const value = Number(this.media?.durationSeconds);
|
|
162
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** @returns {boolean} */
|
|
166
|
-
get hasDuration() {
|
|
167
|
-
return this.durationSeconds !== null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* How many streams of each kind ffmpeg's own banner said this file carries.
|
|
172
|
-
* The one place the file says what it has, and what tells a failed run whose
|
|
173
|
-
* fault it was: an audio index past the end of the list is ours, no streams
|
|
174
|
-
* at all is the file's.
|
|
175
|
-
*
|
|
176
|
-
* @returns {object | null}
|
|
177
|
-
*/
|
|
178
|
-
get streamCounts() {
|
|
179
|
-
return this.media?.streamCounts ?? null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* What decoding this file costs per second, or null while its facts are
|
|
184
|
-
* incomplete.
|
|
185
|
-
*
|
|
186
|
-
* @returns {{ megapixelsPerSecond: number, megabitsPerSecond: number, codec: string, bitDepth: number | null } | null}
|
|
187
|
-
*/
|
|
188
|
-
get decode() {
|
|
189
|
-
return sourceDecodeCharacteristics(this.media);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* The address this proxy's own HTTP serves this file at.
|
|
194
|
-
*
|
|
195
|
-
* The base URL is a parameter because where this proxy listens is not a
|
|
196
|
-
* property of a film. `session` travels with it so the stream route can count
|
|
197
|
-
* the bytes it delivers against a session — that count is what tells a
|
|
198
|
-
* waiting browser the proxy is alive while nothing has been encoded yet,
|
|
199
|
-
* since the encoder's own progress cannot move before its first frame is
|
|
200
|
-
* decoded.
|
|
201
|
-
*
|
|
202
|
-
* @param {string} baseUrl - This proxy's own base, e.g. `http://127.0.0.1:9090`.
|
|
203
|
-
* @param {{ sessionId?: string }} [options]
|
|
204
|
-
* @returns {URL}
|
|
205
|
-
*/
|
|
206
|
-
streamUrl(baseUrl, { sessionId = "" } = {}) {
|
|
207
|
-
const url = new URL("/stream", `${baseUrl}/`);
|
|
208
|
-
url.searchParams.set("sourceKey", this.sourceKey);
|
|
209
|
-
if (sessionId.length > 0) {
|
|
210
|
-
url.searchParams.set("session", sessionId);
|
|
211
|
-
}
|
|
212
|
-
url.searchParams.set("fileIndex", String(this.fileIndex));
|
|
213
|
-
return url;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* The source files this proxy holds, one per file.
|
|
219
|
-
*
|
|
220
|
-
* Keyed by the torrent and the index inside it, which is the only pair that
|
|
221
|
-
* identifies a file — and the same key every cache about a file already used,
|
|
222
|
-
* spelled once here instead of at twenty call sites.
|
|
223
|
-
*/
|
|
224
|
-
export class SourceFiles {
|
|
225
|
-
/** @type {Map<string, SourceFile>} */
|
|
226
|
-
#byKey = new Map();
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* @param {string} sourceKey
|
|
230
|
-
* @param {number} fileIndex
|
|
231
|
-
* @returns {string}
|
|
232
|
-
*/
|
|
233
|
-
static keyFor(sourceKey, fileIndex) {
|
|
234
|
-
return `${sourceKey}:${fileIndex}`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* The file with this key, made if it is not held yet.
|
|
239
|
-
*
|
|
240
|
-
* @param {string} sourceKey
|
|
241
|
-
* @param {number} fileIndex
|
|
242
|
-
* @param {string} [name]
|
|
243
|
-
* @returns {SourceFile}
|
|
244
|
-
*/
|
|
245
|
-
get(sourceKey, fileIndex, name = "") {
|
|
246
|
-
const key = SourceFiles.keyFor(sourceKey, fileIndex);
|
|
247
|
-
let file = this.#byKey.get(key);
|
|
248
|
-
if (!file) {
|
|
249
|
-
file = new SourceFile({ sourceKey, fileIndex, name });
|
|
250
|
-
this.#byKey.set(key, file);
|
|
251
|
-
} else if (name.length > 0 && file.givenName.length === 0) {
|
|
252
|
-
// A name learned later is still this file's name. The first caller is
|
|
253
|
-
// sometimes a route that has an index and no name.
|
|
254
|
-
file.givenName = name.trim();
|
|
255
|
-
}
|
|
256
|
-
return file;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* @param {string} key
|
|
261
|
-
* @returns {SourceFile | null}
|
|
262
|
-
*/
|
|
263
|
-
peek(key) {
|
|
264
|
-
return this.#byKey.get(key) ?? null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Drop every file nobody names any more.
|
|
269
|
-
*
|
|
270
|
-
* A file holds a handle and one probe result — a few dozen numbers — so it is
|
|
271
|
-
* kept for as long as anything refers to it and dropped in one sweep rather
|
|
272
|
-
* than reference-counted. What must NOT happen is dropping one while a
|
|
273
|
-
* session still points at it: the facts would be read again, and a cold
|
|
274
|
-
* torrent would answer differently the second time.
|
|
275
|
-
*
|
|
276
|
-
* Held by the OBJECT rather than by its key, the same way the timelines and
|
|
277
|
-
* the outputs are swept: what must survive is what a live session points at,
|
|
278
|
-
* and that is a reference, not a string that happens to match one.
|
|
279
|
-
*
|
|
280
|
-
* @param {Set<SourceFile>} filesInUse
|
|
281
|
-
* @returns {number} How many were dropped.
|
|
282
|
-
*/
|
|
283
|
-
forgetUnused(filesInUse) {
|
|
284
|
-
let dropped = 0;
|
|
285
|
-
for (const [key, file] of [...this.#byKey.entries()]) {
|
|
286
|
-
if (!filesInUse.has(file)) {
|
|
287
|
-
this.#byKey.delete(key);
|
|
288
|
-
dropped += 1;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return dropped;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** @returns {number} */
|
|
295
|
-
get size() {
|
|
296
|
-
return this.#byKey.size;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file One file of one torrent, as this proxy reads it.
|
|
3
|
+
*
|
|
4
|
+
* Which torrent, which file inside it, what it is called, what a probe of it
|
|
5
|
+
* said, and the address this proxy's own HTTP serves it at. Every one of those
|
|
6
|
+
* is a fact about the FILE: it is the same for the picture and for every
|
|
7
|
+
* quality step of it, the same for one viewer and for five, and it does not
|
|
8
|
+
* change while anybody is watching.
|
|
9
|
+
*
|
|
10
|
+
* They used to be nine fields copied onto every session, and the copying was
|
|
11
|
+
* visible in the code that read them back: twenty places in the session manager
|
|
12
|
+
* assembled `${sourceKey}:${fileIndex}` by hand, through five different
|
|
13
|
+
* spellings of the same two values, to key the caches that are keyed by a file —
|
|
14
|
+
* the host timings, the download stats, the decode cost, the cut table. A file
|
|
15
|
+
* that knows its own key removes all twenty.
|
|
16
|
+
*
|
|
17
|
+
* **What this is NOT.** It is not the container: a container answers for what
|
|
18
|
+
* the format states about itself and needs bytes to do it. It is not the track:
|
|
19
|
+
* a track is one stream inside a container. This is the file as a HANDLE — how
|
|
20
|
+
* it is named, addressed and identified — plus the facts a probe of it
|
|
21
|
+
* returned, which arrive together and are cheapest to keep where the handle is.
|
|
22
|
+
*
|
|
23
|
+
* It is not the keyframe table either, and that one was here until 2026-09-15.
|
|
24
|
+
* A table has two states a bag of probe results cannot tell apart — read and
|
|
25
|
+
* empty, which makes every picture of this file a re-encode for ever, against
|
|
26
|
+
* not read yet, which is a shortage of bytes off the swarm — so it is an object
|
|
27
|
+
* of its own, `container/KeyframeTable.js`, owned by the layer that reads what a
|
|
28
|
+
* file states about itself.
|
|
29
|
+
*
|
|
30
|
+
* Nothing here reaches outward, the address included: the proxy's own base URL
|
|
31
|
+
* is passed in, because where this proxy listens is not a property of a film.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What decoding this source costs per second of it, and which measurement of
|
|
36
|
+
* this host applies.
|
|
37
|
+
*
|
|
38
|
+
* The codec and bit depth travel with the rates because they decide WHICH
|
|
39
|
+
* measurement applies: the model is fitted per codec family, and a video that
|
|
40
|
+
* has to be re-encoded is by definition one the browser could not play — HEVC,
|
|
41
|
+
* 10-bit — which is exactly where H.264 constants are wrong.
|
|
42
|
+
*
|
|
43
|
+
* @param {{ width: number | null, height: number | null, fps: number | null, bitrateKbps: number | null, codec?: string | null, bitDepth?: number | null } | null} mediaInfo
|
|
44
|
+
* @returns {{ megapixelsPerSecond: number, megabitsPerSecond: number, codec: string, bitDepth: number | null } | null}
|
|
45
|
+
*/
|
|
46
|
+
export function sourceDecodeCharacteristics(mediaInfo) {
|
|
47
|
+
const width = Number(mediaInfo?.width);
|
|
48
|
+
const height = Number(mediaInfo?.height);
|
|
49
|
+
const fps = Number(mediaInfo?.fps);
|
|
50
|
+
const kbps = Number(mediaInfo?.bitrateKbps);
|
|
51
|
+
if (!(width > 0) || !(height > 0) || !(fps > 0) || !(kbps > 0)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const depth = Number(mediaInfo?.bitDepth);
|
|
55
|
+
return {
|
|
56
|
+
megapixelsPerSecond: (width * height * fps) / 1e6,
|
|
57
|
+
megabitsPerSecond: kbps / 1000,
|
|
58
|
+
codec: typeof mediaInfo?.codec === "string" ? mediaInfo.codec : "",
|
|
59
|
+
bitDepth: Number.isFinite(depth) && depth > 0 ? depth : null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class SourceFile {
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} params
|
|
66
|
+
* @param {string} params.sourceKey - The registry's key for the torrent.
|
|
67
|
+
* @param {number} params.fileIndex - Zero-based index of the file in it.
|
|
68
|
+
* @param {string} [params.name] - The file's own name, for log lines.
|
|
69
|
+
*/
|
|
70
|
+
constructor({ sourceKey, fileIndex, name = "" }) {
|
|
71
|
+
this.sourceKey = String(sourceKey ?? "");
|
|
72
|
+
this.fileIndex = Number.isFinite(fileIndex) ? Number(fileIndex) : 0;
|
|
73
|
+
this.givenName = typeof name === "string" ? name.trim() : "";
|
|
74
|
+
/**
|
|
75
|
+
* What a probe of this file said. Null until one has answered — a cold
|
|
76
|
+
* torrent has no header yet, and "not read" is not the same as "zero".
|
|
77
|
+
*
|
|
78
|
+
* @type {object | null}
|
|
79
|
+
*/
|
|
80
|
+
this.media = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The key every cache about this file is keyed by.
|
|
85
|
+
*
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
get key() {
|
|
89
|
+
return `${this.sourceKey}:${this.fileIndex}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What to call this file in a log line. A file with no name is named by its
|
|
94
|
+
* index, so a line is never about a nameless thing.
|
|
95
|
+
*
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
get name() {
|
|
99
|
+
return this.givenName.length > 0 ? this.givenName : `file#${this.fileIndex}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Take in what a reading of this file returned.
|
|
104
|
+
*
|
|
105
|
+
* Readings are MERGED rather than replaced, because there is more than one
|
|
106
|
+
* reader and they answer about different things: ffmpeg's banner states the
|
|
107
|
+
* frame size, the frame rate and the bitrate, while the container's own
|
|
108
|
+
* header states where its timeline begins — and a sidecar soundtrack is read
|
|
109
|
+
* for that one field alone. Replacing would mean the second reader erasing
|
|
110
|
+
* what the first found.
|
|
111
|
+
*
|
|
112
|
+
* Called again whenever a fresher reading arrives: on a cold torrent the
|
|
113
|
+
* first answer can be missing the duration, and the second is what has it.
|
|
114
|
+
*
|
|
115
|
+
* @param {object | null} mediaInfo
|
|
116
|
+
* @returns {this}
|
|
117
|
+
*/
|
|
118
|
+
learn(mediaInfo) {
|
|
119
|
+
if (mediaInfo && typeof mediaInfo === "object") {
|
|
120
|
+
this.media = { ...(this.media ?? {}), ...mediaInfo };
|
|
121
|
+
}
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @returns {number | null} */
|
|
126
|
+
get width() {
|
|
127
|
+
const value = Number(this.media?.width);
|
|
128
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @returns {number | null} */
|
|
132
|
+
get height() {
|
|
133
|
+
const value = Number(this.media?.height);
|
|
134
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @returns {number | null} */
|
|
138
|
+
get fps() {
|
|
139
|
+
const value = Number(this.media?.fps);
|
|
140
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @returns {boolean} */
|
|
144
|
+
get isHdr() {
|
|
145
|
+
return this.media?.isHdr === true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Where this file's own timeline begins. Zero when nothing said otherwise —
|
|
150
|
+
* which is what a container that states no start time means.
|
|
151
|
+
*
|
|
152
|
+
* @returns {number}
|
|
153
|
+
*/
|
|
154
|
+
get startTime() {
|
|
155
|
+
const value = Number(this.media?.startTime);
|
|
156
|
+
return Number.isFinite(value) ? value : 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @returns {number | null} */
|
|
160
|
+
get durationSeconds() {
|
|
161
|
+
const value = Number(this.media?.durationSeconds);
|
|
162
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @returns {boolean} */
|
|
166
|
+
get hasDuration() {
|
|
167
|
+
return this.durationSeconds !== null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* How many streams of each kind ffmpeg's own banner said this file carries.
|
|
172
|
+
* The one place the file says what it has, and what tells a failed run whose
|
|
173
|
+
* fault it was: an audio index past the end of the list is ours, no streams
|
|
174
|
+
* at all is the file's.
|
|
175
|
+
*
|
|
176
|
+
* @returns {object | null}
|
|
177
|
+
*/
|
|
178
|
+
get streamCounts() {
|
|
179
|
+
return this.media?.streamCounts ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* What decoding this file costs per second, or null while its facts are
|
|
184
|
+
* incomplete.
|
|
185
|
+
*
|
|
186
|
+
* @returns {{ megapixelsPerSecond: number, megabitsPerSecond: number, codec: string, bitDepth: number | null } | null}
|
|
187
|
+
*/
|
|
188
|
+
get decode() {
|
|
189
|
+
return sourceDecodeCharacteristics(this.media);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The address this proxy's own HTTP serves this file at.
|
|
194
|
+
*
|
|
195
|
+
* The base URL is a parameter because where this proxy listens is not a
|
|
196
|
+
* property of a film. `session` travels with it so the stream route can count
|
|
197
|
+
* the bytes it delivers against a session — that count is what tells a
|
|
198
|
+
* waiting browser the proxy is alive while nothing has been encoded yet,
|
|
199
|
+
* since the encoder's own progress cannot move before its first frame is
|
|
200
|
+
* decoded.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} baseUrl - This proxy's own base, e.g. `http://127.0.0.1:9090`.
|
|
203
|
+
* @param {{ sessionId?: string }} [options]
|
|
204
|
+
* @returns {URL}
|
|
205
|
+
*/
|
|
206
|
+
streamUrl(baseUrl, { sessionId = "" } = {}) {
|
|
207
|
+
const url = new URL("/stream", `${baseUrl}/`);
|
|
208
|
+
url.searchParams.set("sourceKey", this.sourceKey);
|
|
209
|
+
if (sessionId.length > 0) {
|
|
210
|
+
url.searchParams.set("session", sessionId);
|
|
211
|
+
}
|
|
212
|
+
url.searchParams.set("fileIndex", String(this.fileIndex));
|
|
213
|
+
return url;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The source files this proxy holds, one per file.
|
|
219
|
+
*
|
|
220
|
+
* Keyed by the torrent and the index inside it, which is the only pair that
|
|
221
|
+
* identifies a file — and the same key every cache about a file already used,
|
|
222
|
+
* spelled once here instead of at twenty call sites.
|
|
223
|
+
*/
|
|
224
|
+
export class SourceFiles {
|
|
225
|
+
/** @type {Map<string, SourceFile>} */
|
|
226
|
+
#byKey = new Map();
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @param {string} sourceKey
|
|
230
|
+
* @param {number} fileIndex
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
static keyFor(sourceKey, fileIndex) {
|
|
234
|
+
return `${sourceKey}:${fileIndex}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The file with this key, made if it is not held yet.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} sourceKey
|
|
241
|
+
* @param {number} fileIndex
|
|
242
|
+
* @param {string} [name]
|
|
243
|
+
* @returns {SourceFile}
|
|
244
|
+
*/
|
|
245
|
+
get(sourceKey, fileIndex, name = "") {
|
|
246
|
+
const key = SourceFiles.keyFor(sourceKey, fileIndex);
|
|
247
|
+
let file = this.#byKey.get(key);
|
|
248
|
+
if (!file) {
|
|
249
|
+
file = new SourceFile({ sourceKey, fileIndex, name });
|
|
250
|
+
this.#byKey.set(key, file);
|
|
251
|
+
} else if (name.length > 0 && file.givenName.length === 0) {
|
|
252
|
+
// A name learned later is still this file's name. The first caller is
|
|
253
|
+
// sometimes a route that has an index and no name.
|
|
254
|
+
file.givenName = name.trim();
|
|
255
|
+
}
|
|
256
|
+
return file;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {string} key
|
|
261
|
+
* @returns {SourceFile | null}
|
|
262
|
+
*/
|
|
263
|
+
peek(key) {
|
|
264
|
+
return this.#byKey.get(key) ?? null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Drop every file nobody names any more.
|
|
269
|
+
*
|
|
270
|
+
* A file holds a handle and one probe result — a few dozen numbers — so it is
|
|
271
|
+
* kept for as long as anything refers to it and dropped in one sweep rather
|
|
272
|
+
* than reference-counted. What must NOT happen is dropping one while a
|
|
273
|
+
* session still points at it: the facts would be read again, and a cold
|
|
274
|
+
* torrent would answer differently the second time.
|
|
275
|
+
*
|
|
276
|
+
* Held by the OBJECT rather than by its key, the same way the timelines and
|
|
277
|
+
* the outputs are swept: what must survive is what a live session points at,
|
|
278
|
+
* and that is a reference, not a string that happens to match one.
|
|
279
|
+
*
|
|
280
|
+
* @param {Set<SourceFile>} filesInUse
|
|
281
|
+
* @returns {number} How many were dropped.
|
|
282
|
+
*/
|
|
283
|
+
forgetUnused(filesInUse) {
|
|
284
|
+
let dropped = 0;
|
|
285
|
+
for (const [key, file] of [...this.#byKey.entries()]) {
|
|
286
|
+
if (!filesInUse.has(file)) {
|
|
287
|
+
this.#byKey.delete(key);
|
|
288
|
+
dropped += 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return dropped;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** @returns {number} */
|
|
295
|
+
get size() {
|
|
296
|
+
return this.#byKey.size;
|
|
297
|
+
}
|
|
298
|
+
}
|