@torrent-tv/proxy 2.85.1 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2138 -1909
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +745 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +931 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/wire-outputs.js +958 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -489
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +46 -0
- 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 -44
- 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 -518
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +40 -8
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -172
- 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 -9689
- 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/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,15 @@
|
|
|
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
35
|
|
|
36
36
|
/**
|
|
37
37
|
* How long nothing is placed on an output whose input has just gone, and the
|
|
@@ -46,14 +46,32 @@ import { SegmentDemand } from "../encode/SegmentDemand.js";
|
|
|
46
46
|
*/
|
|
47
47
|
const INPUT_QUIET_BASE_MS = 2_000;
|
|
48
48
|
const INPUT_QUIET_MAX_MS = 15_000;
|
|
49
|
+
const START_FAST_FAIL_MS = 2_000;
|
|
50
|
+
const MAX_FAILED_STARTS = 3;
|
|
49
51
|
|
|
50
52
|
export class EncodeOrchestrator {
|
|
53
|
+
/** Derived priority maps. Discardable and rebuilt from viewer state. */
|
|
54
|
+
#demand = new SegmentDemand();
|
|
55
|
+
/** @type {(address: string) => string} */
|
|
56
|
+
#describeWaits = () => "";
|
|
57
|
+
|
|
51
58
|
/** Output address to what has been made of it. @type {Map<string, CoverageMap>} */
|
|
52
59
|
#coverage = new Map();
|
|
53
60
|
|
|
54
61
|
/** Output address to the runs on it. @type {Map<string, import("../encode/EncodeRun.js").EncodeRun[]>} */
|
|
55
62
|
#runs = new Map();
|
|
56
63
|
|
|
64
|
+
/** Last state after an output has no registered run. @type {Map<string, string>} */
|
|
65
|
+
#lastState = new Map();
|
|
66
|
+
|
|
67
|
+
/** Last encoder error per output. @type {Map<string, string>} */
|
|
68
|
+
#lastError = new Map();
|
|
69
|
+
|
|
70
|
+
/** Last progress snapshot per output after its run ends. @type {Map<string, object>} */
|
|
71
|
+
#lastProgress = new Map();
|
|
72
|
+
|
|
73
|
+
/** Consecutive fast failures at one position, per output. @type {Map<string, { at: number, count: number }>} */
|
|
74
|
+
#failedStarts = new Map();
|
|
57
75
|
|
|
58
76
|
/** The fastest speed measured on one output, kept across restarts. @type {Map<string, number>} */
|
|
59
77
|
#lastSpeed = new Map();
|
|
@@ -93,12 +111,20 @@ export class EncodeOrchestrator {
|
|
|
93
111
|
this.#costs.noteStartup(measured);
|
|
94
112
|
}
|
|
95
113
|
|
|
114
|
+
/** Derived segment demand for one output, copied from the discardable cache. */
|
|
115
|
+
wantedSegmentsOn(address) {
|
|
116
|
+
return [...this.#demand.mapOn(address)];
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
/** The last reason a budget was cut, so the same one is not said twice. */
|
|
97
120
|
#lastBudgetReason = new Map();
|
|
98
121
|
|
|
99
122
|
/** The last unmet want said out loud, so a stuck one is said once. */
|
|
100
123
|
#lastUnmet = new Map();
|
|
101
124
|
|
|
125
|
+
/** The last time the machine bound an output's encoders, so it is said once. */
|
|
126
|
+
#lastMachineBound = new Map();
|
|
127
|
+
|
|
102
128
|
/**
|
|
103
129
|
* @param {object} params
|
|
104
130
|
* @param {(address: string) => number} params.maxRunsFor - How many encoders
|
|
@@ -117,6 +143,11 @@ export class EncodeOrchestrator {
|
|
|
117
143
|
* encoding speed.
|
|
118
144
|
* @param {{ info: (line: string) => void, warn: (line: string) => void }} params.logger
|
|
119
145
|
* @param {() => number} [params.now]
|
|
146
|
+
* @param {(address: string) => string} [params.describeWaits] - What the
|
|
147
|
+
* viewers actually waited for on this output, by band, for the state line.
|
|
148
|
+
* HANDED IN: what anybody waited for belongs to the priority layer, and
|
|
149
|
+
* this one may be asked about encoders with no ledger, no clock and no
|
|
150
|
+
* viewer anywhere. Absent, the line simply does not carry it.
|
|
120
151
|
*/
|
|
121
152
|
constructor({
|
|
122
153
|
maxRunsFor,
|
|
@@ -127,15 +158,22 @@ export class EncodeOrchestrator {
|
|
|
127
158
|
startingSpeedFor = () => 0,
|
|
128
159
|
segmentStore = null,
|
|
129
160
|
planSoon = null,
|
|
161
|
+
describeWaits = null,
|
|
162
|
+
admission = null,
|
|
130
163
|
logger,
|
|
131
164
|
now
|
|
132
165
|
}) {
|
|
166
|
+
// HOW MANY ENCODERS THE WHOLE MACHINE HOLDS, across outputs. `maxRunsFor`
|
|
167
|
+
// answers for one output as though it were alone; this is what refuses the
|
|
168
|
+
// encoder that would slow everybody below realtime. Absent in a check that
|
|
169
|
+
// is about one output, and then only the per-output limit binds.
|
|
170
|
+
this.admission = admission && typeof admission.placesFor === "function" ? admission : null;
|
|
171
|
+
this.#describeWaits = typeof describeWaits === "function" ? describeWaits : () => "";
|
|
133
172
|
// The store of produced segments — the layer below this one. It is asked to
|
|
134
173
|
// clean up after a run that ended other than by reaching the end of its
|
|
135
174
|
// stretch, which is the one thing an ending must not leave behind: a file
|
|
136
175
|
// under a name that promises a whole segment.
|
|
137
176
|
this.segmentStore = segmentStore;
|
|
138
|
-
this.demand = new SegmentDemand();
|
|
139
177
|
this.maxRunsFor = maxRunsFor;
|
|
140
178
|
// Seconds of swarm time per second of film: what re-encoding material that
|
|
141
179
|
// already exists costs the download, over and above the encoder's own time.
|
|
@@ -249,6 +287,189 @@ export class EncodeOrchestrator {
|
|
|
249
287
|
return this.#runs.get(address) ?? [];
|
|
250
288
|
}
|
|
251
289
|
|
|
290
|
+
/**
|
|
291
|
+
* The aggregate state of this output's current runs, or its last ending when
|
|
292
|
+
* none remain.
|
|
293
|
+
*
|
|
294
|
+
* @param {string} address
|
|
295
|
+
* @returns {string}
|
|
296
|
+
*/
|
|
297
|
+
stateOf(address) {
|
|
298
|
+
const runs = this.runsOn(address);
|
|
299
|
+
return runs.length > 0 ? runStateOf(runs) : (this.#lastState.get(address) ?? runStateOf([]));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @param {string} address @returns {string} */
|
|
303
|
+
errorOf(address) {
|
|
304
|
+
return this.#lastError.get(address) ?? "";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Progress of the live run that covers a position, or the most recently
|
|
309
|
+
* updated run when no position is supplied.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} address
|
|
312
|
+
* @param {number | null} [index]
|
|
313
|
+
* @returns {object | null}
|
|
314
|
+
*/
|
|
315
|
+
progressOf(address, index = null) {
|
|
316
|
+
const live = this.runsOn(address).filter((run) => run.isAlive);
|
|
317
|
+
const covering = Number.isInteger(index)
|
|
318
|
+
? live.filter((run) => run.from <= index && endOfRun(run) >= index)
|
|
319
|
+
: live;
|
|
320
|
+
const candidates = covering.length > 0 ? covering : live;
|
|
321
|
+
const run = [...candidates].sort(
|
|
322
|
+
(left, right) => (right.progress?.updatedAt ?? 0) - (left.progress?.updatedAt ?? 0)
|
|
323
|
+
)[0];
|
|
324
|
+
return run?.progress?.snapshot?.() ?? this.#lastProgress.get(address) ?? null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Whether another start at this position is allowed.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} address
|
|
331
|
+
* @param {number} from
|
|
332
|
+
* @returns {boolean}
|
|
333
|
+
*/
|
|
334
|
+
mayStartAt(address, from) {
|
|
335
|
+
const failed = this.#failedStarts.get(address);
|
|
336
|
+
return failed?.at !== from || failed.count < MAX_FAILED_STARTS;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Record whether a failed run died before it performed sustained work.
|
|
341
|
+
*
|
|
342
|
+
* @param {string} address
|
|
343
|
+
* @param {number} from
|
|
344
|
+
* @param {number} livedMs
|
|
345
|
+
* @returns {{ at: number, count: number, blocked: boolean, limit: number, fastMs: number }}
|
|
346
|
+
*/
|
|
347
|
+
noteStartFailure(address, from, livedMs) {
|
|
348
|
+
if (!(livedMs < START_FAST_FAIL_MS)) {
|
|
349
|
+
this.#failedStarts.delete(address);
|
|
350
|
+
return { at: -1, count: 0, blocked: false, limit: MAX_FAILED_STARTS, fastMs: START_FAST_FAIL_MS };
|
|
351
|
+
}
|
|
352
|
+
const previous = this.#failedStarts.get(address);
|
|
353
|
+
const next = {
|
|
354
|
+
at: from,
|
|
355
|
+
count: previous?.at === from ? previous.count + 1 : 1
|
|
356
|
+
};
|
|
357
|
+
this.#failedStarts.set(address, next);
|
|
358
|
+
return {
|
|
359
|
+
...next,
|
|
360
|
+
blocked: next.count >= MAX_FAILED_STARTS,
|
|
361
|
+
limit: MAX_FAILED_STARTS,
|
|
362
|
+
fastMs: START_FAST_FAIL_MS
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* An output that no longer exists and has no run left on it.
|
|
368
|
+
*
|
|
369
|
+
* How its last run ended, what went wrong and which position kept failing are
|
|
370
|
+
* facts about THAT output. The name follows from the key, so the next output
|
|
371
|
+
* with the same parameters has the same address; kept, those facts would
|
|
372
|
+
* greet it as its own — a failed ending makes its first wait throw before any
|
|
373
|
+
* encoder is placed, and nothing would ever clear it.
|
|
374
|
+
*
|
|
375
|
+
* @param {string} address
|
|
376
|
+
*/
|
|
377
|
+
forgetOutput(address) {
|
|
378
|
+
if (this.runsOn(address).length > 0) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
this.#lastState.delete(address);
|
|
382
|
+
this.#lastError.delete(address);
|
|
383
|
+
this.#lastProgress.delete(address);
|
|
384
|
+
this.#failedStarts.delete(address);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** @param {string} address @returns {{ at: number, count: number, blocked: boolean, limit: number }} */
|
|
388
|
+
startFailureOf(address) {
|
|
389
|
+
const failure = this.#failedStarts.get(address) ?? { at: -1, count: 0 };
|
|
390
|
+
return { ...failure, blocked: failure.count >= MAX_FAILED_STARTS, limit: MAX_FAILED_STARTS };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* What the map says about one segment of one output, and about the map it
|
|
395
|
+
* sits in.
|
|
396
|
+
*
|
|
397
|
+
* Asked by whoever is holding a request and measuring the wait: a wait means
|
|
398
|
+
* one thing at the top rank and another at the bottom, and telling them apart
|
|
399
|
+
* needs both the segment's own rank and the highest rank stated, since a rank
|
|
400
|
+
* is a position in this map and not an absolute number.
|
|
401
|
+
*
|
|
402
|
+
* AND AN EMPTY MAP IS NOT AN ANSWER. A top rank of zero says the map has not
|
|
403
|
+
* been built yet — a session created a moment ago, before the first pass —
|
|
404
|
+
* and that is a different thing from "nobody is coming". Conflated, every
|
|
405
|
+
* request behind the run reads as absent for as long as a fresh session has
|
|
406
|
+
* no map.
|
|
407
|
+
*
|
|
408
|
+
* It exists because the one caller reached through this object into the store
|
|
409
|
+
* behind it. A reach like that is how a store becomes the surface: the day it
|
|
410
|
+
* changes, a caller two layers away breaks for a reason it cannot see.
|
|
411
|
+
*
|
|
412
|
+
* @param {string} address
|
|
413
|
+
* @param {number} index
|
|
414
|
+
* @returns {{ rank: number, topRank: number }}
|
|
415
|
+
*/
|
|
416
|
+
rankAt(address, index) {
|
|
417
|
+
return this.#demand.rankOf(address, index);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* How many encoder processes are running on this machine right now, over
|
|
422
|
+
* every output.
|
|
423
|
+
*
|
|
424
|
+
* ONE COUNT, and it is this class's because this class is what makes and
|
|
425
|
+
* unmakes runs: adopted the moment one exists and forgotten the moment one
|
|
426
|
+
* ends, both synchronously. The session manager answered the same question
|
|
427
|
+
* twice by walking its own registry — once per SESSION and once per RUN —
|
|
428
|
+
* which agreed only while a session held at most one run. It has held several
|
|
429
|
+
* since the plan was allowed to place them, and the per-session count is the
|
|
430
|
+
* wrong one of the two wherever they differ: what a second encoder costs was
|
|
431
|
+
* measured between ffmpeg PROCESSES, not between outputs.
|
|
432
|
+
*
|
|
433
|
+
* A SUSPENDED run is not counted. It is stopped where it stands and is using
|
|
434
|
+
* nothing; a cost measured beside it is a cost measured alone.
|
|
435
|
+
*
|
|
436
|
+
* @returns {number}
|
|
437
|
+
*/
|
|
438
|
+
/**
|
|
439
|
+
* How many encoders are alive on each output, suspended ones included.
|
|
440
|
+
*
|
|
441
|
+
* What the machine-wide admission counts as holding a place. A suspended
|
|
442
|
+
* encoder is counted here, unlike in {@link runningCount}: that one answers
|
|
443
|
+
* what is competing for the processor this instant, this one what the
|
|
444
|
+
* machine is committed to — and a parked encoder resumes the moment the
|
|
445
|
+
* viewer catches up.
|
|
446
|
+
*
|
|
447
|
+
* @returns {Map<string, number>}
|
|
448
|
+
*/
|
|
449
|
+
liveRunsByAddress() {
|
|
450
|
+
/** @type {Map<string, number>} */
|
|
451
|
+
const byAddress = new Map();
|
|
452
|
+
for (const [address, runs] of this.#runs) {
|
|
453
|
+
const alive = runs.filter((run) => run.isAlive).length;
|
|
454
|
+
if (alive > 0) {
|
|
455
|
+
byAddress.set(address, alive);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return byAddress;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
runningCount() {
|
|
462
|
+
let running = 0;
|
|
463
|
+
for (const runs of this.#runs.values()) {
|
|
464
|
+
for (const run of runs) {
|
|
465
|
+
if (run.isAlive && !run.isSuspended) {
|
|
466
|
+
running += 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return running;
|
|
471
|
+
}
|
|
472
|
+
|
|
252
473
|
/**
|
|
253
474
|
* How long an output is, once its playlist is known.
|
|
254
475
|
*
|
|
@@ -307,7 +528,7 @@ export class EncodeOrchestrator {
|
|
|
307
528
|
* @param {{ from: number, to: number, priority: number, withinSeconds: number }[]} zones
|
|
308
529
|
*/
|
|
309
530
|
notePriorityMap(address, zones) {
|
|
310
|
-
this
|
|
531
|
+
this.#demand.state(address, zones);
|
|
311
532
|
// NOBODY IS COMING HERE, so what was remembered about this output's input
|
|
312
533
|
// is about nobody. Kept, those three entries would stay for the life of the
|
|
313
534
|
// process, and the wait they describe would greet whoever opens this output
|
|
@@ -330,6 +551,7 @@ export class EncodeOrchestrator {
|
|
|
330
551
|
// means. A statement kept beside the disk would be a second owner of one
|
|
331
552
|
// fact, which is what item 87 removed from the coverage map.
|
|
332
553
|
this.coverageOf(address).markReady(index);
|
|
554
|
+
this.segmentStore?.announce?.(address, index);
|
|
333
555
|
for (const run of this.runsOn(address)) {
|
|
334
556
|
run.noteProduced(index);
|
|
335
557
|
}
|
|
@@ -364,7 +586,7 @@ export class EncodeOrchestrator {
|
|
|
364
586
|
* state, so a pass that finds nothing to change does nothing.
|
|
365
587
|
*/
|
|
366
588
|
reconcile() {
|
|
367
|
-
const addresses = new Set([...this
|
|
589
|
+
const addresses = new Set([...this.#demand.addresses(), ...this.#runs.keys()]);
|
|
368
590
|
for (const address of addresses) {
|
|
369
591
|
this.#reconcileOne(address);
|
|
370
592
|
}
|
|
@@ -433,12 +655,12 @@ export class EncodeOrchestrator {
|
|
|
433
655
|
// used to reach into the layer that STATES them for the same arithmetic,
|
|
434
656
|
// which is the coupling the layer rule forbids; the arithmetic itself now
|
|
435
657
|
// lives where it belongs to nobody.
|
|
436
|
-
const windows = this
|
|
658
|
+
const windows = this.#demand.mapOn(address);
|
|
437
659
|
const live = this.runsOn(address).filter((run) => run.isAlive);
|
|
438
660
|
// Asked ONCE. It is arithmetic over measurements, but it also says out loud
|
|
439
661
|
// when the reason it cuts the budget changes, so asking it three times in
|
|
440
662
|
// one pass is three chances to say a thing that happened once.
|
|
441
|
-
const maxRuns = this.#affordableOn(address, live);
|
|
663
|
+
const maxRuns = this.#withinMachine(address, live, this.#affordableOn(address, live));
|
|
442
664
|
// THE TERMS EVERY ARRIVAL IS COMPUTED FROM, named here so the line below can
|
|
443
665
|
// print them. A decision of this plan is `delay + (index - at) / rate +
|
|
444
666
|
// madeBetween * refetch` against a deadline, so without the rate and the two
|
|
@@ -691,6 +913,42 @@ export class EncodeOrchestrator {
|
|
|
691
913
|
return budget.runs;
|
|
692
914
|
}
|
|
693
915
|
|
|
916
|
+
/**
|
|
917
|
+
* The per-output limit, bounded by what the whole machine can still hold.
|
|
918
|
+
*
|
|
919
|
+
* The per-output answer is at least one whatever else is running; the
|
|
920
|
+
* machine's may be none, and that is the refusal: the output gets no encoder
|
|
921
|
+
* until one elsewhere ends. It is never below what already runs here, so a
|
|
922
|
+
* refusal stops a start and never takes an encoder away.
|
|
923
|
+
*
|
|
924
|
+
* Said out loud when the machine is what bound it, once per change.
|
|
925
|
+
*
|
|
926
|
+
* @param {string} address
|
|
927
|
+
* @param {object[]} live
|
|
928
|
+
* @param {number} perOutput
|
|
929
|
+
* @returns {number}
|
|
930
|
+
*/
|
|
931
|
+
#withinMachine(address, live, perOutput) {
|
|
932
|
+
if (!this.admission) {
|
|
933
|
+
return perOutput;
|
|
934
|
+
}
|
|
935
|
+
const machine = this.admission.placesFor(address, live.length);
|
|
936
|
+
const runs = Math.min(perOutput, machine.runs);
|
|
937
|
+
const said = runs < perOutput ? `${runs}:${machine.because}` : "";
|
|
938
|
+
if (said !== (this.#lastMachineBound.get(address) ?? "")) {
|
|
939
|
+
if (said) {
|
|
940
|
+
this.logger.info(
|
|
941
|
+
`encode: ${runs} encoder(s) on ${address} — the whole machine holds no more ` +
|
|
942
|
+
`(${machine.because}; this output alone would be allowed ${perOutput})`
|
|
943
|
+
);
|
|
944
|
+
this.#lastMachineBound.set(address, said);
|
|
945
|
+
} else {
|
|
946
|
+
this.#lastMachineBound.delete(address);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return runs;
|
|
950
|
+
}
|
|
951
|
+
|
|
694
952
|
/**
|
|
695
953
|
* Take charge of a run this class did not start.
|
|
696
954
|
*
|
|
@@ -734,6 +992,16 @@ export class EncodeOrchestrator {
|
|
|
734
992
|
* @param {import("../encode/EncodeRun.js").RunEnded} ended
|
|
735
993
|
*/
|
|
736
994
|
noteEnded(ended) {
|
|
995
|
+
this.#lastState.set(ended.address, ended.run?.state ?? "IDLE");
|
|
996
|
+
if (ended.lastError) {
|
|
997
|
+
this.#lastError.set(ended.address, ended.lastError);
|
|
998
|
+
} else if (ended.because && ended.ending !== ENCODE_EXIT.COMPLETE) {
|
|
999
|
+
this.#lastError.set(ended.address, ended.because);
|
|
1000
|
+
}
|
|
1001
|
+
const progress = ended.run?.progress?.snapshot?.();
|
|
1002
|
+
if (progress) {
|
|
1003
|
+
this.#lastProgress.set(ended.address, progress);
|
|
1004
|
+
}
|
|
737
1005
|
this.#costs.note(ended);
|
|
738
1006
|
// Exactly one ending is normal — the run reached the end of the stretch it
|
|
739
1007
|
// was given and closed its last file. Every other leaves a piece open, and
|
|
@@ -889,9 +1157,9 @@ export class EncodeOrchestrator {
|
|
|
889
1157
|
*/
|
|
890
1158
|
describe() {
|
|
891
1159
|
const parts = [];
|
|
892
|
-
for (const address of new Set([...this
|
|
1160
|
+
for (const address of new Set([...this.#demand.addresses(), ...this.#runs.keys()])) {
|
|
893
1161
|
const coverage = this.coverageOf(address);
|
|
894
|
-
const stated = this
|
|
1162
|
+
const stated = this.#demand.mapOn(address);
|
|
895
1163
|
const windows = stated.map((zone) => ({ from: zone.from, to: zone.to }));
|
|
896
1164
|
const waiting = firstUnmetWant(coverage, windows);
|
|
897
1165
|
const runs = this.runsOn(address)
|
|
@@ -921,7 +1189,7 @@ export class EncodeOrchestrator {
|
|
|
921
1189
|
// existed there was no figure anywhere saying whether the urgent zone was
|
|
922
1190
|
// served first — the map could have been read backwards and every line
|
|
923
1191
|
// above would have looked the same.
|
|
924
|
-
const served =
|
|
1192
|
+
const served = this.#describeWaits(address);
|
|
925
1193
|
parts.push(
|
|
926
1194
|
`${address} ready=${coverage.stats().ready}` +
|
|
927
1195
|
`${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
|
*
|