@torrent-tv/proxy 2.88.2 → 2.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2259 -2246
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +23 -4
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1150 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -254
- package/services/encode/quality/output-format.js +319 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +21 -6
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +298 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1244
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -0
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +13 -8
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +995 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +70 -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/episode-naming.test.js +131 -131
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/audio-frame-boundary.json +1 -0
- package/test/fmp4-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +270 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -0
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the torrent itself costs this machine, per megabyte it moves.
|
|
3
|
-
*
|
|
4
|
-
* Downloading, verifying every piece and pushing segments down a data channel
|
|
5
|
-
* are work on the same box as the encoder, and they scale with what is moved.
|
|
6
|
-
* The reading is taken only while nothing is encoding, because that is the one
|
|
7
|
-
* moment the spending needs no arithmetic to attribute: what the process uses
|
|
8
|
-
* then is the torrent's.
|
|
9
|
-
*
|
|
10
|
-
* Except that it is not all the torrent's, and that is what this file exists
|
|
11
|
-
* for. Measured on the addon host 2026-08-18, one session reported **145.4 ms
|
|
12
|
-
* per megabyte over 8.7 MB** and **23.1 ms per megabyte over 54 MB** — the same
|
|
13
|
-
* host, the same minute, a sixfold disagreement that follows the size of the
|
|
14
|
-
* interval rather than anything about the torrent. A process with nothing to do
|
|
15
|
-
* still runs its timers, its tunnel and its session sweeps, and that draw does
|
|
16
|
-
* not shrink when fewer megabytes move: divided by a small number it swamps the
|
|
17
|
-
* answer. A minimum-megabytes threshold stood against exactly this and did not
|
|
18
|
-
* hold, because a chosen number was standing in for a measured one.
|
|
19
|
-
*
|
|
20
|
-
* The draw is measurable. An interval in which nothing encodes and the torrents
|
|
21
|
-
* move no bytes at all costs whatever this process spends anyway; subtract that
|
|
22
|
-
* from an interval which did move bytes, and what remains is the torrent's.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The share of one core this process draws while it has nothing to do.
|
|
27
|
-
*
|
|
28
|
-
* @param {{ cpuSeconds: number, elapsedSeconds: number }} interval - CPU seconds
|
|
29
|
-
* already divided by the number of cores, so the figure is comparable with
|
|
30
|
-
* the wall seconds every other cost in the budget is stated in.
|
|
31
|
-
* @returns {number | null} Null when the interval cannot be divided.
|
|
32
|
-
*/
|
|
33
|
-
export function baseDrawFrom({ cpuSeconds, elapsedSeconds }) {
|
|
34
|
-
if (!Number.isFinite(cpuSeconds) || !Number.isFinite(elapsedSeconds) || !(elapsedSeconds > 0)) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
if (!(cpuSeconds >= 0)) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
return cpuSeconds / elapsedSeconds;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* What one megabyte cost, once the draw that would have been spent anyway is
|
|
45
|
-
* taken off.
|
|
46
|
-
*
|
|
47
|
-
* The draw is subtracted, so what is left is only as certain as the draw is.
|
|
48
|
-
* The draw's own readings disagree with each other by a measured amount, and
|
|
49
|
-
* over an interval that disagreement is worth `scatter × elapsed` seconds of
|
|
50
|
-
* CPU: a reading whose remainder is smaller than that measures the wobble in
|
|
51
|
-
* the subtraction rather than the torrent. This is what the removed
|
|
52
|
-
* minimum-megabytes threshold was reaching for, arrived at from the readings
|
|
53
|
-
* instead of from a chosen size — a small interval fails it because little was
|
|
54
|
-
* moved in it, and a large one passes on the same arithmetic.
|
|
55
|
-
*
|
|
56
|
-
* @param {{ cpuSeconds: number, elapsedSeconds: number, megabytes: number, baseDraw: number | null, drawScatter?: number | null }} interval
|
|
57
|
-
* @returns {number | null} Seconds of work per megabyte, or null when this
|
|
58
|
-
* interval cannot say — no base draw measured yet, nothing moved, or the
|
|
59
|
-
* interval spent no more than the draw and its own uncertainty account for.
|
|
60
|
-
*/
|
|
61
|
-
export function costPerMegabyteFrom({ cpuSeconds, elapsedSeconds, megabytes, baseDraw, drawScatter = null }) {
|
|
62
|
-
if (!Number.isFinite(cpuSeconds) || !Number.isFinite(elapsedSeconds) || !(elapsedSeconds > 0)) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
if (!Number.isFinite(megabytes) || !(megabytes > 0)) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
if (!Number.isFinite(baseDraw) || !(baseDraw >= 0)) {
|
|
69
|
-
// Nothing measured to subtract. Publishing the whole spending as the
|
|
70
|
-
// torrent's is what produced the 145 ms/MB reading, so this interval says
|
|
71
|
-
// nothing instead.
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
const attributable = cpuSeconds - baseDraw * elapsedSeconds;
|
|
75
|
-
if (!(attributable > 0)) {
|
|
76
|
-
// The draw accounts for everything this interval spent. Not a discovery
|
|
77
|
-
// that the torrent is free — a reading with nothing left in it.
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
const uncertainty = Number.isFinite(drawScatter) && drawScatter > 0 ? drawScatter * elapsedSeconds : 0;
|
|
81
|
-
if (!(attributable > uncertainty)) {
|
|
82
|
-
// What is left is inside the disagreement between the draw's own readings.
|
|
83
|
-
// Dividing it by a small number of megabytes is how 145 ms/MB was reported
|
|
84
|
-
// on a host that costs 23.
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
return attributable / megabytes;
|
|
88
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file What the torrent itself costs this machine, per megabyte it moves.
|
|
3
|
+
*
|
|
4
|
+
* Downloading, verifying every piece and pushing segments down a data channel
|
|
5
|
+
* are work on the same box as the encoder, and they scale with what is moved.
|
|
6
|
+
* The reading is taken only while nothing is encoding, because that is the one
|
|
7
|
+
* moment the spending needs no arithmetic to attribute: what the process uses
|
|
8
|
+
* then is the torrent's.
|
|
9
|
+
*
|
|
10
|
+
* Except that it is not all the torrent's, and that is what this file exists
|
|
11
|
+
* for. Measured on the addon host 2026-08-18, one session reported **145.4 ms
|
|
12
|
+
* per megabyte over 8.7 MB** and **23.1 ms per megabyte over 54 MB** — the same
|
|
13
|
+
* host, the same minute, a sixfold disagreement that follows the size of the
|
|
14
|
+
* interval rather than anything about the torrent. A process with nothing to do
|
|
15
|
+
* still runs its timers, its tunnel and its session sweeps, and that draw does
|
|
16
|
+
* not shrink when fewer megabytes move: divided by a small number it swamps the
|
|
17
|
+
* answer. A minimum-megabytes threshold stood against exactly this and did not
|
|
18
|
+
* hold, because a chosen number was standing in for a measured one.
|
|
19
|
+
*
|
|
20
|
+
* The draw is measurable. An interval in which nothing encodes and the torrents
|
|
21
|
+
* move no bytes at all costs whatever this process spends anyway; subtract that
|
|
22
|
+
* from an interval which did move bytes, and what remains is the torrent's.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The share of one core this process draws while it has nothing to do.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ cpuSeconds: number, elapsedSeconds: number }} interval - CPU seconds
|
|
29
|
+
* already divided by the number of cores, so the figure is comparable with
|
|
30
|
+
* the wall seconds every other cost in the budget is stated in.
|
|
31
|
+
* @returns {number | null} Null when the interval cannot be divided.
|
|
32
|
+
*/
|
|
33
|
+
export function baseDrawFrom({ cpuSeconds, elapsedSeconds }) {
|
|
34
|
+
if (!Number.isFinite(cpuSeconds) || !Number.isFinite(elapsedSeconds) || !(elapsedSeconds > 0)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!(cpuSeconds >= 0)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return cpuSeconds / elapsedSeconds;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What one megabyte cost, once the draw that would have been spent anyway is
|
|
45
|
+
* taken off.
|
|
46
|
+
*
|
|
47
|
+
* The draw is subtracted, so what is left is only as certain as the draw is.
|
|
48
|
+
* The draw's own readings disagree with each other by a measured amount, and
|
|
49
|
+
* over an interval that disagreement is worth `scatter × elapsed` seconds of
|
|
50
|
+
* CPU: a reading whose remainder is smaller than that measures the wobble in
|
|
51
|
+
* the subtraction rather than the torrent. This is what the removed
|
|
52
|
+
* minimum-megabytes threshold was reaching for, arrived at from the readings
|
|
53
|
+
* instead of from a chosen size — a small interval fails it because little was
|
|
54
|
+
* moved in it, and a large one passes on the same arithmetic.
|
|
55
|
+
*
|
|
56
|
+
* @param {{ cpuSeconds: number, elapsedSeconds: number, megabytes: number, baseDraw: number | null, drawScatter?: number | null }} interval
|
|
57
|
+
* @returns {number | null} Seconds of work per megabyte, or null when this
|
|
58
|
+
* interval cannot say — no base draw measured yet, nothing moved, or the
|
|
59
|
+
* interval spent no more than the draw and its own uncertainty account for.
|
|
60
|
+
*/
|
|
61
|
+
export function costPerMegabyteFrom({ cpuSeconds, elapsedSeconds, megabytes, baseDraw, drawScatter = null }) {
|
|
62
|
+
if (!Number.isFinite(cpuSeconds) || !Number.isFinite(elapsedSeconds) || !(elapsedSeconds > 0)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isFinite(megabytes) || !(megabytes > 0)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!Number.isFinite(baseDraw) || !(baseDraw >= 0)) {
|
|
69
|
+
// Nothing measured to subtract. Publishing the whole spending as the
|
|
70
|
+
// torrent's is what produced the 145 ms/MB reading, so this interval says
|
|
71
|
+
// nothing instead.
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const attributable = cpuSeconds - baseDraw * elapsedSeconds;
|
|
75
|
+
if (!(attributable > 0)) {
|
|
76
|
+
// The draw accounts for everything this interval spent. Not a discovery
|
|
77
|
+
// that the torrent is free — a reading with nothing left in it.
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const uncertainty = Number.isFinite(drawScatter) && drawScatter > 0 ? drawScatter * elapsedSeconds : 0;
|
|
81
|
+
if (!(attributable > uncertainty)) {
|
|
82
|
+
// What is left is inside the disagreement between the draw's own readings.
|
|
83
|
+
// Dividing it by a small number of megabytes is how 145 ms/MB was reported
|
|
84
|
+
// on a host that costs 23.
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return attributable / megabytes;
|
|
88
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
const ID_FILE = "proxy-identity.json";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Resolve this installation's registry id. Without a state directory the id
|
|
9
|
-
* remains ephemeral; with one, the id survives process restarts.
|
|
10
|
-
*
|
|
11
|
-
* @param {{ stateDir?: string, explicitId?: string, makeId?: () => string }} options
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
14
|
-
export function installationId({ stateDir, explicitId, makeId = randomUUID } = {}) {
|
|
15
|
-
const requestedId = typeof explicitId === "string" ? explicitId.trim() : "";
|
|
16
|
-
if (!stateDir) {
|
|
17
|
-
return requestedId || makeId();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const file = path.join(stateDir, ID_FILE);
|
|
21
|
-
let savedId = "";
|
|
22
|
-
if (existsSync(file)) {
|
|
23
|
-
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
24
|
-
savedId = typeof saved?.id === "string" ? saved.id.trim() : "";
|
|
25
|
-
if (!savedId) {
|
|
26
|
-
throw new Error(`Invalid proxy identity file: ${file}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const id = requestedId || savedId || makeId();
|
|
31
|
-
if (id !== savedId) {
|
|
32
|
-
mkdirSync(stateDir, { recursive: true });
|
|
33
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
34
|
-
writeFileSync(temporary, `${JSON.stringify({ id })}\n`, { mode: 0o600 });
|
|
35
|
-
renameSync(temporary, file);
|
|
36
|
-
}
|
|
37
|
-
return id;
|
|
38
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const ID_FILE = "proxy-identity.json";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve this installation's registry id. Without a state directory the id
|
|
9
|
+
* remains ephemeral; with one, the id survives process restarts.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ stateDir?: string, explicitId?: string, makeId?: () => string }} options
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function installationId({ stateDir, explicitId, makeId = randomUUID } = {}) {
|
|
15
|
+
const requestedId = typeof explicitId === "string" ? explicitId.trim() : "";
|
|
16
|
+
if (!stateDir) {
|
|
17
|
+
return requestedId || makeId();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const file = path.join(stateDir, ID_FILE);
|
|
21
|
+
let savedId = "";
|
|
22
|
+
if (existsSync(file)) {
|
|
23
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
24
|
+
savedId = typeof saved?.id === "string" ? saved.id.trim() : "";
|
|
25
|
+
if (!savedId) {
|
|
26
|
+
throw new Error(`Invalid proxy identity file: ${file}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const id = requestedId || savedId || makeId();
|
|
31
|
+
if (id !== savedId) {
|
|
32
|
+
mkdirSync(stateDir, { recursive: true });
|
|
33
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
34
|
+
writeFileSync(temporary, `${JSON.stringify({ id })}\n`, { mode: 0o600 });
|
|
35
|
+
renameSync(temporary, file);
|
|
36
|
+
}
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Container orchestrator — application layer over Container domain.
|
|
3
|
-
*
|
|
4
|
-
* Holds a per-file cache of Container instances (key sourceKey:fileIndex) so
|
|
5
|
-
* Tracks and keyframe index are read once per file, not per request.
|
|
6
|
-
* Delegates format detection to ContainerFactory. Transport-agnostic — takes
|
|
7
|
-
* readRange, knows nothing about torrents or HTTP.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* How much of a file's head its track table lives in.
|
|
14
|
-
*
|
|
15
|
-
* A fact of the FORMATS, not of the torrent: Matroska puts its Tracks element
|
|
16
|
-
* in the head and MP4's `moov` is there or pointed to from there. Whoever
|
|
17
|
-
* fetches bytes before asking is told this figure rather than choosing one.
|
|
18
|
-
*/
|
|
19
|
-
export const CONTAINER_HEAD_BYTES = 256 * 1024;
|
|
20
|
-
import { logger } from "../../utils/logger.js";
|
|
21
|
-
|
|
22
|
-
export class ContainerOrchestrator {
|
|
23
|
-
constructor() {
|
|
24
|
-
/** @type {Map<string, import("./container/Container.js").Container|null>} */
|
|
25
|
-
this.cache = new Map();
|
|
26
|
-
/** @type {Map<string, Promise<import("./container/Container.js").Container|null>>} */
|
|
27
|
-
this.pending = new Map();
|
|
28
|
-
/**
|
|
29
|
-
* What each file's container declares, once it has said anything.
|
|
30
|
-
*
|
|
31
|
-
* The container parses on every ask, so without this the header is read
|
|
32
|
-
* again for each question — and the track table is asked for the audio
|
|
33
|
-
* menu, the subtitle defaults and the video facts of one file. It used to
|
|
34
|
-
* be kept in the torrent thread (`torrent-worker/container-tracks.js`), which is where the
|
|
35
|
-
* parse used to happen; it is the same one cache in its new place, not a
|
|
36
|
-
* second one.
|
|
37
|
-
*
|
|
38
|
-
* @type {Map<string, import("./tracks/index.js").ContainerTrack[]>}
|
|
39
|
-
*/
|
|
40
|
-
this.tracks = new Map();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {object} params
|
|
45
|
-
* @param {string} params.sourceKey
|
|
46
|
-
* @param {number} params.fileIndex
|
|
47
|
-
* @param {(start:number,end:number)=>Promise<Buffer|null>} params.readRange
|
|
48
|
-
* @param {number} params.fileSize
|
|
49
|
-
* @param {string} [params.label]
|
|
50
|
-
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
51
|
-
*/
|
|
52
|
-
async getContainer({ sourceKey, fileIndex, readRange, fileSize, label = "" }) {
|
|
53
|
-
const key = `${sourceKey}:${fileIndex}`;
|
|
54
|
-
if (this.cache.has(key)) return this.cache.get(key);
|
|
55
|
-
if (this.pending.has(key)) return this.pending.get(key);
|
|
56
|
-
const p = ContainerFactory.create({ readRange, fileSize, label }).then((c) => {
|
|
57
|
-
this.cache.set(key, c);
|
|
58
|
-
this.pending.delete(key);
|
|
59
|
-
if (c) logger.info(`container: ${c.formatName} for "${label}"`);
|
|
60
|
-
else logger.info(`container: unknown for "${label}"`);
|
|
61
|
-
return c;
|
|
62
|
-
}).catch((e) => {
|
|
63
|
-
this.pending.delete(key);
|
|
64
|
-
logger.warn(`container: failed for "${label}": ${e?.message ?? e}`);
|
|
65
|
-
return null;
|
|
66
|
-
});
|
|
67
|
-
this.pending.set(key, p);
|
|
68
|
-
return p;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @param {object} params - same as getContainer
|
|
73
|
-
* @returns {Promise<import("./tracks/index.js").ContainerTrack[]>}
|
|
74
|
-
*/
|
|
75
|
-
async getTracks(params) {
|
|
76
|
-
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
77
|
-
const known = this.tracks.get(key);
|
|
78
|
-
if (known) {
|
|
79
|
-
return known;
|
|
80
|
-
}
|
|
81
|
-
const container = await this.getContainer(params);
|
|
82
|
-
if (!container) return [];
|
|
83
|
-
try {
|
|
84
|
-
const tracks = await container.readTracks();
|
|
85
|
-
// ONLY A NON-EMPTY READING IS KEPT. An empty one usually means the header
|
|
86
|
-
// has not arrived off the swarm yet, and caching that would answer "this
|
|
87
|
-
// file declares nothing" for the life of the process — which is what
|
|
88
|
-
// decides whether the viewer is offered a soundtrack at all.
|
|
89
|
-
if (Array.isArray(tracks) && tracks.length > 0) {
|
|
90
|
-
this.tracks.set(key, tracks);
|
|
91
|
-
return tracks;
|
|
92
|
-
}
|
|
93
|
-
return Array.isArray(tracks) ? tracks : [];
|
|
94
|
-
} catch (e) {
|
|
95
|
-
logger.warn(`container: readTracks failed for "${params.label}": ${e?.message ?? e}`);
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* What the file declares about itself as a whole — format, duration, and
|
|
102
|
-
* where its own timeline begins.
|
|
103
|
-
*
|
|
104
|
-
* Read once per file, like the track table beside it, and from the same 64 KB
|
|
105
|
-
* of header. A `null` field means the container does not declare it, which is
|
|
106
|
-
* a final answer about the container.
|
|
107
|
-
*
|
|
108
|
-
* @param {object} params - same as getContainer
|
|
109
|
-
* @returns {Promise<import("./container/Container.js").ContainerMediaInfo|null>}
|
|
110
|
-
*/
|
|
111
|
-
async getMediaInfo(params) {
|
|
112
|
-
const container = await this.getContainer(params);
|
|
113
|
-
if (!container) return null;
|
|
114
|
-
try {
|
|
115
|
-
return await container.readMediaInfo();
|
|
116
|
-
} catch (e) {
|
|
117
|
-
logger.warn(`container: readMediaInfo failed for "${params.label}": ${e?.message ?? e}`);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* @param {object} params - same as getContainer
|
|
124
|
-
* @returns {Promise<{times:number[],tolerance:number}|null>}
|
|
125
|
-
*/
|
|
126
|
-
async getKeyframeIndex(params) {
|
|
127
|
-
const container = await this.getContainer(params);
|
|
128
|
-
if (!container) return null;
|
|
129
|
-
try {
|
|
130
|
-
return await container.readKeyframeIndex();
|
|
131
|
-
} catch {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
forget(sourceKey, fileIndex) {
|
|
137
|
-
if (fileIndex === undefined) {
|
|
138
|
-
for (const k of [...this.cache.keys()]) if (k.startsWith(`${sourceKey}:`)) this.cache.delete(k);
|
|
139
|
-
for (const k of [...this.pending.keys()]) if (k.startsWith(`${sourceKey}:`)) this.pending.delete(k);
|
|
140
|
-
for (const k of [...this.tracks.keys()]) if (k.startsWith(`${sourceKey}:`)) this.tracks.delete(k);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
this.cache.delete(`${sourceKey}:${fileIndex}`);
|
|
144
|
-
this.pending.delete(`${sourceKey}:${fileIndex}`);
|
|
145
|
-
this.tracks.delete(`${sourceKey}:${fileIndex}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export const containerOrchestrator = new ContainerOrchestrator();
|
|
1
|
+
/**
|
|
2
|
+
* @file Container orchestrator — application layer over Container domain.
|
|
3
|
+
*
|
|
4
|
+
* Holds a per-file cache of Container instances (key sourceKey:fileIndex) so
|
|
5
|
+
* Tracks and keyframe index are read once per file, not per request.
|
|
6
|
+
* Delegates format detection to ContainerFactory. Transport-agnostic — takes
|
|
7
|
+
* readRange, knows nothing about torrents or HTTP.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How much of a file's head its track table lives in.
|
|
14
|
+
*
|
|
15
|
+
* A fact of the FORMATS, not of the torrent: Matroska puts its Tracks element
|
|
16
|
+
* in the head and MP4's `moov` is there or pointed to from there. Whoever
|
|
17
|
+
* fetches bytes before asking is told this figure rather than choosing one.
|
|
18
|
+
*/
|
|
19
|
+
export const CONTAINER_HEAD_BYTES = 256 * 1024;
|
|
20
|
+
import { logger } from "../../utils/logger.js";
|
|
21
|
+
|
|
22
|
+
export class ContainerOrchestrator {
|
|
23
|
+
constructor() {
|
|
24
|
+
/** @type {Map<string, import("./container/Container.js").Container|null>} */
|
|
25
|
+
this.cache = new Map();
|
|
26
|
+
/** @type {Map<string, Promise<import("./container/Container.js").Container|null>>} */
|
|
27
|
+
this.pending = new Map();
|
|
28
|
+
/**
|
|
29
|
+
* What each file's container declares, once it has said anything.
|
|
30
|
+
*
|
|
31
|
+
* The container parses on every ask, so without this the header is read
|
|
32
|
+
* again for each question — and the track table is asked for the audio
|
|
33
|
+
* menu, the subtitle defaults and the video facts of one file. It used to
|
|
34
|
+
* be kept in the torrent thread (`torrent-worker/container-tracks.js`), which is where the
|
|
35
|
+
* parse used to happen; it is the same one cache in its new place, not a
|
|
36
|
+
* second one.
|
|
37
|
+
*
|
|
38
|
+
* @type {Map<string, import("./tracks/index.js").ContainerTrack[]>}
|
|
39
|
+
*/
|
|
40
|
+
this.tracks = new Map();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} params
|
|
45
|
+
* @param {string} params.sourceKey
|
|
46
|
+
* @param {number} params.fileIndex
|
|
47
|
+
* @param {(start:number,end:number)=>Promise<Buffer|null>} params.readRange
|
|
48
|
+
* @param {number} params.fileSize
|
|
49
|
+
* @param {string} [params.label]
|
|
50
|
+
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
51
|
+
*/
|
|
52
|
+
async getContainer({ sourceKey, fileIndex, readRange, fileSize, label = "" }) {
|
|
53
|
+
const key = `${sourceKey}:${fileIndex}`;
|
|
54
|
+
if (this.cache.has(key)) return this.cache.get(key);
|
|
55
|
+
if (this.pending.has(key)) return this.pending.get(key);
|
|
56
|
+
const p = ContainerFactory.create({ readRange, fileSize, label }).then((c) => {
|
|
57
|
+
this.cache.set(key, c);
|
|
58
|
+
this.pending.delete(key);
|
|
59
|
+
if (c) logger.info(`container: ${c.formatName} for "${label}"`);
|
|
60
|
+
else logger.info(`container: unknown for "${label}"`);
|
|
61
|
+
return c;
|
|
62
|
+
}).catch((e) => {
|
|
63
|
+
this.pending.delete(key);
|
|
64
|
+
logger.warn(`container: failed for "${label}": ${e?.message ?? e}`);
|
|
65
|
+
return null;
|
|
66
|
+
});
|
|
67
|
+
this.pending.set(key, p);
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {object} params - same as getContainer
|
|
73
|
+
* @returns {Promise<import("./tracks/index.js").ContainerTrack[]>}
|
|
74
|
+
*/
|
|
75
|
+
async getTracks(params) {
|
|
76
|
+
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
77
|
+
const known = this.tracks.get(key);
|
|
78
|
+
if (known) {
|
|
79
|
+
return known;
|
|
80
|
+
}
|
|
81
|
+
const container = await this.getContainer(params);
|
|
82
|
+
if (!container) return [];
|
|
83
|
+
try {
|
|
84
|
+
const tracks = await container.readTracks();
|
|
85
|
+
// ONLY A NON-EMPTY READING IS KEPT. An empty one usually means the header
|
|
86
|
+
// has not arrived off the swarm yet, and caching that would answer "this
|
|
87
|
+
// file declares nothing" for the life of the process — which is what
|
|
88
|
+
// decides whether the viewer is offered a soundtrack at all.
|
|
89
|
+
if (Array.isArray(tracks) && tracks.length > 0) {
|
|
90
|
+
this.tracks.set(key, tracks);
|
|
91
|
+
return tracks;
|
|
92
|
+
}
|
|
93
|
+
return Array.isArray(tracks) ? tracks : [];
|
|
94
|
+
} catch (e) {
|
|
95
|
+
logger.warn(`container: readTracks failed for "${params.label}": ${e?.message ?? e}`);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* What the file declares about itself as a whole — format, duration, and
|
|
102
|
+
* where its own timeline begins.
|
|
103
|
+
*
|
|
104
|
+
* Read once per file, like the track table beside it, and from the same 64 KB
|
|
105
|
+
* of header. A `null` field means the container does not declare it, which is
|
|
106
|
+
* a final answer about the container.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} params - same as getContainer
|
|
109
|
+
* @returns {Promise<import("./container/Container.js").ContainerMediaInfo|null>}
|
|
110
|
+
*/
|
|
111
|
+
async getMediaInfo(params) {
|
|
112
|
+
const container = await this.getContainer(params);
|
|
113
|
+
if (!container) return null;
|
|
114
|
+
try {
|
|
115
|
+
return await container.readMediaInfo();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
logger.warn(`container: readMediaInfo failed for "${params.label}": ${e?.message ?? e}`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {object} params - same as getContainer
|
|
124
|
+
* @returns {Promise<{times:number[],tolerance:number}|null>}
|
|
125
|
+
*/
|
|
126
|
+
async getKeyframeIndex(params) {
|
|
127
|
+
const container = await this.getContainer(params);
|
|
128
|
+
if (!container) return null;
|
|
129
|
+
try {
|
|
130
|
+
return await container.readKeyframeIndex();
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
forget(sourceKey, fileIndex) {
|
|
137
|
+
if (fileIndex === undefined) {
|
|
138
|
+
for (const k of [...this.cache.keys()]) if (k.startsWith(`${sourceKey}:`)) this.cache.delete(k);
|
|
139
|
+
for (const k of [...this.pending.keys()]) if (k.startsWith(`${sourceKey}:`)) this.pending.delete(k);
|
|
140
|
+
for (const k of [...this.tracks.keys()]) if (k.startsWith(`${sourceKey}:`)) this.tracks.delete(k);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.cache.delete(`${sourceKey}:${fileIndex}`);
|
|
144
|
+
this.pending.delete(`${sourceKey}:${fileIndex}`);
|
|
145
|
+
this.tracks.delete(`${sourceKey}:${fileIndex}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const containerOrchestrator = new ContainerOrchestrator();
|