@torrent-tv/proxy 2.86.0 → 2.87.1
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 +2142 -1915
- package/CLAUDE.md +322 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/browser-proxy-contract.md +94 -0
- 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 +9 -6
- package/routes/transcode/session-file/get.js +52 -268
- package/routes/transcode/variant-file/get.js +46 -7
- package/routes/transcode/variant-warm/get.js +19 -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 +307 -29
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1229 -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 +755 -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 +935 -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/http-responses.js +60 -0
- package/services/server/transcode-session-files.js +325 -0
- package/services/server/wire-outputs.js +983 -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 +46 -9
- 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
|
@@ -23,15 +23,23 @@
|
|
|
23
23
|
* result when it is ready.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { CoverageMap } from "
|
|
27
|
-
import { firstUnmetWant, planEncoders } from "
|
|
28
|
-
import { endOfRun } from "
|
|
29
|
-
import { ENCODE_EXIT } from "
|
|
30
|
-
import { affordableRuns } from "
|
|
31
|
-
import { RunCosts } from "
|
|
32
|
-
import { contentionPenalty } from "
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
26
|
+
import { CoverageMap } from "./CoverageMap.js";
|
|
27
|
+
import { firstUnmetWant, planEncoders } from "./EncodePlan.js";
|
|
28
|
+
import { endOfRun } from "./EncodeRun.js";
|
|
29
|
+
import { ENCODE_EXIT } from "./encode-exit.js";
|
|
30
|
+
import { affordableRuns } from "./run-budget.js";
|
|
31
|
+
import { RunCosts } from "./run-costs.js";
|
|
32
|
+
import { contentionPenalty } from "./contention.js";
|
|
33
|
+
import { SegmentDemand } from "./SegmentDemand.js";
|
|
34
|
+
import { runStateOf } from "./encode-run-state.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} SegmentCoverage
|
|
38
|
+
* @property {(address: string) => number[]} provenNumbers
|
|
39
|
+
* @property {(address: string, index: number) => void} announce
|
|
40
|
+
* @property {(address: string) => number} filesHeld
|
|
41
|
+
* @property {(address: string, startedAt: number) => number} clearUpAfter
|
|
42
|
+
*/
|
|
35
43
|
|
|
36
44
|
/**
|
|
37
45
|
* How long nothing is placed on an output whose input has just gone, and the
|
|
@@ -46,14 +54,35 @@ import { SegmentDemand } from "../encode/SegmentDemand.js";
|
|
|
46
54
|
*/
|
|
47
55
|
const INPUT_QUIET_BASE_MS = 2_000;
|
|
48
56
|
const INPUT_QUIET_MAX_MS = 15_000;
|
|
57
|
+
const START_FAST_FAIL_MS = 2_000;
|
|
58
|
+
const MAX_FAILED_STARTS = 3;
|
|
49
59
|
|
|
50
60
|
export class EncodeOrchestrator {
|
|
61
|
+
/** Derived priority maps. Discardable and rebuilt from viewer state. */
|
|
62
|
+
#demand = new SegmentDemand();
|
|
63
|
+
/** @type {(address: string) => string} */
|
|
64
|
+
#describeWaits = () => "";
|
|
65
|
+
|
|
51
66
|
/** Output address to what has been made of it. @type {Map<string, CoverageMap>} */
|
|
52
67
|
#coverage = new Map();
|
|
53
68
|
|
|
69
|
+
/** @type {SegmentCoverage | null} */
|
|
70
|
+
#segmentCoverage = null;
|
|
71
|
+
|
|
54
72
|
/** Output address to the runs on it. @type {Map<string, import("../encode/EncodeRun.js").EncodeRun[]>} */
|
|
55
73
|
#runs = new Map();
|
|
56
74
|
|
|
75
|
+
/** Last state after an output has no registered run. @type {Map<string, string>} */
|
|
76
|
+
#lastState = new Map();
|
|
77
|
+
|
|
78
|
+
/** Last encoder error per output. @type {Map<string, string>} */
|
|
79
|
+
#lastError = new Map();
|
|
80
|
+
|
|
81
|
+
/** Last progress snapshot per output after its run ends. @type {Map<string, object>} */
|
|
82
|
+
#lastProgress = new Map();
|
|
83
|
+
|
|
84
|
+
/** Consecutive fast failures at one position, per output. @type {Map<string, { at: number, count: number }>} */
|
|
85
|
+
#failedStarts = new Map();
|
|
57
86
|
|
|
58
87
|
/** The fastest speed measured on one output, kept across restarts. @type {Map<string, number>} */
|
|
59
88
|
#lastSpeed = new Map();
|
|
@@ -93,12 +122,20 @@ export class EncodeOrchestrator {
|
|
|
93
122
|
this.#costs.noteStartup(measured);
|
|
94
123
|
}
|
|
95
124
|
|
|
125
|
+
/** Derived segment demand for one output, copied from the discardable cache. */
|
|
126
|
+
wantedSegmentsOn(address) {
|
|
127
|
+
return [...this.#demand.mapOn(address)];
|
|
128
|
+
}
|
|
129
|
+
|
|
96
130
|
/** The last reason a budget was cut, so the same one is not said twice. */
|
|
97
131
|
#lastBudgetReason = new Map();
|
|
98
132
|
|
|
99
133
|
/** The last unmet want said out loud, so a stuck one is said once. */
|
|
100
134
|
#lastUnmet = new Map();
|
|
101
135
|
|
|
136
|
+
/** The last time the machine bound an output's encoders, so it is said once. */
|
|
137
|
+
#lastMachineBound = new Map();
|
|
138
|
+
|
|
102
139
|
/**
|
|
103
140
|
* @param {object} params
|
|
104
141
|
* @param {(address: string) => number} params.maxRunsFor - How many encoders
|
|
@@ -117,6 +154,12 @@ export class EncodeOrchestrator {
|
|
|
117
154
|
* encoding speed.
|
|
118
155
|
* @param {{ info: (line: string) => void, warn: (line: string) => void }} params.logger
|
|
119
156
|
* @param {() => number} [params.now]
|
|
157
|
+
* @param {(address: string) => string} [params.describeWaits] - What the
|
|
158
|
+
* viewers actually waited for on this output, by band, for the state line.
|
|
159
|
+
* HANDED IN: what anybody waited for belongs to the priority layer, and
|
|
160
|
+
* this one may be asked about encoders with no ledger, no clock and no
|
|
161
|
+
* viewer anywhere. Absent, the line simply does not carry it.
|
|
162
|
+
* @param {SegmentCoverage | null} [params.segmentCoverage]
|
|
120
163
|
*/
|
|
121
164
|
constructor({
|
|
122
165
|
maxRunsFor,
|
|
@@ -125,17 +168,22 @@ export class EncodeOrchestrator {
|
|
|
125
168
|
contentionPenalties = null,
|
|
126
169
|
refetchSecPerFilmSecond = () => 0,
|
|
127
170
|
startingSpeedFor = () => 0,
|
|
128
|
-
|
|
171
|
+
segmentCoverage = null,
|
|
129
172
|
planSoon = null,
|
|
173
|
+
describeWaits = null,
|
|
174
|
+
admission = null,
|
|
130
175
|
logger,
|
|
131
176
|
now
|
|
132
177
|
}) {
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
this.
|
|
138
|
-
this
|
|
178
|
+
// HOW MANY ENCODERS THE WHOLE MACHINE HOLDS, across outputs. `maxRunsFor`
|
|
179
|
+
// answers for one output as though it were alone; this is what refuses the
|
|
180
|
+
// encoder that would slow everybody below realtime. Absent in a check that
|
|
181
|
+
// is about one output, and then only the per-output limit binds.
|
|
182
|
+
this.admission = admission && typeof admission.placesFor === "function" ? admission : null;
|
|
183
|
+
this.#describeWaits = typeof describeWaits === "function" ? describeWaits : () => "";
|
|
184
|
+
// Only the segment facts and operations this planner needs cross the
|
|
185
|
+
// storage boundary; the concrete storage remains in the server assembly.
|
|
186
|
+
this.#segmentCoverage = segmentCoverage;
|
|
139
187
|
this.maxRunsFor = maxRunsFor;
|
|
140
188
|
// Seconds of swarm time per second of film: what re-encoding material that
|
|
141
189
|
// already exists costs the download, over and above the encoder's own time.
|
|
@@ -200,8 +248,8 @@ export class EncodeOrchestrator {
|
|
|
200
248
|
*/
|
|
201
249
|
#upToDateCoverage(address) {
|
|
202
250
|
const coverage = this.coverageOf(address);
|
|
203
|
-
if (this
|
|
204
|
-
coverage.setReady(this.
|
|
251
|
+
if (this.#segmentCoverage) {
|
|
252
|
+
coverage.setReady(this.#segmentCoverage.provenNumbers(address));
|
|
205
253
|
}
|
|
206
254
|
return coverage;
|
|
207
255
|
}
|
|
@@ -249,6 +297,189 @@ export class EncodeOrchestrator {
|
|
|
249
297
|
return this.#runs.get(address) ?? [];
|
|
250
298
|
}
|
|
251
299
|
|
|
300
|
+
/**
|
|
301
|
+
* The aggregate state of this output's current runs, or its last ending when
|
|
302
|
+
* none remain.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} address
|
|
305
|
+
* @returns {string}
|
|
306
|
+
*/
|
|
307
|
+
stateOf(address) {
|
|
308
|
+
const runs = this.runsOn(address);
|
|
309
|
+
return runs.length > 0 ? runStateOf(runs) : (this.#lastState.get(address) ?? runStateOf([]));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** @param {string} address @returns {string} */
|
|
313
|
+
errorOf(address) {
|
|
314
|
+
return this.#lastError.get(address) ?? "";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Progress of the live run that covers a position, or the most recently
|
|
319
|
+
* updated run when no position is supplied.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} address
|
|
322
|
+
* @param {number | null} [index]
|
|
323
|
+
* @returns {object | null}
|
|
324
|
+
*/
|
|
325
|
+
progressOf(address, index = null) {
|
|
326
|
+
const live = this.runsOn(address).filter((run) => run.isAlive);
|
|
327
|
+
const covering = Number.isInteger(index)
|
|
328
|
+
? live.filter((run) => run.from <= index && endOfRun(run) >= index)
|
|
329
|
+
: live;
|
|
330
|
+
const candidates = covering.length > 0 ? covering : live;
|
|
331
|
+
const run = [...candidates].sort(
|
|
332
|
+
(left, right) => (right.progress?.updatedAt ?? 0) - (left.progress?.updatedAt ?? 0)
|
|
333
|
+
)[0];
|
|
334
|
+
return run?.progress?.snapshot?.() ?? this.#lastProgress.get(address) ?? null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Whether another start at this position is allowed.
|
|
339
|
+
*
|
|
340
|
+
* @param {string} address
|
|
341
|
+
* @param {number} from
|
|
342
|
+
* @returns {boolean}
|
|
343
|
+
*/
|
|
344
|
+
mayStartAt(address, from) {
|
|
345
|
+
const failed = this.#failedStarts.get(address);
|
|
346
|
+
return failed?.at !== from || failed.count < MAX_FAILED_STARTS;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Record whether a failed run died before it performed sustained work.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} address
|
|
353
|
+
* @param {number} from
|
|
354
|
+
* @param {number} livedMs
|
|
355
|
+
* @returns {{ at: number, count: number, blocked: boolean, limit: number, fastMs: number }}
|
|
356
|
+
*/
|
|
357
|
+
noteStartFailure(address, from, livedMs) {
|
|
358
|
+
if (!(livedMs < START_FAST_FAIL_MS)) {
|
|
359
|
+
this.#failedStarts.delete(address);
|
|
360
|
+
return { at: -1, count: 0, blocked: false, limit: MAX_FAILED_STARTS, fastMs: START_FAST_FAIL_MS };
|
|
361
|
+
}
|
|
362
|
+
const previous = this.#failedStarts.get(address);
|
|
363
|
+
const next = {
|
|
364
|
+
at: from,
|
|
365
|
+
count: previous?.at === from ? previous.count + 1 : 1
|
|
366
|
+
};
|
|
367
|
+
this.#failedStarts.set(address, next);
|
|
368
|
+
return {
|
|
369
|
+
...next,
|
|
370
|
+
blocked: next.count >= MAX_FAILED_STARTS,
|
|
371
|
+
limit: MAX_FAILED_STARTS,
|
|
372
|
+
fastMs: START_FAST_FAIL_MS
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* An output that no longer exists and has no run left on it.
|
|
378
|
+
*
|
|
379
|
+
* How its last run ended, what went wrong and which position kept failing are
|
|
380
|
+
* facts about THAT output. The name follows from the key, so the next output
|
|
381
|
+
* with the same parameters has the same address; kept, those facts would
|
|
382
|
+
* greet it as its own — a failed ending makes its first wait throw before any
|
|
383
|
+
* encoder is placed, and nothing would ever clear it.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} address
|
|
386
|
+
*/
|
|
387
|
+
forgetOutput(address) {
|
|
388
|
+
if (this.runsOn(address).length > 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.#lastState.delete(address);
|
|
392
|
+
this.#lastError.delete(address);
|
|
393
|
+
this.#lastProgress.delete(address);
|
|
394
|
+
this.#failedStarts.delete(address);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** @param {string} address @returns {{ at: number, count: number, blocked: boolean, limit: number }} */
|
|
398
|
+
startFailureOf(address) {
|
|
399
|
+
const failure = this.#failedStarts.get(address) ?? { at: -1, count: 0 };
|
|
400
|
+
return { ...failure, blocked: failure.count >= MAX_FAILED_STARTS, limit: MAX_FAILED_STARTS };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* What the map says about one segment of one output, and about the map it
|
|
405
|
+
* sits in.
|
|
406
|
+
*
|
|
407
|
+
* Asked by whoever is holding a request and measuring the wait: a wait means
|
|
408
|
+
* one thing at the top rank and another at the bottom, and telling them apart
|
|
409
|
+
* needs both the segment's own rank and the highest rank stated, since a rank
|
|
410
|
+
* is a position in this map and not an absolute number.
|
|
411
|
+
*
|
|
412
|
+
* AND AN EMPTY MAP IS NOT AN ANSWER. A top rank of zero says the map has not
|
|
413
|
+
* been built yet — a session created a moment ago, before the first pass —
|
|
414
|
+
* and that is a different thing from "nobody is coming". Conflated, every
|
|
415
|
+
* request behind the run reads as absent for as long as a fresh session has
|
|
416
|
+
* no map.
|
|
417
|
+
*
|
|
418
|
+
* It exists because the one caller reached through this object into the store
|
|
419
|
+
* behind it. A reach like that is how a store becomes the surface: the day it
|
|
420
|
+
* changes, a caller two layers away breaks for a reason it cannot see.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} address
|
|
423
|
+
* @param {number} index
|
|
424
|
+
* @returns {{ rank: number, topRank: number }}
|
|
425
|
+
*/
|
|
426
|
+
rankAt(address, index) {
|
|
427
|
+
return this.#demand.rankOf(address, index);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* How many encoder processes are running on this machine right now, over
|
|
432
|
+
* every output.
|
|
433
|
+
*
|
|
434
|
+
* ONE COUNT, and it is this class's because this class is what makes and
|
|
435
|
+
* unmakes runs: adopted the moment one exists and forgotten the moment one
|
|
436
|
+
* ends, both synchronously. The session manager answered the same question
|
|
437
|
+
* twice by walking its own registry — once per SESSION and once per RUN —
|
|
438
|
+
* which agreed only while a session held at most one run. It has held several
|
|
439
|
+
* since the plan was allowed to place them, and the per-session count is the
|
|
440
|
+
* wrong one of the two wherever they differ: what a second encoder costs was
|
|
441
|
+
* measured between ffmpeg PROCESSES, not between outputs.
|
|
442
|
+
*
|
|
443
|
+
* A SUSPENDED run is not counted. It is stopped where it stands and is using
|
|
444
|
+
* nothing; a cost measured beside it is a cost measured alone.
|
|
445
|
+
*
|
|
446
|
+
* @returns {number}
|
|
447
|
+
*/
|
|
448
|
+
/**
|
|
449
|
+
* How many encoders are alive on each output, suspended ones included.
|
|
450
|
+
*
|
|
451
|
+
* What the machine-wide admission counts as holding a place. A suspended
|
|
452
|
+
* encoder is counted here, unlike in {@link runningCount}: that one answers
|
|
453
|
+
* what is competing for the processor this instant, this one what the
|
|
454
|
+
* machine is committed to — and a parked encoder resumes the moment the
|
|
455
|
+
* viewer catches up.
|
|
456
|
+
*
|
|
457
|
+
* @returns {Map<string, number>}
|
|
458
|
+
*/
|
|
459
|
+
liveRunsByAddress() {
|
|
460
|
+
/** @type {Map<string, number>} */
|
|
461
|
+
const byAddress = new Map();
|
|
462
|
+
for (const [address, runs] of this.#runs) {
|
|
463
|
+
const alive = runs.filter((run) => run.isAlive).length;
|
|
464
|
+
if (alive > 0) {
|
|
465
|
+
byAddress.set(address, alive);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return byAddress;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
runningCount() {
|
|
472
|
+
let running = 0;
|
|
473
|
+
for (const runs of this.#runs.values()) {
|
|
474
|
+
for (const run of runs) {
|
|
475
|
+
if (run.isAlive && !run.isSuspended) {
|
|
476
|
+
running += 1;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return running;
|
|
481
|
+
}
|
|
482
|
+
|
|
252
483
|
/**
|
|
253
484
|
* How long an output is, once its playlist is known.
|
|
254
485
|
*
|
|
@@ -307,7 +538,7 @@ export class EncodeOrchestrator {
|
|
|
307
538
|
* @param {{ from: number, to: number, priority: number, withinSeconds: number }[]} zones
|
|
308
539
|
*/
|
|
309
540
|
notePriorityMap(address, zones) {
|
|
310
|
-
this
|
|
541
|
+
this.#demand.state(address, zones);
|
|
311
542
|
// NOBODY IS COMING HERE, so what was remembered about this output's input
|
|
312
543
|
// is about nobody. Kept, those three entries would stay for the life of the
|
|
313
544
|
// process, and the wait they describe would greet whoever opens this output
|
|
@@ -328,8 +559,9 @@ export class EncodeOrchestrator {
|
|
|
328
559
|
// Nothing is told to the store: by the time this is called the piece is
|
|
329
560
|
// ALREADY under its served name, because the rename is what closing it
|
|
330
561
|
// means. A statement kept beside the disk would be a second owner of one
|
|
331
|
-
// fact, which
|
|
562
|
+
// fact, which removed the stale second owner from the coverage map.
|
|
332
563
|
this.coverageOf(address).markReady(index);
|
|
564
|
+
this.#segmentCoverage?.announce?.(address, index);
|
|
333
565
|
for (const run of this.runsOn(address)) {
|
|
334
566
|
run.noteProduced(index);
|
|
335
567
|
}
|
|
@@ -364,7 +596,7 @@ export class EncodeOrchestrator {
|
|
|
364
596
|
* state, so a pass that finds nothing to change does nothing.
|
|
365
597
|
*/
|
|
366
598
|
reconcile() {
|
|
367
|
-
const addresses = new Set([...this
|
|
599
|
+
const addresses = new Set([...this.#demand.addresses(), ...this.#runs.keys()]);
|
|
368
600
|
for (const address of addresses) {
|
|
369
601
|
this.#reconcileOne(address);
|
|
370
602
|
}
|
|
@@ -433,12 +665,12 @@ export class EncodeOrchestrator {
|
|
|
433
665
|
// used to reach into the layer that STATES them for the same arithmetic,
|
|
434
666
|
// which is the coupling the layer rule forbids; the arithmetic itself now
|
|
435
667
|
// lives where it belongs to nobody.
|
|
436
|
-
const windows = this
|
|
668
|
+
const windows = this.#demand.mapOn(address);
|
|
437
669
|
const live = this.runsOn(address).filter((run) => run.isAlive);
|
|
438
670
|
// Asked ONCE. It is arithmetic over measurements, but it also says out loud
|
|
439
671
|
// when the reason it cuts the budget changes, so asking it three times in
|
|
440
672
|
// one pass is three chances to say a thing that happened once.
|
|
441
|
-
const maxRuns = this.#affordableOn(address, live);
|
|
673
|
+
const maxRuns = this.#withinMachine(address, live, this.#affordableOn(address, live));
|
|
442
674
|
// THE TERMS EVERY ARRIVAL IS COMPUTED FROM, named here so the line below can
|
|
443
675
|
// print them. A decision of this plan is `delay + (index - at) / rate +
|
|
444
676
|
// madeBetween * refetch` against a deadline, so without the rate and the two
|
|
@@ -583,7 +815,7 @@ export class EncodeOrchestrator {
|
|
|
583
815
|
this.#lastUnmet.delete(address);
|
|
584
816
|
} else if (this.#lastUnmet.get(address) !== wanting) {
|
|
585
817
|
this.#lastUnmet.set(address, wanting);
|
|
586
|
-
const held = this
|
|
818
|
+
const held = this.#segmentCoverage ? this.#segmentCoverage.filesHeld(address) : -1;
|
|
587
819
|
this.logger.warn(
|
|
588
820
|
`encode: #${wanting} of ${address} is wanted and NO ENCODER IS MAKING IT — ` +
|
|
589
821
|
`ready=${coverage.stats().ready} of ${coverage.segmentCount} ` +
|
|
@@ -691,6 +923,42 @@ export class EncodeOrchestrator {
|
|
|
691
923
|
return budget.runs;
|
|
692
924
|
}
|
|
693
925
|
|
|
926
|
+
/**
|
|
927
|
+
* The per-output limit, bounded by what the whole machine can still hold.
|
|
928
|
+
*
|
|
929
|
+
* The per-output answer is at least one whatever else is running; the
|
|
930
|
+
* machine's may be none, and that is the refusal: the output gets no encoder
|
|
931
|
+
* until one elsewhere ends. It is never below what already runs here, so a
|
|
932
|
+
* refusal stops a start and never takes an encoder away.
|
|
933
|
+
*
|
|
934
|
+
* Said out loud when the machine is what bound it, once per change.
|
|
935
|
+
*
|
|
936
|
+
* @param {string} address
|
|
937
|
+
* @param {object[]} live
|
|
938
|
+
* @param {number} perOutput
|
|
939
|
+
* @returns {number}
|
|
940
|
+
*/
|
|
941
|
+
#withinMachine(address, live, perOutput) {
|
|
942
|
+
if (!this.admission) {
|
|
943
|
+
return perOutput;
|
|
944
|
+
}
|
|
945
|
+
const machine = this.admission.placesFor(address, live.length);
|
|
946
|
+
const runs = Math.min(perOutput, machine.runs);
|
|
947
|
+
const said = runs < perOutput ? `${runs}:${machine.because}` : "";
|
|
948
|
+
if (said !== (this.#lastMachineBound.get(address) ?? "")) {
|
|
949
|
+
if (said) {
|
|
950
|
+
this.logger.info(
|
|
951
|
+
`encode: ${runs} encoder(s) on ${address} — the whole machine holds no more ` +
|
|
952
|
+
`(${machine.because}; this output alone would be allowed ${perOutput})`
|
|
953
|
+
);
|
|
954
|
+
this.#lastMachineBound.set(address, said);
|
|
955
|
+
} else {
|
|
956
|
+
this.#lastMachineBound.delete(address);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return runs;
|
|
960
|
+
}
|
|
961
|
+
|
|
694
962
|
/**
|
|
695
963
|
* Take charge of a run this class did not start.
|
|
696
964
|
*
|
|
@@ -734,6 +1002,16 @@ export class EncodeOrchestrator {
|
|
|
734
1002
|
* @param {import("../encode/EncodeRun.js").RunEnded} ended
|
|
735
1003
|
*/
|
|
736
1004
|
noteEnded(ended) {
|
|
1005
|
+
this.#lastState.set(ended.address, ended.run?.state ?? "IDLE");
|
|
1006
|
+
if (ended.lastError) {
|
|
1007
|
+
this.#lastError.set(ended.address, ended.lastError);
|
|
1008
|
+
} else if (ended.because && ended.ending !== ENCODE_EXIT.COMPLETE) {
|
|
1009
|
+
this.#lastError.set(ended.address, ended.because);
|
|
1010
|
+
}
|
|
1011
|
+
const progress = ended.run?.progress?.snapshot?.();
|
|
1012
|
+
if (progress) {
|
|
1013
|
+
this.#lastProgress.set(ended.address, progress);
|
|
1014
|
+
}
|
|
737
1015
|
this.#costs.note(ended);
|
|
738
1016
|
// Exactly one ending is normal — the run reached the end of the stretch it
|
|
739
1017
|
// was given and closed its last file. Every other leaves a piece open, and
|
|
@@ -741,7 +1019,7 @@ export class EncodeOrchestrator {
|
|
|
741
1019
|
// is a name match: no stretch to search and no bytes to judge. Done for
|
|
742
1020
|
// every ending, the normal one included, since a run that finished cleanly
|
|
743
1021
|
// has nothing under a working name and the sweep then removes nothing.
|
|
744
|
-
this
|
|
1022
|
+
this.#segmentCoverage?.clearUpAfter(ended.address, ended.from);
|
|
745
1023
|
this.coverageOf(ended.address).release(ended.run);
|
|
746
1024
|
const remaining = this.runsOn(ended.address).filter((run) => run !== ended.run);
|
|
747
1025
|
if (remaining.length === 0) {
|
|
@@ -889,9 +1167,9 @@ export class EncodeOrchestrator {
|
|
|
889
1167
|
*/
|
|
890
1168
|
describe() {
|
|
891
1169
|
const parts = [];
|
|
892
|
-
for (const address of new Set([...this
|
|
1170
|
+
for (const address of new Set([...this.#demand.addresses(), ...this.#runs.keys()])) {
|
|
893
1171
|
const coverage = this.coverageOf(address);
|
|
894
|
-
const stated = this
|
|
1172
|
+
const stated = this.#demand.mapOn(address);
|
|
895
1173
|
const windows = stated.map((zone) => ({ from: zone.from, to: zone.to }));
|
|
896
1174
|
const waiting = firstUnmetWant(coverage, windows);
|
|
897
1175
|
const runs = this.runsOn(address)
|
|
@@ -914,14 +1192,14 @@ export class EncodeOrchestrator {
|
|
|
914
1192
|
// different statements: files with nothing proving them closed reads as a
|
|
915
1193
|
// reporting fault, no files at all reads as an output yet to be made, and
|
|
916
1194
|
// the difference decides where to look.
|
|
917
|
-
const held = this
|
|
1195
|
+
const held = this.#segmentCoverage ? this.#segmentCoverage.filesHeld(address) : -1;
|
|
918
1196
|
// AND WHETHER THE MAP IS BEING SERVED IN ITS OWN ORDER. The zones say
|
|
919
1197
|
// what matters most; this says what the viewer actually waited for, by
|
|
920
1198
|
// band. Waits at `now` are the ones that cost a spinner, and until this
|
|
921
1199
|
// existed there was no figure anywhere saying whether the urgent zone was
|
|
922
1200
|
// served first — the map could have been read backwards and every line
|
|
923
1201
|
// above would have looked the same.
|
|
924
|
-
const served =
|
|
1202
|
+
const served = this.#describeWaits(address);
|
|
925
1203
|
parts.push(
|
|
926
1204
|
`${address} ready=${coverage.stats().ready}` +
|
|
927
1205
|
`${held < 0 ? "" : ` of ${held} file(s) on disk`} ` +
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
import { ENCODE_RUN_EVENT, ENCODE_RUN_STATE, INITIAL_RUN_STATE, nextState } from "./encode-run-state.js";
|
|
41
41
|
import { classifyEncodeExit, ENCODE_EXIT } from "./encode-exit.js";
|
|
42
|
+
import { RunProgress } from "./RunProgress.js";
|
|
42
43
|
|
|
43
44
|
/** Microseconds in a second, as ffmpeg's `out_time_ms` counts them. */
|
|
44
45
|
const MICROSECONDS_PER_SECOND = 1_000_000;
|
|
@@ -186,12 +187,8 @@ export class EncodeRun {
|
|
|
186
187
|
* encoder itself names it on its own channel, and answers with the name that
|
|
187
188
|
* piece is served under — because making it servable is a rename, and only
|
|
188
189
|
* whoever owns the disk can perform one.
|
|
189
|
-
* @param {(progress:
|
|
190
|
-
*
|
|
191
|
-
* RUN on both branches — neither `-output_ts_offset` nor `-copyts` changes
|
|
192
|
-
* what `-progress` reports, both measured — so rebasing them onto the
|
|
193
|
-
* source's timeline is the caller's, which is the only side that knows
|
|
194
|
-
* where this run began.
|
|
190
|
+
* @param {(progress: object) => void} [params.onProgress] - Called with this
|
|
191
|
+
* run's progress on the source timeline after every `-progress` report.
|
|
195
192
|
* @param {() => number | null} [params.lastSegmentIndex] - The film's last
|
|
196
193
|
* segment number, for telling "reached the end" from "the input dried up".
|
|
197
194
|
* ffmpeg exits zero for both, and over a torrent it cannot tell them apart.
|
|
@@ -208,6 +205,8 @@ export class EncodeRun {
|
|
|
208
205
|
* a closed piece's name carries. How a piece is named belongs to the format
|
|
209
206
|
* that writes it, so it arrives as a plain function rather than this class
|
|
210
207
|
* knowing any naming.
|
|
208
|
+
* @param {number} [params.startSeconds] - Where this run begins on the source timeline.
|
|
209
|
+
* @param {number | null} [params.totalSeconds] - Source duration when known.
|
|
211
210
|
*/
|
|
212
211
|
constructor({
|
|
213
212
|
address,
|
|
@@ -226,6 +225,8 @@ export class EncodeRun {
|
|
|
226
225
|
argsDescribed = "",
|
|
227
226
|
usesExplicitCuts = false,
|
|
228
227
|
indexOfName,
|
|
228
|
+
startSeconds = 0,
|
|
229
|
+
totalSeconds = null,
|
|
229
230
|
because = "no reason was given"
|
|
230
231
|
}) {
|
|
231
232
|
this.address = address;
|
|
@@ -236,6 +237,7 @@ export class EncodeRun {
|
|
|
236
237
|
this.spawnProcess = spawn;
|
|
237
238
|
this.logger = logger;
|
|
238
239
|
this.now = typeof now === "function" ? now : Date.now;
|
|
240
|
+
this.progress = new RunProgress({ startSeconds, totalSeconds, now: this.now });
|
|
239
241
|
this.onEnded = typeof onEnded === "function" ? onEnded : () => {};
|
|
240
242
|
this.onProgress = typeof onProgress === "function" ? onProgress : () => {};
|
|
241
243
|
// Told the NAME of every piece the encoder has closed. The name is the
|
|
@@ -449,17 +451,22 @@ export class EncodeRun {
|
|
|
449
451
|
if (key === "out_time_ms") {
|
|
450
452
|
const numeric = Number(value);
|
|
451
453
|
if (Number.isFinite(numeric) && numeric >= 0) {
|
|
452
|
-
this
|
|
454
|
+
this.#reportProgress({ processedSeconds: numeric / MICROSECONDS_PER_SECOND, speed: null });
|
|
453
455
|
}
|
|
454
456
|
} else if (key === "out_time") {
|
|
455
|
-
this
|
|
457
|
+
this.#reportProgress({ processedSeconds: null, speed: null, outTime: value });
|
|
456
458
|
} else if (key === "speed") {
|
|
457
459
|
this.noteSpeed(Number.parseFloat(value));
|
|
458
|
-
this
|
|
460
|
+
this.#reportProgress({ processedSeconds: null, speed: value });
|
|
459
461
|
}
|
|
460
462
|
}
|
|
461
463
|
}
|
|
462
464
|
|
|
465
|
+
#reportProgress(report) {
|
|
466
|
+
this.progress.note(report);
|
|
467
|
+
this.onProgress(this.progress.snapshot());
|
|
468
|
+
}
|
|
469
|
+
|
|
463
470
|
/**
|
|
464
471
|
* A segment this run has finished.
|
|
465
472
|
*
|