@torrent-tv/proxy 2.88.4 → 2.88.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2269 -2258
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/calibration/audio/NOTICE.md +19 -0
- package/assets/calibration/audio/aac.m4a +0 -0
- package/assets/calibration/audio/ac3.ac3 +0 -0
- package/assets/calibration/audio/alac.m4a +0 -0
- package/assets/calibration/audio/dts.dts +0 -0
- package/assets/calibration/audio/eac3.eac3 +0 -0
- package/assets/calibration/audio/flac.flac +0 -0
- package/assets/calibration/audio/manifest.json +98 -0
- package/assets/calibration/audio/mp2.mp2 +0 -0
- package/assets/calibration/audio/mp3.mp3 +0 -0
- package/assets/calibration/audio/opus.opus +0 -0
- package/assets/calibration/audio/pcm.wav +0 -0
- package/assets/calibration/audio/truehd.thd +0 -0
- package/assets/calibration/audio/vorbis.ogg +0 -0
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -411
- package/docs/browser-proxy-contract.md +155 -152
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +56 -0
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/build-audio-samples.mjs +31 -0
- package/scripts/render-run-graph.js +173 -173
- package/server.js +730 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1239 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -783
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2282
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/audio-calibration.js +67 -0
- package/services/encode/audio-work-rate.js +15 -0
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +24 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1167 -1150
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -349
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +755 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +458 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +12 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +299 -298
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -609
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1344
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +786 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +516 -708
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1020 -995
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1937 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1255 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2688 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-calibration.test.js +23 -0
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-presentation-clock.test.js +162 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -112
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +273 -270
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +483 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,217 +1,217 @@
|
|
|
1
|
-
import { logger } from "../../../../utils/logger.js";
|
|
2
|
-
import { TEXT_SUBTITLE_SIDECAR_EXTENSIONS } from "../../../../services/torrent/files.js";
|
|
3
|
-
import { contentsOf } from "../../../../services/torrent/Contents.js";
|
|
4
|
-
|
|
5
|
-
const metadataWarmups = new Map();
|
|
6
|
-
const METADATA_RETRY_MS = 2500;
|
|
7
|
-
|
|
8
|
-
function startMetadataWarmup({ sourceKey, items, preferredFileIndex, userAgent, playbackPlanner, torrentPool, torrent }) {
|
|
9
|
-
if (!playbackPlanner || !Array.isArray(items) || items.length === 0) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
let state = metadataWarmups.get(sourceKey);
|
|
13
|
-
if (!state) {
|
|
14
|
-
state = { pending: new Set(), preferredFileIndex: null, running: false, items: [] };
|
|
15
|
-
metadataWarmups.set(sourceKey, state);
|
|
16
|
-
}
|
|
17
|
-
state.items = items;
|
|
18
|
-
for (const item of items) {
|
|
19
|
-
if (Number.isInteger(item?.fileIndex)) {
|
|
20
|
-
state.pending.add(item.fileIndex);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
if (Number.isInteger(preferredFileIndex) && state.pending.has(preferredFileIndex)) {
|
|
24
|
-
state.preferredFileIndex = preferredFileIndex;
|
|
25
|
-
}
|
|
26
|
-
if (state.running) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
state.running = true;
|
|
30
|
-
void (async () => {
|
|
31
|
-
try {
|
|
32
|
-
while (state.pending.size > 0) {
|
|
33
|
-
const fileIndex = state.preferredFileIndex !== null && state.pending.has(state.preferredFileIndex)
|
|
34
|
-
? state.preferredFileIndex
|
|
35
|
-
: state.pending.values().next().value;
|
|
36
|
-
state.preferredFileIndex = null;
|
|
37
|
-
if (!Number.isInteger(fileIndex)) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
const item = state.items.find((entry) => entry.fileIndex === fileIndex);
|
|
41
|
-
if (!item) {
|
|
42
|
-
state.pending.delete(fileIndex);
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const sidecars = contentsOf(torrent).sidecarsOf(fileIndex);
|
|
46
|
-
const sidecarFiles = [...sidecars.audio, ...sidecars.subtitles];
|
|
47
|
-
await Promise.all(sidecarFiles.map((file) =>
|
|
48
|
-
torrentPool.prefetchFileEdges(torrent, file.fileIndex, {
|
|
49
|
-
tailBytes: 0,
|
|
50
|
-
headBytes: TEXT_SUBTITLE_SIDECAR_EXTENSIONS.has(file.extension)
|
|
51
|
-
? Math.max(1, file.length)
|
|
52
|
-
: undefined,
|
|
53
|
-
timeoutMs: 10_000
|
|
54
|
-
}).catch((error) => {
|
|
55
|
-
logger.info(`warm ${sourceKey.slice(0, 8)}:${fileIndex}: sidecar metadata not ready for "${file.name}": ${error?.message ?? error}`);
|
|
56
|
-
})
|
|
57
|
-
));
|
|
58
|
-
const plan = await playbackPlanner.getPlan({
|
|
59
|
-
sourceKey,
|
|
60
|
-
fileIndex,
|
|
61
|
-
userAgent,
|
|
62
|
-
maxWaitMs: 0,
|
|
63
|
-
background: true
|
|
64
|
-
}).catch((error) => {
|
|
65
|
-
logger.info(`warm ${sourceKey.slice(0, 8)}:${fileIndex}: media metadata not ready: ${error?.message ?? error}`);
|
|
66
|
-
return null;
|
|
67
|
-
});
|
|
68
|
-
if (plan && !plan.pending) {
|
|
69
|
-
state.pending.delete(fileIndex);
|
|
70
|
-
} else if (torrent.files?.[fileIndex]?.done === true) {
|
|
71
|
-
state.pending.delete(fileIndex);
|
|
72
|
-
}
|
|
73
|
-
if (state.pending.size > 0) {
|
|
74
|
-
await new Promise((resolve) => setTimeout(resolve, METADATA_RETRY_MS));
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch (error) {
|
|
78
|
-
logger.warn(`warm ${sourceKey.slice(0, 8)}: metadata queue stopped: ${error?.message ?? error}`);
|
|
79
|
-
} finally {
|
|
80
|
-
state.running = false;
|
|
81
|
-
if (state.pending.size === 0) {
|
|
82
|
-
metadataWarmups.delete(sourceKey);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
})();
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Start fetching a source before anyone asks to play it.
|
|
91
|
-
*
|
|
92
|
-
* POST /api/sources/:sourceKey/warm { fileIndex?: number, positionSeconds?: number, userAgent?: string }
|
|
93
|
-
*
|
|
94
|
-
* Everything a torrent must do before the first byte of video can be served
|
|
95
|
-
* takes seconds and none of it depends on the viewer: announce to the
|
|
96
|
-
* trackers, connect to peers, be unchoked by them, and fetch the two pieces at
|
|
97
|
-
* the file's edges that the codec probe reads. Measured 2026-08-04 on a cold
|
|
98
|
-
* 7.4 GB torrent: 6.7 s of the 10.3 s before playback was those two pieces
|
|
99
|
-
* arriving, with the swarm ramping from nothing.
|
|
100
|
-
*
|
|
101
|
-
* That work used to begin only when a file had been chosen, because it was
|
|
102
|
-
* buried inside the playback plan. It can begin as soon as the viewer has
|
|
103
|
-
* picked a TORRENT — while they are still reading the list of episodes — and
|
|
104
|
-
* then most or all of it has happened by the time they choose.
|
|
105
|
-
*
|
|
106
|
-
* Returns as soon as the work is under way. The caller is not waiting for a
|
|
107
|
-
* result; it is only saying "you may start". Failures are logged and answered
|
|
108
|
-
* as `started: false` rather than as an error, because nothing is broken if a
|
|
109
|
-
* warm-up does not happen — the ordinary path still does all of it.
|
|
110
|
-
*
|
|
111
|
-
* @param {import("fastify").FastifyRequest} req
|
|
112
|
-
* @param {import("fastify").FastifyReply} reply
|
|
113
|
-
* @param {{
|
|
114
|
-
* sourceRegistry: ReturnType<import("../../../../store/source-registry.js").createSourceRegistry>,
|
|
115
|
-
* torrentPool: import("../../../../services/torrent/torrent-pool.js").TorrentPool,
|
|
116
|
-
* playbackPlanner?: ReturnType<import("../../../../services/media/playback-planner.js").createPlaybackPlanner>,
|
|
117
|
-
* durationOf?: ({ sourceKey: string, fileIndex: number }) => Promise<number | null>
|
|
118
|
-
* }} deps
|
|
119
|
-
* @returns {Promise<void>}
|
|
120
|
-
*/
|
|
121
|
-
export async function handleApiSourceWarmPost(req, reply, { sourceRegistry, torrentPool, playbackPlanner = null, durationOf = null }) {
|
|
122
|
-
const sourceKey = typeof req.params.sourceKey === "string" ? req.params.sourceKey.trim() : "";
|
|
123
|
-
if (!sourceKey) {
|
|
124
|
-
return reply.code(400).send({ error: "sourceKey is required." });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const sourceRecord = sourceRegistry.get(sourceKey);
|
|
128
|
-
if (!sourceRecord) {
|
|
129
|
-
return reply.code(404).send({ error: "Source key was not found." });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
|
|
133
|
-
const requestedIndex = Number(body.fileIndex);
|
|
134
|
-
const fileIndex = Number.isInteger(requestedIndex) && requestedIndex >= 0 ? requestedIndex : null;
|
|
135
|
-
// Where the viewer is about to resume, if they are resuming. The edges below
|
|
136
|
-
// are what the codec probe reads; this is what the VIEWER will read, and until
|
|
137
|
-
// now nothing asked for it before the encoder did.
|
|
138
|
-
const requestedPosition = Number(body.positionSeconds);
|
|
139
|
-
const positionSeconds = Number.isFinite(requestedPosition) && requestedPosition > 0
|
|
140
|
-
? requestedPosition
|
|
141
|
-
: 0;
|
|
142
|
-
const userAgent = typeof body.userAgent === "string" ? body.userAgent : "";
|
|
143
|
-
|
|
144
|
-
// Adding the torrent is what announces to the trackers and starts connecting
|
|
145
|
-
// to peers, and it is also what a magnet needs in order to fetch its
|
|
146
|
-
// metadata. It is awaited because everything else needs the torrent object,
|
|
147
|
-
// and because until it resolves there is nothing to report.
|
|
148
|
-
let torrent;
|
|
149
|
-
try {
|
|
150
|
-
torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
-
logger.warn(`warm ${sourceKey.slice(0, 8)}: could not add the torrent: ${message}`);
|
|
154
|
-
return reply.send({ started: false, swarm: false, edges: false });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const contents = contentsOf(torrent);
|
|
158
|
-
const named = fileIndex !== null && torrent.files?.[fileIndex] ? fileIndex : null;
|
|
159
|
-
if (named !== null) {
|
|
160
|
-
const selectedSidecars = contents.sidecarsOf(named);
|
|
161
|
-
for (const file of [...selectedSidecars.audio, ...selectedSidecars.subtitles]) {
|
|
162
|
-
void torrentPool.prefetchFileEdges(torrent, file.fileIndex, {
|
|
163
|
-
tailBytes: 0,
|
|
164
|
-
headBytes: TEXT_SUBTITLE_SIDECAR_EXTENSIONS.has(file.extension)
|
|
165
|
-
? Math.max(1, file.length)
|
|
166
|
-
: undefined,
|
|
167
|
-
timeoutMs: 10_000,
|
|
168
|
-
awaited: true
|
|
169
|
-
}).catch((error) => {
|
|
170
|
-
logger.info(`warm ${sourceKey.slice(0, 8)}:${named}: selected sidecar metadata not ready for "${file.name}": ${error?.message ?? error}`);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const orderedItems = named === null
|
|
175
|
-
? contents.items
|
|
176
|
-
: [...contents.items.filter((item) => item.fileIndex === named), ...contents.items.filter((item) => item.fileIndex !== named)];
|
|
177
|
-
const metadataStarted = startMetadataWarmup({
|
|
178
|
-
sourceKey,
|
|
179
|
-
items: orderedItems,
|
|
180
|
-
preferredFileIndex: named,
|
|
181
|
-
userAgent,
|
|
182
|
-
playbackPlanner,
|
|
183
|
-
torrentPool,
|
|
184
|
-
torrent
|
|
185
|
-
});
|
|
186
|
-
const sidecarCount = new Set(orderedItems.flatMap((item) => {
|
|
187
|
-
const matched = contents.sidecarsOf(item.fileIndex);
|
|
188
|
-
return [...matched.audio, ...matched.subtitles].map((file) => file.fileIndex);
|
|
189
|
-
})).size;
|
|
190
|
-
const fillStarted = typeof torrentPool.fillTorrent === "function"
|
|
191
|
-
? await torrentPool.fillTorrent(torrent).catch((error) => {
|
|
192
|
-
logger.warn(`warm ${sourceKey.slice(0, 8)}: whole-torrent fill could not start: ${error?.message ?? error}`);
|
|
193
|
-
return false;
|
|
194
|
-
})
|
|
195
|
-
: false;
|
|
196
|
-
if (named !== null && positionSeconds > 0 && typeof torrentPool.warmResumePosition === "function") {
|
|
197
|
-
void Promise.resolve(
|
|
198
|
-
(typeof durationOf === "function"
|
|
199
|
-
? durationOf({ sourceKey, fileIndex: named })
|
|
200
|
-
: Promise.resolve(null)
|
|
201
|
-
).then((durationSeconds) =>
|
|
202
|
-
torrentPool.warmResumePosition(torrent, named, positionSeconds, durationSeconds)
|
|
203
|
-
)
|
|
204
|
-
).catch((error) => {
|
|
205
|
-
logger.warn(`warm ${sourceKey.slice(0, 8)}: the viewer's position failed: ${error?.message ?? error}`);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
logger.info(
|
|
210
|
-
`warm ${sourceKey.slice(0, 8)}: swarm started for "${torrent.name}"` +
|
|
211
|
-
`, metadata for ${contents.items.length} video file(s) ${metadataStarted ? "queued" : "unavailable"}` +
|
|
212
|
-
`, ${sidecarCount} related audio/subtitle file(s)` +
|
|
213
|
-
(fillStarted ? ", whole-torrent fill enabled at TAIL urgency" : ", whole-torrent fill unavailable")
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
return reply.send({ started: true, swarm: true, edges: metadataStarted, sidecars: sidecarCount, fill: fillStarted });
|
|
217
|
-
}
|
|
1
|
+
import { logger } from "../../../../utils/logger.js";
|
|
2
|
+
import { TEXT_SUBTITLE_SIDECAR_EXTENSIONS } from "../../../../services/torrent/files.js";
|
|
3
|
+
import { contentsOf } from "../../../../services/torrent/Contents.js";
|
|
4
|
+
|
|
5
|
+
const metadataWarmups = new Map();
|
|
6
|
+
const METADATA_RETRY_MS = 2500;
|
|
7
|
+
|
|
8
|
+
function startMetadataWarmup({ sourceKey, items, preferredFileIndex, userAgent, playbackPlanner, torrentPool, torrent }) {
|
|
9
|
+
if (!playbackPlanner || !Array.isArray(items) || items.length === 0) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
let state = metadataWarmups.get(sourceKey);
|
|
13
|
+
if (!state) {
|
|
14
|
+
state = { pending: new Set(), preferredFileIndex: null, running: false, items: [] };
|
|
15
|
+
metadataWarmups.set(sourceKey, state);
|
|
16
|
+
}
|
|
17
|
+
state.items = items;
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
if (Number.isInteger(item?.fileIndex)) {
|
|
20
|
+
state.pending.add(item.fileIndex);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (Number.isInteger(preferredFileIndex) && state.pending.has(preferredFileIndex)) {
|
|
24
|
+
state.preferredFileIndex = preferredFileIndex;
|
|
25
|
+
}
|
|
26
|
+
if (state.running) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
state.running = true;
|
|
30
|
+
void (async () => {
|
|
31
|
+
try {
|
|
32
|
+
while (state.pending.size > 0) {
|
|
33
|
+
const fileIndex = state.preferredFileIndex !== null && state.pending.has(state.preferredFileIndex)
|
|
34
|
+
? state.preferredFileIndex
|
|
35
|
+
: state.pending.values().next().value;
|
|
36
|
+
state.preferredFileIndex = null;
|
|
37
|
+
if (!Number.isInteger(fileIndex)) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
const item = state.items.find((entry) => entry.fileIndex === fileIndex);
|
|
41
|
+
if (!item) {
|
|
42
|
+
state.pending.delete(fileIndex);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const sidecars = contentsOf(torrent).sidecarsOf(fileIndex);
|
|
46
|
+
const sidecarFiles = [...sidecars.audio, ...sidecars.subtitles];
|
|
47
|
+
await Promise.all(sidecarFiles.map((file) =>
|
|
48
|
+
torrentPool.prefetchFileEdges(torrent, file.fileIndex, {
|
|
49
|
+
tailBytes: 0,
|
|
50
|
+
headBytes: TEXT_SUBTITLE_SIDECAR_EXTENSIONS.has(file.extension)
|
|
51
|
+
? Math.max(1, file.length)
|
|
52
|
+
: undefined,
|
|
53
|
+
timeoutMs: 10_000
|
|
54
|
+
}).catch((error) => {
|
|
55
|
+
logger.info(`warm ${sourceKey.slice(0, 8)}:${fileIndex}: sidecar metadata not ready for "${file.name}": ${error?.message ?? error}`);
|
|
56
|
+
})
|
|
57
|
+
));
|
|
58
|
+
const plan = await playbackPlanner.getPlan({
|
|
59
|
+
sourceKey,
|
|
60
|
+
fileIndex,
|
|
61
|
+
userAgent,
|
|
62
|
+
maxWaitMs: 0,
|
|
63
|
+
background: true
|
|
64
|
+
}).catch((error) => {
|
|
65
|
+
logger.info(`warm ${sourceKey.slice(0, 8)}:${fileIndex}: media metadata not ready: ${error?.message ?? error}`);
|
|
66
|
+
return null;
|
|
67
|
+
});
|
|
68
|
+
if (plan && !plan.pending) {
|
|
69
|
+
state.pending.delete(fileIndex);
|
|
70
|
+
} else if (torrent.files?.[fileIndex]?.done === true) {
|
|
71
|
+
state.pending.delete(fileIndex);
|
|
72
|
+
}
|
|
73
|
+
if (state.pending.size > 0) {
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, METADATA_RETRY_MS));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
logger.warn(`warm ${sourceKey.slice(0, 8)}: metadata queue stopped: ${error?.message ?? error}`);
|
|
79
|
+
} finally {
|
|
80
|
+
state.running = false;
|
|
81
|
+
if (state.pending.size === 0) {
|
|
82
|
+
metadataWarmups.delete(sourceKey);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Start fetching a source before anyone asks to play it.
|
|
91
|
+
*
|
|
92
|
+
* POST /api/sources/:sourceKey/warm { fileIndex?: number, positionSeconds?: number, userAgent?: string }
|
|
93
|
+
*
|
|
94
|
+
* Everything a torrent must do before the first byte of video can be served
|
|
95
|
+
* takes seconds and none of it depends on the viewer: announce to the
|
|
96
|
+
* trackers, connect to peers, be unchoked by them, and fetch the two pieces at
|
|
97
|
+
* the file's edges that the codec probe reads. Measured 2026-08-04 on a cold
|
|
98
|
+
* 7.4 GB torrent: 6.7 s of the 10.3 s before playback was those two pieces
|
|
99
|
+
* arriving, with the swarm ramping from nothing.
|
|
100
|
+
*
|
|
101
|
+
* That work used to begin only when a file had been chosen, because it was
|
|
102
|
+
* buried inside the playback plan. It can begin as soon as the viewer has
|
|
103
|
+
* picked a TORRENT — while they are still reading the list of episodes — and
|
|
104
|
+
* then most or all of it has happened by the time they choose.
|
|
105
|
+
*
|
|
106
|
+
* Returns as soon as the work is under way. The caller is not waiting for a
|
|
107
|
+
* result; it is only saying "you may start". Failures are logged and answered
|
|
108
|
+
* as `started: false` rather than as an error, because nothing is broken if a
|
|
109
|
+
* warm-up does not happen — the ordinary path still does all of it.
|
|
110
|
+
*
|
|
111
|
+
* @param {import("fastify").FastifyRequest} req
|
|
112
|
+
* @param {import("fastify").FastifyReply} reply
|
|
113
|
+
* @param {{
|
|
114
|
+
* sourceRegistry: ReturnType<import("../../../../store/source-registry.js").createSourceRegistry>,
|
|
115
|
+
* torrentPool: import("../../../../services/torrent/torrent-pool.js").TorrentPool,
|
|
116
|
+
* playbackPlanner?: ReturnType<import("../../../../services/media/playback-planner.js").createPlaybackPlanner>,
|
|
117
|
+
* durationOf?: ({ sourceKey: string, fileIndex: number }) => Promise<number | null>
|
|
118
|
+
* }} deps
|
|
119
|
+
* @returns {Promise<void>}
|
|
120
|
+
*/
|
|
121
|
+
export async function handleApiSourceWarmPost(req, reply, { sourceRegistry, torrentPool, playbackPlanner = null, durationOf = null }) {
|
|
122
|
+
const sourceKey = typeof req.params.sourceKey === "string" ? req.params.sourceKey.trim() : "";
|
|
123
|
+
if (!sourceKey) {
|
|
124
|
+
return reply.code(400).send({ error: "sourceKey is required." });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sourceRecord = sourceRegistry.get(sourceKey);
|
|
128
|
+
if (!sourceRecord) {
|
|
129
|
+
return reply.code(404).send({ error: "Source key was not found." });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const body = req.body && typeof req.body === "object" && !Array.isArray(req.body) ? req.body : {};
|
|
133
|
+
const requestedIndex = Number(body.fileIndex);
|
|
134
|
+
const fileIndex = Number.isInteger(requestedIndex) && requestedIndex >= 0 ? requestedIndex : null;
|
|
135
|
+
// Where the viewer is about to resume, if they are resuming. The edges below
|
|
136
|
+
// are what the codec probe reads; this is what the VIEWER will read, and until
|
|
137
|
+
// now nothing asked for it before the encoder did.
|
|
138
|
+
const requestedPosition = Number(body.positionSeconds);
|
|
139
|
+
const positionSeconds = Number.isFinite(requestedPosition) && requestedPosition > 0
|
|
140
|
+
? requestedPosition
|
|
141
|
+
: 0;
|
|
142
|
+
const userAgent = typeof body.userAgent === "string" ? body.userAgent : "";
|
|
143
|
+
|
|
144
|
+
// Adding the torrent is what announces to the trackers and starts connecting
|
|
145
|
+
// to peers, and it is also what a magnet needs in order to fetch its
|
|
146
|
+
// metadata. It is awaited because everything else needs the torrent object,
|
|
147
|
+
// and because until it resolves there is nothing to report.
|
|
148
|
+
let torrent;
|
|
149
|
+
try {
|
|
150
|
+
torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
logger.warn(`warm ${sourceKey.slice(0, 8)}: could not add the torrent: ${message}`);
|
|
154
|
+
return reply.send({ started: false, swarm: false, edges: false });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const contents = contentsOf(torrent);
|
|
158
|
+
const named = fileIndex !== null && torrent.files?.[fileIndex] ? fileIndex : null;
|
|
159
|
+
if (named !== null) {
|
|
160
|
+
const selectedSidecars = contents.sidecarsOf(named);
|
|
161
|
+
for (const file of [...selectedSidecars.audio, ...selectedSidecars.subtitles]) {
|
|
162
|
+
void torrentPool.prefetchFileEdges(torrent, file.fileIndex, {
|
|
163
|
+
tailBytes: 0,
|
|
164
|
+
headBytes: TEXT_SUBTITLE_SIDECAR_EXTENSIONS.has(file.extension)
|
|
165
|
+
? Math.max(1, file.length)
|
|
166
|
+
: undefined,
|
|
167
|
+
timeoutMs: 10_000,
|
|
168
|
+
awaited: true
|
|
169
|
+
}).catch((error) => {
|
|
170
|
+
logger.info(`warm ${sourceKey.slice(0, 8)}:${named}: selected sidecar metadata not ready for "${file.name}": ${error?.message ?? error}`);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const orderedItems = named === null
|
|
175
|
+
? contents.items
|
|
176
|
+
: [...contents.items.filter((item) => item.fileIndex === named), ...contents.items.filter((item) => item.fileIndex !== named)];
|
|
177
|
+
const metadataStarted = startMetadataWarmup({
|
|
178
|
+
sourceKey,
|
|
179
|
+
items: orderedItems,
|
|
180
|
+
preferredFileIndex: named,
|
|
181
|
+
userAgent,
|
|
182
|
+
playbackPlanner,
|
|
183
|
+
torrentPool,
|
|
184
|
+
torrent
|
|
185
|
+
});
|
|
186
|
+
const sidecarCount = new Set(orderedItems.flatMap((item) => {
|
|
187
|
+
const matched = contents.sidecarsOf(item.fileIndex);
|
|
188
|
+
return [...matched.audio, ...matched.subtitles].map((file) => file.fileIndex);
|
|
189
|
+
})).size;
|
|
190
|
+
const fillStarted = typeof torrentPool.fillTorrent === "function"
|
|
191
|
+
? await torrentPool.fillTorrent(torrent).catch((error) => {
|
|
192
|
+
logger.warn(`warm ${sourceKey.slice(0, 8)}: whole-torrent fill could not start: ${error?.message ?? error}`);
|
|
193
|
+
return false;
|
|
194
|
+
})
|
|
195
|
+
: false;
|
|
196
|
+
if (named !== null && positionSeconds > 0 && typeof torrentPool.warmResumePosition === "function") {
|
|
197
|
+
void Promise.resolve(
|
|
198
|
+
(typeof durationOf === "function"
|
|
199
|
+
? durationOf({ sourceKey, fileIndex: named })
|
|
200
|
+
: Promise.resolve(null)
|
|
201
|
+
).then((durationSeconds) =>
|
|
202
|
+
torrentPool.warmResumePosition(torrent, named, positionSeconds, durationSeconds)
|
|
203
|
+
)
|
|
204
|
+
).catch((error) => {
|
|
205
|
+
logger.warn(`warm ${sourceKey.slice(0, 8)}: the viewer's position failed: ${error?.message ?? error}`);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
logger.info(
|
|
210
|
+
`warm ${sourceKey.slice(0, 8)}: swarm started for "${torrent.name}"` +
|
|
211
|
+
`, metadata for ${contents.items.length} video file(s) ${metadataStarted ? "queued" : "unavailable"}` +
|
|
212
|
+
`, ${sidecarCount} related audio/subtitle file(s)` +
|
|
213
|
+
(fillStarted ? ", whole-torrent fill enabled at TAIL urgency" : ", whole-torrent fill unavailable")
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return reply.send({ started: true, swarm: true, edges: metadataStarted, sidecars: sidecarCount, fill: fillStarted });
|
|
217
|
+
}
|