@torrent-tv/proxy 2.88.4 → 2.88.5
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 +2266 -2259
- 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 +1237 -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 +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 +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 +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 +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 +785 -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 +338 -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,142 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How well the priority map is being served, measured where somebody waits.
|
|
3
|
-
*
|
|
4
|
-
* The map says what matters most. Nothing said whether what mattered most was
|
|
5
|
-
* actually delivered first — so "is the prioritisation any good" had no answer
|
|
6
|
-
* of any kind, for either of the two things that read the map.
|
|
7
|
-
*
|
|
8
|
-
* The measure is the same for both, and it is a wait recorded against the rank
|
|
9
|
-
* the map gave the thing waited for AT THE MOMENT it was asked for. Read that
|
|
10
|
-
* way it says which part is wrong rather than whether the whole scheme is:
|
|
11
|
-
*
|
|
12
|
-
* - long waits at the TOP rank mean the urgent zone is not being served first,
|
|
13
|
-
* which is a fault in whoever acts on the map;
|
|
14
|
-
* - long waits further down with none at the top mean the zones are the wrong
|
|
15
|
-
* width — the urgent one too narrow, so the viewer reaches material that was
|
|
16
|
-
* only ever ranked "soon";
|
|
17
|
-
* - a rank with no waits at all is not a good sign or a bad one, it is silence,
|
|
18
|
-
* and it is reported as such rather than as a zero.
|
|
19
|
-
*
|
|
20
|
-
* There is nothing in here about encoders, torrents, sessions or viewers: it is
|
|
21
|
-
* given a rank and a number of milliseconds.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/** How many waits to keep per rank. A median wants a sample, not a history. */
|
|
25
|
-
const HISTORY = 200;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Ranks are collapsed into bands before they are counted.
|
|
29
|
-
*
|
|
30
|
-
* The map's ranks are as many as the film needs — 100 down to 1 on a long file
|
|
31
|
-
* — and a table with a hundred rows says nothing a reader can hold. What is
|
|
32
|
-
* being asked is coarse: was the thing waited for what the viewer needs NOW,
|
|
33
|
-
* what they reach shortly, or the rest of the film. So the top rank is its own
|
|
34
|
-
* band, the next few are the second, and everything below is the third.
|
|
35
|
-
*
|
|
36
|
-
* @param {number} rank
|
|
37
|
-
* @param {number} topRank - The highest rank the map currently states.
|
|
38
|
-
* @returns {"now" | "soon" | "later"}
|
|
39
|
-
*/
|
|
40
|
-
export function bandOf(rank, topRank) {
|
|
41
|
-
if (!Number.isFinite(rank) || rank <= 0) {
|
|
42
|
-
return "later";
|
|
43
|
-
}
|
|
44
|
-
const top = Number.isFinite(topRank) && topRank > 0 ? topRank : rank;
|
|
45
|
-
if (rank >= top) {
|
|
46
|
-
return "now";
|
|
47
|
-
}
|
|
48
|
-
// Within a tenth of the top: what a viewer reaches while watching what they
|
|
49
|
-
// hold. A tenth is the map's own shape — its zones widen geometrically — and
|
|
50
|
-
// not a threshold chosen for this table.
|
|
51
|
-
return rank >= top - Math.max(1, Math.round(top / 10)) ? "soon" : "later";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class WaitLedger {
|
|
55
|
-
/** @type {Map<string, Map<string, number[]>>} */
|
|
56
|
-
#waits = new Map();
|
|
57
|
-
|
|
58
|
-
/** @type {Map<string, Map<string, number>>} */
|
|
59
|
-
#counts = new Map();
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Somebody waited this long for something the map ranked this highly.
|
|
63
|
-
*
|
|
64
|
-
* @param {string} key - What the waits belong to: an output, or a file.
|
|
65
|
-
* @param {number} waitedMs
|
|
66
|
-
* @param {number} rank - The map's rank for the thing waited for.
|
|
67
|
-
* @param {number} topRank - The highest rank the map states, so the rank can
|
|
68
|
-
* be read as a position rather than as an absolute number.
|
|
69
|
-
*/
|
|
70
|
-
note(key, waitedMs, rank, topRank) {
|
|
71
|
-
if (!key || !Number.isFinite(waitedMs) || waitedMs < 0) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const band = bandOf(rank, topRank);
|
|
75
|
-
let byBand = this.#waits.get(key);
|
|
76
|
-
if (!byBand) {
|
|
77
|
-
byBand = new Map();
|
|
78
|
-
this.#waits.set(key, byBand);
|
|
79
|
-
}
|
|
80
|
-
const held = byBand.get(band) ?? [];
|
|
81
|
-
held.push(waitedMs);
|
|
82
|
-
while (held.length > HISTORY) {
|
|
83
|
-
held.shift();
|
|
84
|
-
}
|
|
85
|
-
byBand.set(band, held);
|
|
86
|
-
|
|
87
|
-
let counts = this.#counts.get(key);
|
|
88
|
-
if (!counts) {
|
|
89
|
-
counts = new Map();
|
|
90
|
-
this.#counts.set(key, counts);
|
|
91
|
-
}
|
|
92
|
-
counts.set(band, (counts.get(band) ?? 0) + 1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* What the waits say, in one line, or null while nothing has waited.
|
|
97
|
-
*
|
|
98
|
-
* The count is the whole run and the median and worst are the recent sample,
|
|
99
|
-
* because those answer different questions: how often, and how badly.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} key
|
|
102
|
-
* @returns {string | null}
|
|
103
|
-
*/
|
|
104
|
-
describe(key) {
|
|
105
|
-
const byBand = this.#waits.get(key);
|
|
106
|
-
if (!byBand || byBand.size === 0) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
const counts = this.#counts.get(key) ?? new Map();
|
|
110
|
-
const parts = [];
|
|
111
|
-
for (const band of ["now", "soon", "later"]) {
|
|
112
|
-
const held = byBand.get(band);
|
|
113
|
-
if (!held || held.length === 0) {
|
|
114
|
-
// Said out loud, because an absent band and a band that never waited
|
|
115
|
-
// are the same silence and neither is a zero.
|
|
116
|
-
parts.push(`${band} none`);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const sorted = [...held].sort((a, b) => a - b);
|
|
120
|
-
const median = sorted[Math.floor(sorted.length / 2)];
|
|
121
|
-
parts.push(
|
|
122
|
-
`${band} ${counts.get(band) ?? held.length} wait(s) median ${Math.round(median)}ms ` +
|
|
123
|
-
`worst ${Math.round(sorted[sorted.length - 1])}ms`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
return parts.join(", ");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @param {string} key
|
|
131
|
-
*/
|
|
132
|
-
forget(key) {
|
|
133
|
-
this.#waits.delete(key);
|
|
134
|
-
this.#counts.delete(key);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* One ledger for the process, because the question is asked in two layers and
|
|
140
|
-
* the answer is comparable only if the scale is the same.
|
|
141
|
-
*/
|
|
142
|
-
export const waits = new WaitLedger();
|
|
1
|
+
/**
|
|
2
|
+
* @file How well the priority map is being served, measured where somebody waits.
|
|
3
|
+
*
|
|
4
|
+
* The map says what matters most. Nothing said whether what mattered most was
|
|
5
|
+
* actually delivered first — so "is the prioritisation any good" had no answer
|
|
6
|
+
* of any kind, for either of the two things that read the map.
|
|
7
|
+
*
|
|
8
|
+
* The measure is the same for both, and it is a wait recorded against the rank
|
|
9
|
+
* the map gave the thing waited for AT THE MOMENT it was asked for. Read that
|
|
10
|
+
* way it says which part is wrong rather than whether the whole scheme is:
|
|
11
|
+
*
|
|
12
|
+
* - long waits at the TOP rank mean the urgent zone is not being served first,
|
|
13
|
+
* which is a fault in whoever acts on the map;
|
|
14
|
+
* - long waits further down with none at the top mean the zones are the wrong
|
|
15
|
+
* width — the urgent one too narrow, so the viewer reaches material that was
|
|
16
|
+
* only ever ranked "soon";
|
|
17
|
+
* - a rank with no waits at all is not a good sign or a bad one, it is silence,
|
|
18
|
+
* and it is reported as such rather than as a zero.
|
|
19
|
+
*
|
|
20
|
+
* There is nothing in here about encoders, torrents, sessions or viewers: it is
|
|
21
|
+
* given a rank and a number of milliseconds.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** How many waits to keep per rank. A median wants a sample, not a history. */
|
|
25
|
+
const HISTORY = 200;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ranks are collapsed into bands before they are counted.
|
|
29
|
+
*
|
|
30
|
+
* The map's ranks are as many as the film needs — 100 down to 1 on a long file
|
|
31
|
+
* — and a table with a hundred rows says nothing a reader can hold. What is
|
|
32
|
+
* being asked is coarse: was the thing waited for what the viewer needs NOW,
|
|
33
|
+
* what they reach shortly, or the rest of the film. So the top rank is its own
|
|
34
|
+
* band, the next few are the second, and everything below is the third.
|
|
35
|
+
*
|
|
36
|
+
* @param {number} rank
|
|
37
|
+
* @param {number} topRank - The highest rank the map currently states.
|
|
38
|
+
* @returns {"now" | "soon" | "later"}
|
|
39
|
+
*/
|
|
40
|
+
export function bandOf(rank, topRank) {
|
|
41
|
+
if (!Number.isFinite(rank) || rank <= 0) {
|
|
42
|
+
return "later";
|
|
43
|
+
}
|
|
44
|
+
const top = Number.isFinite(topRank) && topRank > 0 ? topRank : rank;
|
|
45
|
+
if (rank >= top) {
|
|
46
|
+
return "now";
|
|
47
|
+
}
|
|
48
|
+
// Within a tenth of the top: what a viewer reaches while watching what they
|
|
49
|
+
// hold. A tenth is the map's own shape — its zones widen geometrically — and
|
|
50
|
+
// not a threshold chosen for this table.
|
|
51
|
+
return rank >= top - Math.max(1, Math.round(top / 10)) ? "soon" : "later";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class WaitLedger {
|
|
55
|
+
/** @type {Map<string, Map<string, number[]>>} */
|
|
56
|
+
#waits = new Map();
|
|
57
|
+
|
|
58
|
+
/** @type {Map<string, Map<string, number>>} */
|
|
59
|
+
#counts = new Map();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Somebody waited this long for something the map ranked this highly.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} key - What the waits belong to: an output, or a file.
|
|
65
|
+
* @param {number} waitedMs
|
|
66
|
+
* @param {number} rank - The map's rank for the thing waited for.
|
|
67
|
+
* @param {number} topRank - The highest rank the map states, so the rank can
|
|
68
|
+
* be read as a position rather than as an absolute number.
|
|
69
|
+
*/
|
|
70
|
+
note(key, waitedMs, rank, topRank) {
|
|
71
|
+
if (!key || !Number.isFinite(waitedMs) || waitedMs < 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const band = bandOf(rank, topRank);
|
|
75
|
+
let byBand = this.#waits.get(key);
|
|
76
|
+
if (!byBand) {
|
|
77
|
+
byBand = new Map();
|
|
78
|
+
this.#waits.set(key, byBand);
|
|
79
|
+
}
|
|
80
|
+
const held = byBand.get(band) ?? [];
|
|
81
|
+
held.push(waitedMs);
|
|
82
|
+
while (held.length > HISTORY) {
|
|
83
|
+
held.shift();
|
|
84
|
+
}
|
|
85
|
+
byBand.set(band, held);
|
|
86
|
+
|
|
87
|
+
let counts = this.#counts.get(key);
|
|
88
|
+
if (!counts) {
|
|
89
|
+
counts = new Map();
|
|
90
|
+
this.#counts.set(key, counts);
|
|
91
|
+
}
|
|
92
|
+
counts.set(band, (counts.get(band) ?? 0) + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What the waits say, in one line, or null while nothing has waited.
|
|
97
|
+
*
|
|
98
|
+
* The count is the whole run and the median and worst are the recent sample,
|
|
99
|
+
* because those answer different questions: how often, and how badly.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} key
|
|
102
|
+
* @returns {string | null}
|
|
103
|
+
*/
|
|
104
|
+
describe(key) {
|
|
105
|
+
const byBand = this.#waits.get(key);
|
|
106
|
+
if (!byBand || byBand.size === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const counts = this.#counts.get(key) ?? new Map();
|
|
110
|
+
const parts = [];
|
|
111
|
+
for (const band of ["now", "soon", "later"]) {
|
|
112
|
+
const held = byBand.get(band);
|
|
113
|
+
if (!held || held.length === 0) {
|
|
114
|
+
// Said out loud, because an absent band and a band that never waited
|
|
115
|
+
// are the same silence and neither is a zero.
|
|
116
|
+
parts.push(`${band} none`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const sorted = [...held].sort((a, b) => a - b);
|
|
120
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
121
|
+
parts.push(
|
|
122
|
+
`${band} ${counts.get(band) ?? held.length} wait(s) median ${Math.round(median)}ms ` +
|
|
123
|
+
`worst ${Math.round(sorted[sorted.length - 1])}ms`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return parts.join(", ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} key
|
|
131
|
+
*/
|
|
132
|
+
forget(key) {
|
|
133
|
+
this.#waits.delete(key);
|
|
134
|
+
this.#counts.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One ledger for the process, because the question is asked in two layers and
|
|
140
|
+
* the answer is comparable only if the scale is the same.
|
|
141
|
+
*/
|
|
142
|
+
export const waits = new WaitLedger();
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Playback controller — interface layer over playback planning.
|
|
3
|
-
*
|
|
4
|
-
* Thin adapter between HTTP/routes and the playback plan. Parses nothing
|
|
5
|
-
* itself. Exists so routes depend on a controller contract, not on service
|
|
6
|
-
* internals.
|
|
7
|
-
*
|
|
8
|
-
* It used to hold a `ContainerOrchestrator` as well, and that was the main
|
|
9
|
-
* thread's own instance of a module singleton the torrent worker also loads —
|
|
10
|
-
* so it held no container, was asked nothing, and made it look as though a
|
|
11
|
-
* container could be consulted here. It cannot: the file's bytes are in the
|
|
12
|
-
* worker, and an object with methods does not cross a thread. What crosses is a
|
|
13
|
-
* message.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export class PlaybackController {
|
|
17
|
-
/**
|
|
18
|
-
* @param {object} deps
|
|
19
|
-
* @param {import("../../torrent/torrent-pool.js").TorrentPool} deps.torrentPool
|
|
20
|
-
* @param {ReturnType<import("../../../store/source-registry.js").createSourceRegistry>} deps.sourceRegistry
|
|
21
|
-
* @param {string} deps.ffmpegBin
|
|
22
|
-
* @param {string} deps.localBaseUrl
|
|
23
|
-
* @param {ReturnType<import("../../media/playback-planner.js").createPlaybackPlanner>} deps.playbackPlanner
|
|
24
|
-
*/
|
|
25
|
-
constructor({ torrentPool, sourceRegistry, ffmpegBin, localBaseUrl, playbackPlanner }) {
|
|
26
|
-
this.torrentPool = torrentPool;
|
|
27
|
-
this.sourceRegistry = sourceRegistry;
|
|
28
|
-
this.ffmpegBin = ffmpegBin;
|
|
29
|
-
this.localBaseUrl = localBaseUrl;
|
|
30
|
-
this.playbackPlanner = playbackPlanner;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async getPlan(params) {
|
|
34
|
-
return this.playbackPlanner.getPlan(params);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async refreshAudioTracks(params) {
|
|
38
|
-
return this.playbackPlanner.refreshAudioTracks(params);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Playback controller — interface layer over playback planning.
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter between HTTP/routes and the playback plan. Parses nothing
|
|
5
|
+
* itself. Exists so routes depend on a controller contract, not on service
|
|
6
|
+
* internals.
|
|
7
|
+
*
|
|
8
|
+
* It used to hold a `ContainerOrchestrator` as well, and that was the main
|
|
9
|
+
* thread's own instance of a module singleton the torrent worker also loads —
|
|
10
|
+
* so it held no container, was asked nothing, and made it look as though a
|
|
11
|
+
* container could be consulted here. It cannot: the file's bytes are in the
|
|
12
|
+
* worker, and an object with methods does not cross a thread. What crosses is a
|
|
13
|
+
* message.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class PlaybackController {
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {import("../../torrent/torrent-pool.js").TorrentPool} deps.torrentPool
|
|
20
|
+
* @param {ReturnType<import("../../../store/source-registry.js").createSourceRegistry>} deps.sourceRegistry
|
|
21
|
+
* @param {string} deps.ffmpegBin
|
|
22
|
+
* @param {string} deps.localBaseUrl
|
|
23
|
+
* @param {ReturnType<import("../../media/playback-planner.js").createPlaybackPlanner>} deps.playbackPlanner
|
|
24
|
+
*/
|
|
25
|
+
constructor({ torrentPool, sourceRegistry, ffmpegBin, localBaseUrl, playbackPlanner }) {
|
|
26
|
+
this.torrentPool = torrentPool;
|
|
27
|
+
this.sourceRegistry = sourceRegistry;
|
|
28
|
+
this.ffmpegBin = ffmpegBin;
|
|
29
|
+
this.localBaseUrl = localBaseUrl;
|
|
30
|
+
this.playbackPlanner = playbackPlanner;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getPlan(params) {
|
|
34
|
+
return this.playbackPlanner.getPlan(params);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async refreshAudioTracks(params) {
|
|
38
|
+
return this.playbackPlanner.refreshAudioTracks(params);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Subtitle controller — interface layer over SubtitleOrchestrator.
|
|
3
|
-
*
|
|
4
|
-
* Routes (HTTP or data-channel) call this, not the domain module directly.
|
|
5
|
-
* Handles external files vs embedded tracks branching, header setting, and
|
|
6
|
-
* cursor/covered-cluster bookkeeping. Domain work (cluster walk, conversion,
|
|
7
|
-
* language detection) stays in orchestrator/domain.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const EXTERNAL_MAX_BYTES = 8 * 1024 * 1024;
|
|
11
|
-
|
|
12
|
-
function readFileFully(file, maxBytes) {
|
|
13
|
-
return new Promise((resolve, reject) => {
|
|
14
|
-
const stream = file.createReadStream();
|
|
15
|
-
const chunks = [];
|
|
16
|
-
let total = 0;
|
|
17
|
-
stream.on("data", (chunk) => {
|
|
18
|
-
total += chunk.length;
|
|
19
|
-
if (total > maxBytes) { stream.destroy(); reject(new Error("subtitle file exceeds size cap")); return; }
|
|
20
|
-
chunks.push(chunk);
|
|
21
|
-
});
|
|
22
|
-
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
23
|
-
stream.on("error", reject);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class SubtitleController {
|
|
28
|
-
/**
|
|
29
|
-
* @param {object} deps
|
|
30
|
-
* @param {object} deps.sourceRegistry
|
|
31
|
-
* @param {object} deps.torrentPool
|
|
32
|
-
* @param {import("../../media/SubtitleOrchestrator.js").SubtitleOrchestrator} deps.subtitles -
|
|
33
|
-
* HANDED IN rather than built here. A controller translates a request for
|
|
34
|
-
* an orchestrator and reaches every layer below it THROUGH that one; putting
|
|
35
|
-
* the media layer and the torrent's cue reading together is composition, and
|
|
36
|
-
* composition belongs to the one place that already knows about both.
|
|
37
|
-
*/
|
|
38
|
-
constructor({ sourceRegistry, torrentPool, subtitles }) {
|
|
39
|
-
this.sourceRegistry = sourceRegistry;
|
|
40
|
-
this.torrentPool = torrentPool;
|
|
41
|
-
this.orchestrator = subtitles;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Serve external subtitle file or embedded track.
|
|
46
|
-
* Returns { vtt, language, headers } or { error, status }.
|
|
47
|
-
*/
|
|
48
|
-
async getSubtitle({ sourceKey, fileIndex, trackIndex, since, after }) {
|
|
49
|
-
const rec = this.sourceRegistry.get(sourceKey);
|
|
50
|
-
if (!rec) return { error: "Source key was not found.", status: 404 };
|
|
51
|
-
const torrent = await this.torrentPool.getTorrent(rec.sourceType, rec.source);
|
|
52
|
-
const file = torrent.files[fileIndex];
|
|
53
|
-
if (!file) return { error: "File index was not found in torrent.", status: 404 };
|
|
54
|
-
|
|
55
|
-
const hasTrack = trackIndex !== undefined && trackIndex !== "" && Number.isFinite(Number(trackIndex));
|
|
56
|
-
if (!hasTrack) {
|
|
57
|
-
const name = file.name ?? "";
|
|
58
|
-
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
59
|
-
try {
|
|
60
|
-
const bytes = await readFileFully(file, EXTERNAL_MAX_BYTES);
|
|
61
|
-
const converted = this.orchestrator.fileAsVtt(bytes, ext);
|
|
62
|
-
if (!converted) return { error: `Unsupported subtitle format: ${ext}`, status: 422 };
|
|
63
|
-
return { ...converted, headers: {} };
|
|
64
|
-
} catch (e) {
|
|
65
|
-
return { error: `Could not read subtitle file: ${e?.message ?? e}`, status: 502 };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const idx = Number(trackIndex);
|
|
70
|
-
if (!Number.isInteger(idx) || idx < 0) return { error: "trackIndex must be a non-negative integer.", status: 400 };
|
|
71
|
-
|
|
72
|
-
// Resolve via orchestrator (domain: cluster walk or MP4 sample ranges)
|
|
73
|
-
const tracks = await this.orchestrator.getTracks(torrent, fileIndex, sourceKey);
|
|
74
|
-
const track = Array.isArray(tracks) ? tracks.find((c) => c.declaredIndex === idx) ?? null : null;
|
|
75
|
-
// Also try domain's declaredIndex-agnostic lookup via getCues path — keep compat with existing subtitle-cues declaredIndex
|
|
76
|
-
let held = null;
|
|
77
|
-
try {
|
|
78
|
-
// Need trackNumber for domain call — find via declared workspace
|
|
79
|
-
const domainTracks = await this.orchestrator.getDeclaredTracks(torrent, fileIndex, sourceKey);
|
|
80
|
-
// If not found, fall back to direct cuesHeldFor via trackNumber from tracks list
|
|
81
|
-
const target = track ?? domainTracks.find((t) => t.declaredIndex === idx) ?? null;
|
|
82
|
-
const trackNumber = target?.trackNumber ?? track?.trackNumber;
|
|
83
|
-
if (trackNumber != null) {
|
|
84
|
-
held = await this.orchestrator.getCues(torrent, fileIndex, sourceKey, trackNumber);
|
|
85
|
-
}
|
|
86
|
-
} catch {}
|
|
87
|
-
if (held && Array.isArray(held.cues)) {
|
|
88
|
-
const cursor = held.cues.reduce((h, c) => Math.max(h, Number(c.seq) || 0), 0);
|
|
89
|
-
const fresh = Number.isInteger(since) ? held.cues.filter((c) => (Number(c.seq) || 0) > since)
|
|
90
|
-
: Number.isFinite(after) ? held.cues.filter((c) => c.startSeconds > after) : held.cues;
|
|
91
|
-
const codecId = held.track?.codecId ?? track?.codecId ?? "";
|
|
92
|
-
const { vtt, language } = this.orchestrator.cuesAsVtt(fresh, held.cues, codecId);
|
|
93
|
-
return {
|
|
94
|
-
vtt,
|
|
95
|
-
language,
|
|
96
|
-
headers: {
|
|
97
|
-
"X-Subtitle-Covered-Clusters": String(held.coveredClusters ?? 0),
|
|
98
|
-
"X-Subtitle-Indexed-Clusters": String(held.indexedClusters ?? 0),
|
|
99
|
-
"X-Subtitle-Cursor": String(cursor)
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return { pending: true, status: 202 };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async warm(torrent, fileIndex, sourceKey) {
|
|
107
|
-
return this.orchestrator.warm(torrent, fileIndex, sourceKey);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Subtitle controller — interface layer over SubtitleOrchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Routes (HTTP or data-channel) call this, not the domain module directly.
|
|
5
|
+
* Handles external files vs embedded tracks branching, header setting, and
|
|
6
|
+
* cursor/covered-cluster bookkeeping. Domain work (cluster walk, conversion,
|
|
7
|
+
* language detection) stays in orchestrator/domain.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const EXTERNAL_MAX_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
function readFileFully(file, maxBytes) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const stream = file.createReadStream();
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let total = 0;
|
|
17
|
+
stream.on("data", (chunk) => {
|
|
18
|
+
total += chunk.length;
|
|
19
|
+
if (total > maxBytes) { stream.destroy(); reject(new Error("subtitle file exceeds size cap")); return; }
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
});
|
|
22
|
+
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
23
|
+
stream.on("error", reject);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class SubtitleController {
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} deps
|
|
30
|
+
* @param {object} deps.sourceRegistry
|
|
31
|
+
* @param {object} deps.torrentPool
|
|
32
|
+
* @param {import("../../media/SubtitleOrchestrator.js").SubtitleOrchestrator} deps.subtitles -
|
|
33
|
+
* HANDED IN rather than built here. A controller translates a request for
|
|
34
|
+
* an orchestrator and reaches every layer below it THROUGH that one; putting
|
|
35
|
+
* the media layer and the torrent's cue reading together is composition, and
|
|
36
|
+
* composition belongs to the one place that already knows about both.
|
|
37
|
+
*/
|
|
38
|
+
constructor({ sourceRegistry, torrentPool, subtitles }) {
|
|
39
|
+
this.sourceRegistry = sourceRegistry;
|
|
40
|
+
this.torrentPool = torrentPool;
|
|
41
|
+
this.orchestrator = subtitles;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Serve external subtitle file or embedded track.
|
|
46
|
+
* Returns { vtt, language, headers } or { error, status }.
|
|
47
|
+
*/
|
|
48
|
+
async getSubtitle({ sourceKey, fileIndex, trackIndex, since, after }) {
|
|
49
|
+
const rec = this.sourceRegistry.get(sourceKey);
|
|
50
|
+
if (!rec) return { error: "Source key was not found.", status: 404 };
|
|
51
|
+
const torrent = await this.torrentPool.getTorrent(rec.sourceType, rec.source);
|
|
52
|
+
const file = torrent.files[fileIndex];
|
|
53
|
+
if (!file) return { error: "File index was not found in torrent.", status: 404 };
|
|
54
|
+
|
|
55
|
+
const hasTrack = trackIndex !== undefined && trackIndex !== "" && Number.isFinite(Number(trackIndex));
|
|
56
|
+
if (!hasTrack) {
|
|
57
|
+
const name = file.name ?? "";
|
|
58
|
+
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
59
|
+
try {
|
|
60
|
+
const bytes = await readFileFully(file, EXTERNAL_MAX_BYTES);
|
|
61
|
+
const converted = this.orchestrator.fileAsVtt(bytes, ext);
|
|
62
|
+
if (!converted) return { error: `Unsupported subtitle format: ${ext}`, status: 422 };
|
|
63
|
+
return { ...converted, headers: {} };
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { error: `Could not read subtitle file: ${e?.message ?? e}`, status: 502 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const idx = Number(trackIndex);
|
|
70
|
+
if (!Number.isInteger(idx) || idx < 0) return { error: "trackIndex must be a non-negative integer.", status: 400 };
|
|
71
|
+
|
|
72
|
+
// Resolve via orchestrator (domain: cluster walk or MP4 sample ranges)
|
|
73
|
+
const tracks = await this.orchestrator.getTracks(torrent, fileIndex, sourceKey);
|
|
74
|
+
const track = Array.isArray(tracks) ? tracks.find((c) => c.declaredIndex === idx) ?? null : null;
|
|
75
|
+
// Also try domain's declaredIndex-agnostic lookup via getCues path — keep compat with existing subtitle-cues declaredIndex
|
|
76
|
+
let held = null;
|
|
77
|
+
try {
|
|
78
|
+
// Need trackNumber for domain call — find via declared workspace
|
|
79
|
+
const domainTracks = await this.orchestrator.getDeclaredTracks(torrent, fileIndex, sourceKey);
|
|
80
|
+
// If not found, fall back to direct cuesHeldFor via trackNumber from tracks list
|
|
81
|
+
const target = track ?? domainTracks.find((t) => t.declaredIndex === idx) ?? null;
|
|
82
|
+
const trackNumber = target?.trackNumber ?? track?.trackNumber;
|
|
83
|
+
if (trackNumber != null) {
|
|
84
|
+
held = await this.orchestrator.getCues(torrent, fileIndex, sourceKey, trackNumber);
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
if (held && Array.isArray(held.cues)) {
|
|
88
|
+
const cursor = held.cues.reduce((h, c) => Math.max(h, Number(c.seq) || 0), 0);
|
|
89
|
+
const fresh = Number.isInteger(since) ? held.cues.filter((c) => (Number(c.seq) || 0) > since)
|
|
90
|
+
: Number.isFinite(after) ? held.cues.filter((c) => c.startSeconds > after) : held.cues;
|
|
91
|
+
const codecId = held.track?.codecId ?? track?.codecId ?? "";
|
|
92
|
+
const { vtt, language } = this.orchestrator.cuesAsVtt(fresh, held.cues, codecId);
|
|
93
|
+
return {
|
|
94
|
+
vtt,
|
|
95
|
+
language,
|
|
96
|
+
headers: {
|
|
97
|
+
"X-Subtitle-Covered-Clusters": String(held.coveredClusters ?? 0),
|
|
98
|
+
"X-Subtitle-Indexed-Clusters": String(held.indexedClusters ?? 0),
|
|
99
|
+
"X-Subtitle-Cursor": String(cursor)
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { pending: true, status: 202 };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async warm(torrent, fileIndex, sourceKey) {
|
|
107
|
+
return this.orchestrator.warm(torrent, fileIndex, sourceKey);
|
|
108
|
+
}
|
|
109
|
+
}
|