@torrent-tv/proxy 2.86.0 → 2.87.0
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/CHANGELOG.md +2138 -1915
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +745 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +931 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/wire-outputs.js +958 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -45
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -515
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +40 -8
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -202
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9608
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/services/viewer/link-readings.js +0 -56
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { Worker } from "node:worker_threads";
|
|
18
18
|
import { Readable } from "node:stream";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { logger, writeAlreadyDecided } from "
|
|
20
|
+
import { logger, writeAlreadyDecided } from "../../../utils/logger.js";
|
|
21
21
|
import { createCaller, createReceiveStream } from "./channel.js";
|
|
22
22
|
import { Command, Event } from "./protocol.js";
|
|
23
23
|
|
|
@@ -107,21 +107,22 @@ export class TorrentWorkerClient {
|
|
|
107
107
|
wholeFiles = new Map();
|
|
108
108
|
|
|
109
109
|
/** @type {(event: { sourceKey: string, fileIndex: number, trackIndex: number, cues: object[], language: string }) => void} */
|
|
110
|
-
|
|
110
|
+
/** @type {(event: { sourceKey: string, fileIndexes: number[] }) => void} */
|
|
111
|
+
#onPiecesArrived;
|
|
111
112
|
|
|
112
113
|
/**
|
|
113
|
-
* @param {{
|
|
114
|
+
* @param {{ memoryBytes?: number, stateDir?: string, onPiecesArrived?: (event: object) => void }} [options]
|
|
114
115
|
*/
|
|
115
|
-
constructor({
|
|
116
|
+
constructor({ memoryBytes, stateDir, onPiecesArrived } = {}) {
|
|
116
117
|
this.#worker = new Worker(fileURLToPath(WORKER_URL), {
|
|
117
118
|
// `stateDir` travels because the worker writes heap snapshots of its own
|
|
118
119
|
// isolate there. It cannot choose a directory any other way: a worker may
|
|
119
120
|
// not change the process's working directory, and the isolate that has
|
|
120
121
|
// died three times is the one no snapshot has ever been taken of.
|
|
121
|
-
workerData: {
|
|
122
|
+
workerData: { memoryBytes, stateDir }
|
|
122
123
|
});
|
|
123
124
|
this.#caller = createCaller(this.#worker);
|
|
124
|
-
this.#
|
|
125
|
+
this.#onPiecesArrived = onPiecesArrived ?? (() => undefined);
|
|
125
126
|
|
|
126
127
|
this.#worker.on("message", (message) => {
|
|
127
128
|
// A failed read must fail its stream. This is checked BEFORE the caller
|
|
@@ -232,14 +233,12 @@ export class TorrentWorkerClient {
|
|
|
232
233
|
name: message.name
|
|
233
234
|
});
|
|
234
235
|
break;
|
|
235
|
-
case Event.
|
|
236
|
-
|
|
236
|
+
case Event.PIECES_ARRIVED:
|
|
237
|
+
// ANNOUNCED by the thread that owns the swarm, acted on here: the
|
|
238
|
+
// subtitle walk is this thread's and needs to know a piece arrived.
|
|
239
|
+
this.#onPiecesArrived({
|
|
237
240
|
sourceKey: message.sourceKey,
|
|
238
|
-
|
|
239
|
-
trackIndex: message.trackIndex,
|
|
240
|
-
cues: message.cues,
|
|
241
|
-
language: message.language,
|
|
242
|
-
cursor: message.cursor
|
|
241
|
+
fileIndexes: Array.isArray(message.fileIndexes) ? message.fileIndexes : []
|
|
243
242
|
});
|
|
244
243
|
break;
|
|
245
244
|
default:
|
|
@@ -336,75 +335,54 @@ export class TorrentWorkerClient {
|
|
|
336
335
|
* @param {{ sourceKey: string, fileIndex: number }} params
|
|
337
336
|
* @returns {Promise<{ tracks: object[] }>}
|
|
338
337
|
*/
|
|
339
|
-
/**
|
|
340
|
-
* Fetch one whole file in the room the viewer's own reading leaves.
|
|
341
|
-
*
|
|
342
|
-
* @param {{ sourceKey: string, fileIndex: number }} params
|
|
343
|
-
* @returns {Promise<{ started: boolean }>}
|
|
344
|
-
*/
|
|
345
|
-
async fillFile({ sourceKey, fileIndex }) {
|
|
346
|
-
return this.#caller.call(Command.FILL_FILE, { sourceKey, fileIndex });
|
|
347
|
-
}
|
|
348
338
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
339
|
+
|
|
340
|
+
|
|
352
341
|
|
|
353
342
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* Asked of the picture for the flags ffmpeg's banner does not carry, and of a
|
|
357
|
-
* soundtrack shipped as its own file beside it — the same question about a
|
|
358
|
-
* different file, which is why there is one command rather than two.
|
|
343
|
+
* Start fetching the region a viewer is about to resume at.
|
|
359
344
|
*
|
|
360
|
-
* @param {{ sourceKey: string, fileIndex: number }} params
|
|
361
|
-
* @returns {Promise<{
|
|
345
|
+
* @param {{ sourceKey: string, fileIndex: number, positionSeconds: number }} params
|
|
346
|
+
* @returns {Promise<{ started: boolean }>}
|
|
362
347
|
*/
|
|
363
|
-
async getContainerTracks({ sourceKey, fileIndex }) {
|
|
364
|
-
return this.#caller.call(Command.CONTAINER_TRACKS, { sourceKey, fileIndex });
|
|
365
|
-
}
|
|
366
|
-
|
|
367
348
|
/**
|
|
368
|
-
*
|
|
369
|
-
* timeline begins.
|
|
349
|
+
* Fetch one whole file in the room the viewer's own reading leaves.
|
|
370
350
|
*
|
|
371
351
|
* @param {{ sourceKey: string, fileIndex: number }} params
|
|
372
|
-
* @returns {Promise<{
|
|
352
|
+
* @returns {Promise<{ started: boolean }>}
|
|
373
353
|
*/
|
|
374
|
-
async
|
|
375
|
-
return this.#caller.call(Command.
|
|
354
|
+
async fillFile({ sourceKey, fileIndex }) {
|
|
355
|
+
return this.#caller.call(Command.FILL_FILE, { sourceKey, fileIndex });
|
|
376
356
|
}
|
|
377
357
|
|
|
378
358
|
/**
|
|
379
|
-
*
|
|
359
|
+
* The byte ranges of one file the torrent holds WHOLE.
|
|
380
360
|
*
|
|
381
361
|
* @param {{ sourceKey: string, fileIndex: number }} params
|
|
382
|
-
* @returns {Promise<
|
|
362
|
+
* @returns {Promise<Array<[number, number]>>}
|
|
383
363
|
*/
|
|
384
|
-
async
|
|
385
|
-
|
|
364
|
+
async heldRanges({ sourceKey, fileIndex }) {
|
|
365
|
+
const answer = await this.#caller.call(Command.HELD_RANGES, { sourceKey, fileIndex });
|
|
366
|
+
return Array.isArray(answer?.ranges) ? answer.ranges : [];
|
|
386
367
|
}
|
|
387
368
|
|
|
388
369
|
/**
|
|
389
|
-
*
|
|
370
|
+
* Bytes of a range the torrent already holds. Never fetched: null where they
|
|
371
|
+
* are not there.
|
|
390
372
|
*
|
|
391
|
-
* @param {{ sourceKey: string, fileIndex: number,
|
|
392
|
-
* @returns {Promise<
|
|
373
|
+
* @param {{ sourceKey: string, fileIndex: number, start: number, end: number }} params
|
|
374
|
+
* @returns {Promise<Buffer | null>}
|
|
393
375
|
*/
|
|
394
|
-
async
|
|
395
|
-
|
|
376
|
+
async readHeld({ sourceKey, fileIndex, start, end }) {
|
|
377
|
+
const answer = await this.#caller.call(Command.READ_HELD, { sourceKey, fileIndex, start, end });
|
|
378
|
+
return answer?.bytes ? Buffer.from(answer.bytes) : null;
|
|
396
379
|
}
|
|
397
380
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
*
|
|
401
|
-
* @param {{ sourceKey: string, fileIndex: number, trackNumber: number }} params
|
|
402
|
-
* @returns {Promise<object>}
|
|
403
|
-
*/
|
|
404
|
-
async getSubtitleCues({ sourceKey, fileIndex, trackNumber }) {
|
|
405
|
-
return this.#caller.call(Command.SUBTITLE_CUES, { sourceKey, fileIndex, trackNumber });
|
|
381
|
+
async warmResumePosition({ sourceKey, fileIndex, positionSeconds, durationSeconds }) {
|
|
382
|
+
return this.#caller.call(Command.WARM_POSITION, { sourceKey, fileIndex, positionSeconds, durationSeconds });
|
|
406
383
|
}
|
|
407
384
|
|
|
385
|
+
|
|
408
386
|
/**
|
|
409
387
|
* Bytes every torrent on the worker has moved, downloaded and uploaded apart.
|
|
410
388
|
*
|
|
@@ -684,6 +662,47 @@ export class TorrentWorkerClient {
|
|
|
684
662
|
*/
|
|
685
663
|
spilledBytes = 0;
|
|
686
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Tell the piece stores on the torrent thread their share of MEMORY.
|
|
667
|
+
*
|
|
668
|
+
* The same owner divides memory and disk, because the two trade: pieces that
|
|
669
|
+
* do not fit in memory are spilled to disk, so what the stores are given here
|
|
670
|
+
* decides how much disk they need there.
|
|
671
|
+
*
|
|
672
|
+
* @param {number} bytes
|
|
673
|
+
* @returns {Promise<{ held: number, wanted: number }>}
|
|
674
|
+
*/
|
|
675
|
+
async allowMemoryBytes(bytes) {
|
|
676
|
+
try {
|
|
677
|
+
const claim = await this.#caller.call(Command.MEMORY_ALLOWANCE, { bytes });
|
|
678
|
+
this.memoryClaim = {
|
|
679
|
+
held: Number(claim?.held) || 0,
|
|
680
|
+
wanted: Number(claim?.wanted) || 0
|
|
681
|
+
};
|
|
682
|
+
} catch {
|
|
683
|
+
// The thread is gone or busy; the next revision says it again.
|
|
684
|
+
}
|
|
685
|
+
return this.memoryClaim ?? { held: 0, wanted: 0 };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Tell the whole files on the torrent thread their share of the disk.
|
|
690
|
+
*
|
|
691
|
+
* Same shape as the spilled pieces, and for the same reason: they hold bytes
|
|
692
|
+
* on this machine and the owner of the disk is on the other thread.
|
|
693
|
+
*
|
|
694
|
+
* @param {number} bytes
|
|
695
|
+
* @returns {Promise<number>} What they hold after it.
|
|
696
|
+
*/
|
|
697
|
+
async allowWholeFileBytes(bytes) {
|
|
698
|
+
try {
|
|
699
|
+
this.wholeFileBytes = Number(await this.#caller.call(Command.WHOLE_FILES_ALLOWANCE, { bytes })) || 0;
|
|
700
|
+
} catch {
|
|
701
|
+
// The thread is gone or busy; the next revision says it again.
|
|
702
|
+
}
|
|
703
|
+
return this.wholeFileBytes ?? 0;
|
|
704
|
+
}
|
|
705
|
+
|
|
687
706
|
/**
|
|
688
707
|
* Say how much disk the spilled pieces on this thread may take between them.
|
|
689
708
|
*
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What of a file the torrent already HOLDS, and reading it without
|
|
3
|
+
* asking the swarm for anything.
|
|
4
|
+
*
|
|
5
|
+
* Two questions, and both are the torrent's own: which pieces have arrived is
|
|
6
|
+
* its bitfield, and reading a piece it has is its store. They are here because
|
|
7
|
+
* of that and for no other reason — everything else that used to keep them
|
|
8
|
+
* company, the subtitle plan and the cue walk and the cursor a browser follows,
|
|
9
|
+
* is what a FILE says about itself and moved to the thread the sessions are on
|
|
10
|
+
* (2026-09-15).
|
|
11
|
+
*
|
|
12
|
+
* **Why the subtitle walk cannot use the ordinary range read.** That one
|
|
13
|
+
* declares demand: it registers a window, steers the swarm toward the piece it
|
|
14
|
+
* is stopped on, and waits. Right for a viewer waiting on a segment, wrong
|
|
15
|
+
* here — switching subtitles on must never pull bytes the viewer is not waiting
|
|
16
|
+
* for, which is the rule the walk has kept since it was written. So these two
|
|
17
|
+
* read the store and say "not here" rather than fetching.
|
|
18
|
+
*
|
|
19
|
+
* **The held ranges are answered as a LIST, not asked piece by piece.** The
|
|
20
|
+
* walk asks "is this cluster downloaded" for every cluster of the file — on the
|
|
21
|
+
* field files that is hundreds of questions per pass, and a pass runs every
|
|
22
|
+
* three seconds. One list per pass costs one message; the same answers asked
|
|
23
|
+
* one at a time cost hundreds of round trips for a walk that is meant to be
|
|
24
|
+
* free when there is nothing new to find.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The byte ranges of one file that are downloaded WHOLE.
|
|
29
|
+
*
|
|
30
|
+
* Whole because a piece is the unit the swarm delivers and the unit the
|
|
31
|
+
* bitfield counts: a range that is half a piece short cannot be read, so
|
|
32
|
+
* reporting it would be reporting bytes that are not there.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} torrent
|
|
35
|
+
* @param {number} fileIndex
|
|
36
|
+
* @returns {Array<[number, number]>} Ascending, non-overlapping, inclusive
|
|
37
|
+
* offsets WITHIN the file. Empty when nothing of it has arrived, or when the
|
|
38
|
+
* torrent cannot say — which reads the same way: nothing may be walked yet.
|
|
39
|
+
*/
|
|
40
|
+
export function heldRangesOf(torrent, fileIndex) {
|
|
41
|
+
const file = torrent?.files?.[fileIndex];
|
|
42
|
+
const pieceLength = Number(torrent?.pieceLength);
|
|
43
|
+
if (!file || !Number.isFinite(pieceLength) || pieceLength <= 0 || !torrent?.bitfield) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const offset = Number(file.offset) || 0;
|
|
47
|
+
const length = Number(file.length) || 0;
|
|
48
|
+
if (!(length > 0)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const first = Math.floor(offset / pieceLength);
|
|
52
|
+
const last = Math.floor((offset + length - 1) / pieceLength);
|
|
53
|
+
/** @type {Array<[number, number]>} */
|
|
54
|
+
const ranges = [];
|
|
55
|
+
let runStart = -1;
|
|
56
|
+
for (let index = first; index <= last; index += 1) {
|
|
57
|
+
if (torrent.bitfield.get(index)) {
|
|
58
|
+
if (runStart < 0) {
|
|
59
|
+
runStart = index;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (runStart >= 0) {
|
|
64
|
+
ranges.push(rangeWithin(runStart, index - 1, pieceLength, offset, length));
|
|
65
|
+
runStart = -1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (runStart >= 0) {
|
|
69
|
+
ranges.push(rangeWithin(runStart, last, pieceLength, offset, length));
|
|
70
|
+
}
|
|
71
|
+
return ranges;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A run of whole pieces, as offsets within the file.
|
|
76
|
+
*
|
|
77
|
+
* @param {number} firstPiece
|
|
78
|
+
* @param {number} lastPiece
|
|
79
|
+
* @param {number} pieceLength
|
|
80
|
+
* @param {number} offset - Where the file begins in the torrent.
|
|
81
|
+
* @param {number} length - How long the file is.
|
|
82
|
+
* @returns {[number, number]}
|
|
83
|
+
*/
|
|
84
|
+
function rangeWithin(firstPiece, lastPiece, pieceLength, offset, length) {
|
|
85
|
+
const start = Math.max(0, firstPiece * pieceLength - offset);
|
|
86
|
+
const end = Math.min(length - 1, (lastPiece + 1) * pieceLength - 1 - offset);
|
|
87
|
+
return [start, end];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* How long a read of bytes the torrent already holds may take before it is
|
|
92
|
+
* given up.
|
|
93
|
+
*
|
|
94
|
+
* Not a measurement and nothing is derived from it: such a read either answers
|
|
95
|
+
* or it does not, and this is the point past which it is presumed lost — so
|
|
96
|
+
* that one stream which never ends cannot hold a file's walk, and with it the
|
|
97
|
+
* browser's own request for its subtitles, for the rest of the session.
|
|
98
|
+
*/
|
|
99
|
+
const READ_ABANDON_MS = 30_000;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read a byte range of a file straight from the store, without asking the swarm
|
|
103
|
+
* for anything.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} torrent
|
|
106
|
+
* @param {number} fileIndex
|
|
107
|
+
* @param {number} start
|
|
108
|
+
* @param {number} end - Inclusive.
|
|
109
|
+
* @param {{ info: Function }} [logger]
|
|
110
|
+
* @returns {Promise<Buffer | null>}
|
|
111
|
+
*/
|
|
112
|
+
export function readHeldBytes(torrent, fileIndex, start, end, logger = null) {
|
|
113
|
+
const file = torrent?.files?.[fileIndex];
|
|
114
|
+
if (!file || !(end >= start) || !(start >= 0)) {
|
|
115
|
+
return Promise.resolve(null);
|
|
116
|
+
}
|
|
117
|
+
const last = Math.min(end, Number(file.length) - 1);
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const chunks = [];
|
|
120
|
+
let stream;
|
|
121
|
+
try {
|
|
122
|
+
stream = file.createReadStream({ start, end: last });
|
|
123
|
+
} catch {
|
|
124
|
+
resolve(null);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
let settled = false;
|
|
128
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
129
|
+
let abandon = null;
|
|
130
|
+
const settle = (value) => {
|
|
131
|
+
if (settled) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
settled = true;
|
|
135
|
+
if (abandon !== null) {
|
|
136
|
+
clearTimeout(abandon);
|
|
137
|
+
}
|
|
138
|
+
if (value === null) {
|
|
139
|
+
stream.destroy?.();
|
|
140
|
+
}
|
|
141
|
+
resolve(value);
|
|
142
|
+
};
|
|
143
|
+
abandon = setTimeout(() => {
|
|
144
|
+
logger?.info(
|
|
145
|
+
`subtitles: a read of ${start}-${last} in "${String(file.name).slice(0, 40)}" ` +
|
|
146
|
+
`did not finish in ${READ_ABANDON_MS / 1000}s and was given up`
|
|
147
|
+
);
|
|
148
|
+
settle(null);
|
|
149
|
+
}, READ_ABANDON_MS);
|
|
150
|
+
abandon.unref?.();
|
|
151
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
152
|
+
stream.on("end", () => settle(Buffer.concat(chunks)));
|
|
153
|
+
stream.on("error", () => settle(null));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Imported for its side effect, and imported FIRST by `worker.js` — ES module
|
|
5
5
|
* bodies run in import order, so registering the hook here happens before
|
|
6
|
-
* `torrent-pool.js` pulls in WebTorrent, which is what reaches
|
|
6
|
+
* `torrent/torrent-pool.js` pulls in WebTorrent, which is what reaches
|
|
7
7
|
* `@thaunknown/simple-peer` and, through it, `webrtc-polyfill`.
|
|
8
8
|
*
|
|
9
9
|
* Scope is deliberately narrow. The hook lives in the worker's isolate, so the
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* with those bytes — not when they were sent, because nothing was sent.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { pieceStoreOf } from "../piece-store-of.js";
|
|
24
24
|
import { bytesOf, Urgency, urgencyName } from "../demand/index.js";
|
|
25
25
|
import { demandFor } from "../download/registry.js";
|
|
26
|
-
import { logger } from "
|
|
26
|
+
import { logger } from "../../../utils/logger.js";
|
|
27
27
|
import {
|
|
28
28
|
askFastestWiresFor,
|
|
29
29
|
canPlaceRequests,
|
|
@@ -144,7 +144,7 @@ export function pieceSupply(torrent, pieceIndex) {
|
|
|
144
144
|
*
|
|
145
145
|
* @param {import("webtorrent").Torrent} torrent
|
|
146
146
|
* @param {number} index
|
|
147
|
-
* @param {{ isCancelled: () => boolean }} cancellation
|
|
147
|
+
* @param {{ isCancelled: () => boolean, onCancel?: (listener: () => void) => () => void }} cancellation
|
|
148
148
|
* @returns {Promise<void>}
|
|
149
149
|
*/
|
|
150
150
|
function whenPieceReady(torrent, index, cancellation) {
|
|
@@ -164,17 +164,21 @@ function whenPieceReady(torrent, index, cancellation) {
|
|
|
164
164
|
cleanup();
|
|
165
165
|
reject(new Error(`Torrent went away while waiting for piece ${index}.`));
|
|
166
166
|
};
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
167
|
+
// A superseded seek destroys the read, and without this the wait would
|
|
168
|
+
// outlive it and hold a pin. The read's own stream says so the moment it
|
|
169
|
+
// happens; it was polled every 250 ms before. A cancellation that cannot
|
|
170
|
+
// announce itself is one that is never cancelled (a test's plain object).
|
|
171
|
+
if (cancellation.isCancelled()) {
|
|
172
|
+
reject(new Error(`Read cancelled while waiting for piece ${index}.`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const stopListening = cancellation.onCancel?.(() => {
|
|
176
|
+
cleanup();
|
|
177
|
+
reject(new Error(`Read cancelled while waiting for piece ${index}.`));
|
|
178
|
+
}) ?? (() => {});
|
|
175
179
|
|
|
176
180
|
function cleanup() {
|
|
177
|
-
|
|
181
|
+
stopListening();
|
|
178
182
|
torrent.removeListener("verified", onVerified);
|
|
179
183
|
torrent.removeListener("close", onDestroyed);
|
|
180
184
|
}
|
|
@@ -301,20 +305,8 @@ export function supplyFiguresFor(infoHash, fileName, segmentSeconds) {
|
|
|
301
305
|
const waitsBySteering = new Map();
|
|
302
306
|
|
|
303
307
|
/**
|
|
304
|
-
* Record one wait against the
|
|
305
|
-
*
|
|
306
|
-
* What this replaces: until 2026-09-02 each read was assigned at random to one
|
|
307
|
-
* of two ways of claiming — one band or four — and the waits were sorted by
|
|
308
|
-
* which. The comparison never decided anything, and could not: the split halved
|
|
309
|
-
* the sample for each arm, so on 2026-08-28 there were nine reads in one arm and
|
|
310
|
-
* three in the other and the ten-wait threshold was never reached in either; on
|
|
311
|
-
* 2026-08-29 the two arms printed together for the first and only time, as forty
|
|
312
|
-
* waits against one.
|
|
313
|
-
*
|
|
314
|
-
* This is the more useful question anyway. A wait belongs to a LEVEL, and the
|
|
315
|
-
* level says whether a width is wrong rather than whether the whole scheme is:
|
|
316
|
-
* long waits in the band being watched mean the urgent window is too narrow,
|
|
317
|
-
* long waits further out mean the lead is.
|
|
308
|
+
* Record one wait against the priority level of the requested piece. The level
|
|
309
|
+
* identifies which part of the priority map needs more lead time.
|
|
318
310
|
*
|
|
319
311
|
* @param {string} key
|
|
320
312
|
* @param {number} waitedMs
|
|
@@ -536,7 +528,7 @@ export async function* readFragments({
|
|
|
536
528
|
cancellation,
|
|
537
529
|
windowBytes = READ_WINDOW_BYTES
|
|
538
530
|
}) {
|
|
539
|
-
const store =
|
|
531
|
+
const store = pieceStoreOf(torrent);
|
|
540
532
|
if (!store) {
|
|
541
533
|
throw new Error("This torrent is not backed by a shared piece store.");
|
|
542
534
|
}
|
|
@@ -649,9 +641,8 @@ export async function* readFragments({
|
|
|
649
641
|
/**
|
|
650
642
|
* State one band as a need, in bytes.
|
|
651
643
|
*
|
|
652
|
-
* The
|
|
653
|
-
*
|
|
654
|
-
* not disturb what was said about the tail.
|
|
644
|
+
* The urgency is part of the claimant key, so this blocked-read claim can be
|
|
645
|
+
* withdrawn without changing the priority map's claims for the same file.
|
|
655
646
|
*
|
|
656
647
|
* @param {{ from: number, to: number, urgency: number }} band
|
|
657
648
|
* @returns {void}
|