@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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The bitrate limit and the level are decided with the rest of an
|
|
3
|
+
* output's format, before it is named — roadmap item 97, step 10.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here starts ffmpeg, a torrent or a worker: the decision is a pure
|
|
6
|
+
* function of values.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { decideOutputFormat } from "../services/encode/quality/output-format.js";
|
|
12
|
+
import { nominalKbpsFor, softwareRateControlFor } from "../services/encode/args.js";
|
|
13
|
+
import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../services/encode/output/OutputSpec.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A 720p output asked for exactly, from a 1080p source, on the encoder given.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ kind: string, name: string }} encoder
|
|
19
|
+
* @param {number | null} [capKbps]
|
|
20
|
+
* @returns {ReturnType<typeof decideOutputFormat>}
|
|
21
|
+
*/
|
|
22
|
+
function decide(encoder, capKbps = null) {
|
|
23
|
+
return decideOutputFormat({
|
|
24
|
+
encodesPicture: true,
|
|
25
|
+
exact: true,
|
|
26
|
+
target: { width: 1280, height: 720 },
|
|
27
|
+
source: { width: 1920, height: 1080, megabitsPerSecond: 8, decode: null },
|
|
28
|
+
fps: 24,
|
|
29
|
+
encoder,
|
|
30
|
+
benchmark: null,
|
|
31
|
+
cost: { decodeModel: null, observedDecodeCostSec: null, requiredSpeed: null },
|
|
32
|
+
chooseBudget: () => null,
|
|
33
|
+
tonemap: false,
|
|
34
|
+
capKbps,
|
|
35
|
+
specWith: (encode) => new OutputSpec({
|
|
36
|
+
sourceKey: "torrent:abc",
|
|
37
|
+
segmentFormatId: "fmp4",
|
|
38
|
+
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
39
|
+
video: new VideoOutput({ fileIndex: 0, encode }),
|
|
40
|
+
audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: true })
|
|
41
|
+
}),
|
|
42
|
+
serving: { mode: "manual", linkMbps: null, keys: [], readyAt: () => false }
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SOFTWARE = { kind: "software", name: "libx264" };
|
|
47
|
+
const HARDWARE = { kind: "nvenc", name: "h264_nvenc" };
|
|
48
|
+
|
|
49
|
+
test("a software output is bounded at its size's nominal rate unless a lower limit is asked for", () => {
|
|
50
|
+
const plain = decide(SOFTWARE).spec.video.encode.rateControl;
|
|
51
|
+
const lower = decide(SOFTWARE, 1400).spec.video.encode.rateControl;
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(plain, softwareRateControlFor({ width: 1280, height: 720, fps: 24 }));
|
|
54
|
+
assert.deepEqual(lower, softwareRateControlFor({ width: 1280, height: 720, fps: 24, capKbps: 1400 }));
|
|
55
|
+
assert.equal(lower.level, plain.level, "both are declared at the nominal output's level");
|
|
56
|
+
assert.notEqual(decide(SOFTWARE).spec.toKey(), decide(SOFTWARE, 1400).spec.toKey(), "and are two outputs");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a limit named in the request is not answered by an output of another limit already here", () => {
|
|
60
|
+
// The nominal 720p output exists and is ready, and it suits this viewer —
|
|
61
|
+
// which is exactly what a move to a lower limit is for leaving.
|
|
62
|
+
const nominal = decide(SOFTWARE).spec.toKey();
|
|
63
|
+
const asked = decideOutputFormat({
|
|
64
|
+
encodesPicture: true,
|
|
65
|
+
exact: true,
|
|
66
|
+
target: { width: 1280, height: 720 },
|
|
67
|
+
source: { width: 1920, height: 1080, megabitsPerSecond: 8, decode: null },
|
|
68
|
+
fps: 24,
|
|
69
|
+
encoder: SOFTWARE,
|
|
70
|
+
benchmark: null,
|
|
71
|
+
cost: { decodeModel: null, observedDecodeCostSec: null, requiredSpeed: null },
|
|
72
|
+
chooseBudget: () => null,
|
|
73
|
+
tonemap: false,
|
|
74
|
+
capKbps: 1400,
|
|
75
|
+
specWith: (encode) => new OutputSpec({
|
|
76
|
+
sourceKey: "torrent:abc",
|
|
77
|
+
segmentFormatId: "fmp4",
|
|
78
|
+
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
79
|
+
video: new VideoOutput({ fileIndex: 0, encode }),
|
|
80
|
+
audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: true })
|
|
81
|
+
}),
|
|
82
|
+
serving: { mode: "manual", linkMbps: null, keys: [nominal], readyAt: () => true }
|
|
83
|
+
});
|
|
84
|
+
assert.equal(asked.reusedKey, null);
|
|
85
|
+
assert.deepEqual(asked.spec.video.encode.rateControl, softwareRateControlFor({ width: 1280, height: 720, fps: 24, capKbps: 1400 }));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a hardware output states no limit, because it is given none", () => {
|
|
89
|
+
assert.equal(decide(HARDWARE).spec.video.encode.rateControl, null);
|
|
90
|
+
assert.ok(decide(HARDWARE).spec.toKey().endsWith("/vbv=-:a=0/0/aac"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a limit above the size's own is refused rather than lowered", () => {
|
|
94
|
+
assert.throws(() => decide(SOFTWARE, nominalKbpsFor({ width: 1280, height: 720 }) + 1), RangeError);
|
|
95
|
+
assert.throws(() => decide(SOFTWARE, 0), RangeError, "and a limit that is not a positive rate is not a limit");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Step 11: the viewer's own link decides whether a format may be given at all.
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A 720p output of a 1080p source for a viewer whose link measured `linkMbps`.
|
|
104
|
+
*
|
|
105
|
+
* @param {{ mode?: "auto" | "manual", linkMbps?: number | null, limits?: number[], encoder?: object, audioLoad?: object | null }} how
|
|
106
|
+
*/
|
|
107
|
+
function decideFor({ mode = "manual", linkMbps = null, limits = null, encoder = SOFTWARE, audioLoad = null } = {}) {
|
|
108
|
+
return decideOutputFormat({
|
|
109
|
+
encodesPicture: true,
|
|
110
|
+
exact: true,
|
|
111
|
+
target: { width: 1280, height: 720 },
|
|
112
|
+
source: { width: 1920, height: 1080, megabitsPerSecond: 8, decode: null },
|
|
113
|
+
fps: 24,
|
|
114
|
+
encoder,
|
|
115
|
+
benchmark: null,
|
|
116
|
+
cost: { decodeModel: null, observedDecodeCostSec: null, requiredSpeed: null },
|
|
117
|
+
chooseBudget: () => null,
|
|
118
|
+
tonemap: false,
|
|
119
|
+
...(limits ? { limitsFor: (frame) => (frame.height === 720 ? limits : [nominalKbpsFor(frame)]) } : {}),
|
|
120
|
+
audioLoad,
|
|
121
|
+
specWith: (encode) => new OutputSpec({
|
|
122
|
+
sourceKey: "torrent:abc",
|
|
123
|
+
segmentFormatId: "fmp4",
|
|
124
|
+
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
125
|
+
video: new VideoOutput({ fileIndex: 0, encode }),
|
|
126
|
+
audio: null
|
|
127
|
+
}),
|
|
128
|
+
serving: { mode, linkMbps, keys: [], readyAt: () => false }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
test("a size picked by hand keeps its height and takes the highest limit the link admits", () => {
|
|
133
|
+
// 720p nominal peaks at 3.64 Mbit/s; a 3 Mbit/s link admits 2.4 of it.
|
|
134
|
+
const decided = decideFor({ mode: "manual", linkMbps: 3, limits: [2800, 1400] });
|
|
135
|
+
assert.equal(decided.unavailable, null);
|
|
136
|
+
assert.equal(decided.spec.video.encode.height, 720, "the height picked by hand is kept");
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
decided.spec.video.encode.rateControl,
|
|
139
|
+
softwareRateControlFor({ width: 1280, height: 720, fps: 24, capKbps: 1400 })
|
|
140
|
+
);
|
|
141
|
+
assert.equal(decided.answer.verdict, "fits");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("AUTO moves to a smaller rung only when that rung's own load is admitted", () => {
|
|
145
|
+
// Nothing at 720p fits 3 Mbit/s with only the nominal limit, and 480p's
|
|
146
|
+
// nominal peak (1.82 Mbit/s) does.
|
|
147
|
+
const decided = decideFor({ mode: "auto", linkMbps: 3 });
|
|
148
|
+
assert.equal(decided.unavailable, null);
|
|
149
|
+
assert.ok(decided.spec.video.encode.height < 720, "a smaller rung");
|
|
150
|
+
assert.ok(decided.answer.admitted, "whose own load the link admits");
|
|
151
|
+
assert.equal(
|
|
152
|
+
decideFor({ mode: "auto", linkMbps: 0.3 }).unavailable?.reason?.length > 0,
|
|
153
|
+
true,
|
|
154
|
+
"and with nothing admitted at any rung, nothing is handed over"
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("nothing admitted is answered as unavailable, with the figures, and names no output", () => {
|
|
159
|
+
const decided = decideFor({ mode: "manual", linkMbps: 3 });
|
|
160
|
+
assert.equal(decided.spec, null, "a format known not to fit is never handed over");
|
|
161
|
+
assert.ok(decided.unavailable.reason.length > 0);
|
|
162
|
+
assert.equal(decided.unavailable.figures.verdict, "does not fit");
|
|
163
|
+
assert.equal(decided.unavailable.figures.linkMbps, 3);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a hardware output is unavailable against a measured link, and given while nothing measured it", () => {
|
|
167
|
+
assert.equal(decideFor({ encoder: HARDWARE, linkMbps: 50 }).unavailable.figures.verdict, "no safe bound");
|
|
168
|
+
assert.equal(decideFor({ encoder: HARDWARE, linkMbps: null }).answer.verdict, "no measurement");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("the soundtrack the viewer hears counts toward what their link carries", () => {
|
|
172
|
+
// 720p nominal peaks at 3.64 Mbit/s. A 4.6 Mbit/s link admits 3.68 of it:
|
|
173
|
+
// enough for the picture, not for the picture and 128 kbit/s of sound.
|
|
174
|
+
const linkMbps = 4.6;
|
|
175
|
+
assert.equal(decideFor({ mode: "manual", linkMbps }).answer.verdict, "fits");
|
|
176
|
+
assert.ok(
|
|
177
|
+
decideFor({ mode: "manual", linkMbps, audioLoad: { mbps: 0.128, peakClass: "known" } }).unavailable,
|
|
178
|
+
"with the sound it no longer fits"
|
|
179
|
+
);
|
|
180
|
+
});
|
|
@@ -15,8 +15,8 @@ import { EventEmitter } from "node:events";
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import fs from "node:fs/promises";
|
|
18
|
-
import { readFragments } from "../services/torrent
|
|
19
|
-
import { SharedPieceStore } from "../services/piece-store/shared-piece-store.js";
|
|
18
|
+
import { readFragments } from "../services/torrent/worker/piece-reader.js";
|
|
19
|
+
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
20
20
|
|
|
21
21
|
const PIECE = 1024;
|
|
22
22
|
const TOTAL = 4 * PIECE;
|
package/test/read-window.test.js
CHANGED
|
@@ -31,8 +31,8 @@ import {
|
|
|
31
31
|
nextWindowPieces,
|
|
32
32
|
readFragments,
|
|
33
33
|
readWindowFor
|
|
34
|
-
} from "../services/torrent
|
|
35
|
-
import { SharedPieceStore } from "../services/piece-store/shared-piece-store.js";
|
|
34
|
+
} from "../services/torrent/worker/piece-reader.js";
|
|
35
|
+
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
36
36
|
|
|
37
37
|
const PIECE = 1024;
|
|
38
38
|
// The production window is 32 MB against 8 MiB pieces — four of them. Sized
|
|
@@ -47,9 +47,16 @@ const WINDOW_PIECES = 4;
|
|
|
47
47
|
async function recordingTorrent({ pieceCount, present = () => true }) {
|
|
48
48
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "read-window-test-"));
|
|
49
49
|
const totalLength = pieceCount * PIECE;
|
|
50
|
+
// ROOM FOR THE WHOLE FIXTURE, because none of these tests is about eviction.
|
|
51
|
+
// At 64 pieces of memory an 8000-piece fixture spilled 7936 of them to disk —
|
|
52
|
+
// a file each — and read most of them back, so four checks about which piece
|
|
53
|
+
// RANGES a reader claims cost 25-28 seconds apiece and this one file took
|
|
54
|
+
// 132 s of a 140 s suite. The store's own behaviour under pressure is
|
|
55
|
+
// measured where that is the subject (`piece-store-eviction`,
|
|
56
|
+
// `piece-lru`, `piece-disk-store`).
|
|
50
57
|
const store = new SharedPieceStore(PIECE, {
|
|
51
58
|
length: totalLength,
|
|
52
|
-
memoryBytes:
|
|
59
|
+
memoryBytes: totalLength,
|
|
53
60
|
path: directory,
|
|
54
61
|
name: "test"
|
|
55
62
|
});
|
package/test/resume-warm.test.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
13
|
import test from "node:test";
|
|
14
14
|
|
|
15
|
-
import { resumeByteOffset } from "../services/torrent
|
|
15
|
+
import { resumeByteOffset } from "../services/torrent/worker/resume-warm.js";
|
|
16
16
|
|
|
17
17
|
test("a position halfway through a film is halfway through its file", () => {
|
|
18
18
|
assert.equal(resumeByteOffset(1000, 100, 50), 500);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What an encoder is told to read.
|
|
3
|
+
*
|
|
4
|
+
* The encoder's own read of its source is the one read that tracks where the
|
|
5
|
+
* viewer is (`reader=playback`), and the only one sized in seconds of playback
|
|
6
|
+
* (`windowBytes`). Both travel on the input address. When the address stopped
|
|
7
|
+
* being kept on the session and started being built per run, both were left
|
|
8
|
+
* off it: every encoder read looked to the stream route like a probe, and the
|
|
9
|
+
* read window fell back to the reader's default.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { encoderInputs } from "../services/encode/run-inputs.js";
|
|
15
|
+
|
|
16
|
+
const BASE = "http://127.0.0.1:9090";
|
|
17
|
+
|
|
18
|
+
function file(fileIndex) {
|
|
19
|
+
return {
|
|
20
|
+
fileIndex,
|
|
21
|
+
streamUrl(baseUrl, { sessionId = "" } = {}) {
|
|
22
|
+
const url = new URL("/stream", `${baseUrl}/`);
|
|
23
|
+
url.searchParams.set("fileIndex", String(fileIndex));
|
|
24
|
+
if (sessionId) {
|
|
25
|
+
url.searchParams.set("session", sessionId);
|
|
26
|
+
}
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("the picture's read says it is the playback read, with its window", () => {
|
|
33
|
+
const picture = file(0);
|
|
34
|
+
const { inputUrl } = encoderInputs({
|
|
35
|
+
picture,
|
|
36
|
+
soundtrack: picture,
|
|
37
|
+
carries: "video-only",
|
|
38
|
+
audioSeparate: true,
|
|
39
|
+
sessionId: "aaaaaaaabbbbcccc",
|
|
40
|
+
readWindowBytes: 32_000_000,
|
|
41
|
+
baseUrl: BASE
|
|
42
|
+
});
|
|
43
|
+
const url = new URL(inputUrl);
|
|
44
|
+
assert.equal(url.searchParams.get("reader"), "playback");
|
|
45
|
+
assert.equal(url.searchParams.get("windowBytes"), "32000000");
|
|
46
|
+
assert.equal(url.searchParams.get("session"), "aaaaaaaabbbbcccc");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("with no window measured the read states neither parameter", () => {
|
|
50
|
+
const picture = file(0);
|
|
51
|
+
const { inputUrl } = encoderInputs({
|
|
52
|
+
picture,
|
|
53
|
+
soundtrack: picture,
|
|
54
|
+
carries: "muxed",
|
|
55
|
+
audioSeparate: false,
|
|
56
|
+
sessionId: "aaaaaaaabbbbcccc",
|
|
57
|
+
readWindowBytes: 0,
|
|
58
|
+
baseUrl: BASE
|
|
59
|
+
});
|
|
60
|
+
const url = new URL(inputUrl);
|
|
61
|
+
assert.equal(url.searchParams.get("windowBytes"), null);
|
|
62
|
+
assert.equal(url.searchParams.get("reader"), null, "exactly as before: both are set together or neither");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a soundtrack in its own file is read directly by its rendition, and is a second input only when muxed", () => {
|
|
66
|
+
const picture = file(0);
|
|
67
|
+
const dub = file(3);
|
|
68
|
+
const rendition = encoderInputs({
|
|
69
|
+
picture, soundtrack: dub, carries: "audio-only", audioSeparate: true,
|
|
70
|
+
sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
|
|
71
|
+
});
|
|
72
|
+
assert.equal(new URL(rendition.inputUrl).searchParams.get("fileIndex"), "3");
|
|
73
|
+
assert.equal(rendition.audioInputUrl, "");
|
|
74
|
+
const muxed = encoderInputs({
|
|
75
|
+
picture, soundtrack: dub, carries: "muxed", audioSeparate: false,
|
|
76
|
+
sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
|
|
77
|
+
});
|
|
78
|
+
assert.equal(new URL(muxed.inputUrl).searchParams.get("fileIndex"), "0");
|
|
79
|
+
assert.equal(new URL(muxed.audioInputUrl).searchParams.get("fileIndex"), "3");
|
|
80
|
+
});
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
22
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
23
23
|
import test from "node:test";
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
26
|
+
import { segmentCutTimesFrom } from "../services/encode/run-command.js";
|
|
27
|
+
import { describeGridDrift } from "../services/encode/OutputTimes.js";
|
|
26
28
|
|
|
27
29
|
/** What the playlist in the player's hands says. */
|
|
28
30
|
const PUBLISHED = [0, 8.342, 16.684, 25.026, 33.368, 41.71];
|
|
@@ -30,10 +32,10 @@ const PUBLISHED = [0, 8.342, 16.684, 25.026, 33.368, 41.71];
|
|
|
30
32
|
const CORRECTED = [0, 8.342, 14.682, 25.026, 31.366, 41.71];
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
|
-
* @returns {{ manager:
|
|
35
|
+
* @returns {{ manager: object, session: object }}
|
|
34
36
|
*/
|
|
35
37
|
function sessionWithDriftedGrid() {
|
|
36
|
-
const manager =
|
|
38
|
+
const manager = wireOutputs({
|
|
37
39
|
enabled: true,
|
|
38
40
|
ffmpegBin: "ffmpeg",
|
|
39
41
|
localBindHost: "127.0.0.1",
|
|
@@ -48,14 +50,14 @@ function sessionWithDriftedGrid() {
|
|
|
48
50
|
|
|
49
51
|
test("a run starts at the time the player was told, not at the corrected one", () => {
|
|
50
52
|
const { manager, session } = sessionWithDriftedGrid();
|
|
51
|
-
assert.equal(manager.runStartTimeFor(session, 2), 16.684);
|
|
52
|
-
assert.notEqual(manager.runStartTimeFor(session, 2), session.timeline.boundaries[2]);
|
|
53
|
+
assert.equal(manager.outputTimes.runStartTimeFor(session, 2), 16.684);
|
|
54
|
+
assert.notEqual(manager.outputTimes.runStartTimeFor(session, 2), session.timeline.boundaries[2]);
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
test("position and cut list come from the same table", () => {
|
|
56
58
|
const { manager, session } = sessionWithDriftedGrid();
|
|
57
|
-
const grid = manager.publishedGridFor(session);
|
|
58
|
-
const start = manager.runStartTimeFor(session, 2);
|
|
59
|
+
const grid = manager.outputTimes.publishedGridFor(session);
|
|
60
|
+
const start = manager.outputTimes.runStartTimeFor(session, 2);
|
|
59
61
|
// The cut list is stated as offsets from where the run begins. Adding the
|
|
60
62
|
// position back must land on the published boundaries exactly — which is the
|
|
61
63
|
// property that was false while the two came from different tables.
|
|
@@ -66,13 +68,13 @@ test("position and cut list come from the same table", () => {
|
|
|
66
68
|
test("a session that published no grid positions on the live one", () => {
|
|
67
69
|
const { manager } = sessionWithDriftedGrid();
|
|
68
70
|
const session = { id: "no-playlist", timeline: new Timeline({ boundaries: [...CORRECTED], published: [], cutGrid: "uniform" }) };
|
|
69
|
-
assert.equal(manager.runStartTimeFor(session, 2), CORRECTED[2]);
|
|
71
|
+
assert.equal(manager.outputTimes.runStartTimeFor(session, 2), CORRECTED[2]);
|
|
70
72
|
});
|
|
71
73
|
|
|
72
74
|
test("an index beyond the table is clamped rather than returning nothing", () => {
|
|
73
75
|
const { manager, session } = sessionWithDriftedGrid();
|
|
74
|
-
assert.equal(manager.runStartTimeFor(session, 9999), PUBLISHED[PUBLISHED.length - 1]);
|
|
75
|
-
assert.equal(manager.runStartTimeFor(session, -3), PUBLISHED[0]);
|
|
76
|
+
assert.equal(manager.outputTimes.runStartTimeFor(session, 9999), PUBLISHED[PUBLISHED.length - 1]);
|
|
77
|
+
assert.equal(manager.outputTimes.runStartTimeFor(session, -3), PUBLISHED[0]);
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
test("the drift between the two tables is stated in full", () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { RunProgress } from "../services/encode/RunProgress.js";
|
|
4
|
+
import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
|
|
5
|
+
|
|
6
|
+
function orchestrator() {
|
|
7
|
+
return new EncodeOrchestrator({
|
|
8
|
+
maxRunsFor: () => 2,
|
|
9
|
+
makeRun: () => null,
|
|
10
|
+
segmentSeconds: 4,
|
|
11
|
+
logger: { info() {}, warn() {} }
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("a run rebases ffmpeg's relative clock onto its own source position", () => {
|
|
16
|
+
let now = 10;
|
|
17
|
+
const progress = new RunProgress({ startSeconds: 600, totalSeconds: 1000, now: () => now });
|
|
18
|
+
now = 20;
|
|
19
|
+
progress.note({ processedSeconds: 12.5, speed: "2.0x" });
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(progress.snapshot(), {
|
|
22
|
+
processedSeconds: 612.5,
|
|
23
|
+
startPositionSeconds: 600,
|
|
24
|
+
totalSeconds: 1000,
|
|
25
|
+
percent: 3.125,
|
|
26
|
+
remainingSeconds: 387.5,
|
|
27
|
+
speed: "2.0x",
|
|
28
|
+
updatedAt: 20
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("two runs of one output retain separate clocks", () => {
|
|
33
|
+
const made = orchestrator();
|
|
34
|
+
const first = { from: 0, to: 49, isAlive: true, progress: new RunProgress({ startSeconds: 0 }) };
|
|
35
|
+
const second = { from: 50, to: 99, isAlive: true, progress: new RunProgress({ startSeconds: 200 }) };
|
|
36
|
+
first.progress.note({ processedSeconds: 40, speed: "1.0x" });
|
|
37
|
+
second.progress.note({ processedSeconds: 8, speed: "2.0x" });
|
|
38
|
+
made.adopt("picture", first);
|
|
39
|
+
made.adopt("picture", second);
|
|
40
|
+
|
|
41
|
+
assert.equal(made.progressOf("picture", 20).processedSeconds, 40);
|
|
42
|
+
assert.equal(made.progressOf("picture", 60).processedSeconds, 208);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("the last run's progress remains available after it ends", () => {
|
|
46
|
+
const made = orchestrator();
|
|
47
|
+
const progress = new RunProgress({ startSeconds: 100 });
|
|
48
|
+
progress.note({ processedSeconds: 5, speed: "1.5x" });
|
|
49
|
+
const run = { from: 25, to: 30, isAlive: false, state: "ENDED_COMPLETE", progress };
|
|
50
|
+
made.adopt("picture", run);
|
|
51
|
+
made.noteEnded({
|
|
52
|
+
address: "picture",
|
|
53
|
+
run,
|
|
54
|
+
ending: "complete",
|
|
55
|
+
because: "the assigned stretch was completed",
|
|
56
|
+
from: 25,
|
|
57
|
+
to: 30,
|
|
58
|
+
reached: 30,
|
|
59
|
+
livedMs: 1000,
|
|
60
|
+
normal: true,
|
|
61
|
+
lastError: ""
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.equal(made.progressOf("picture").processedSeconds, 105);
|
|
65
|
+
});
|