@torrent-tv/proxy 2.86.0 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2138 -1915
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +745 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +931 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/wire-outputs.js +958 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -45
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -515
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +40 -8
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -202
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9608
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/services/viewer/link-readings.js +0 -56
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import assert from "node:assert/strict";
|
|
16
|
-
import { SourceFile } from "../services/
|
|
16
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
17
17
|
import { startRunOn } from "./helpers/encode-run.js";
|
|
18
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
18
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
19
19
|
import test from "node:test";
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
22
22
|
import { trueStartOf } from "../services/encode/run-command.js";
|
|
23
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
23
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
24
24
|
import { ENCODE_RUN_STATE, INITIAL_RUN_STATE } from "../services/encode/encode-run-state.js";
|
|
25
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
25
26
|
|
|
26
27
|
const BOUNDARIES = [0, 4, 8, 12, 16, 20];
|
|
27
28
|
|
|
@@ -29,13 +30,13 @@ const BOUNDARIES = [0, 4, 8, 12, 16, 20];
|
|
|
29
30
|
* A film's family: the picture, and a soundtrack rendition of it. Both runs
|
|
30
31
|
* begin at boundary #2, which the container's table puts at 8 s.
|
|
31
32
|
*
|
|
32
|
-
* @returns {{ manager:
|
|
33
|
+
* @returns {{ manager: object, picture: object, sound: object }}
|
|
33
34
|
*/
|
|
34
35
|
function familyAtBoundaryTwo() {
|
|
35
36
|
// ONE table for the film. Where it is cut is a fact about the FILE, so the
|
|
36
37
|
// picture and its soundtrack hold the same array rather than a copy each.
|
|
37
38
|
const boundaries = [...BOUNDARIES];
|
|
38
|
-
const manager =
|
|
39
|
+
const manager = wireOutputs({
|
|
39
40
|
enabled: true,
|
|
40
41
|
ffmpegBin: "ffmpeg",
|
|
41
42
|
localBindHost: "127.0.0.1",
|
|
@@ -43,6 +44,8 @@ function familyAtBoundaryTwo() {
|
|
|
43
44
|
});
|
|
44
45
|
const picture = {
|
|
45
46
|
id: "picture",
|
|
47
|
+
spec: outputSpec({ transcodeVideo: false }),
|
|
48
|
+
get outputKey() { return this.spec.toKey(); },
|
|
46
49
|
state: "ready",
|
|
47
50
|
timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
|
|
48
51
|
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
|
|
@@ -57,16 +60,15 @@ function familyAtBoundaryTwo() {
|
|
|
57
60
|
segmentFormat: fmp4Format,
|
|
58
61
|
// Where its encoder has got to, which a run writes into as it works.
|
|
59
62
|
progress: { processedSeconds: 0 },
|
|
60
|
-
runs: new Set()
|
|
61
|
-
pendingRun: null,
|
|
62
|
-
indexCheck: null
|
|
63
|
+
runs: new Set()
|
|
63
64
|
};
|
|
64
65
|
// The soundtrack of that picture: the same file, published on its own. Found
|
|
65
66
|
// by what it is, since no list of ids names it any more.
|
|
66
67
|
const sound = {
|
|
67
68
|
id: "sound",
|
|
69
|
+
spec: outputSpec({ audioOnly: true }),
|
|
70
|
+
get outputKey() { return this.spec.toKey(); },
|
|
68
71
|
state: "ready",
|
|
69
|
-
audioOnly: true,
|
|
70
72
|
baseSessionId: "picture",
|
|
71
73
|
timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
|
|
72
74
|
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
|
|
@@ -81,14 +83,14 @@ function familyAtBoundaryTwo() {
|
|
|
81
83
|
segmentFormat: fmp4Format,
|
|
82
84
|
// Where its encoder has got to, which a run writes into as it works.
|
|
83
85
|
progress: { processedSeconds: 0 },
|
|
84
|
-
runs: new Set()
|
|
85
|
-
pendingRun: null,
|
|
86
|
-
indexCheck: null
|
|
86
|
+
runs: new Set()
|
|
87
87
|
};
|
|
88
|
-
startRunOn(picture, { from: 2 });
|
|
89
|
-
startRunOn(sound, { from: 2 });
|
|
90
|
-
manager.
|
|
91
|
-
manager.
|
|
88
|
+
const pictureRun = startRunOn(picture, { from: 2 });
|
|
89
|
+
const soundRun = startRunOn(sound, { from: 2 });
|
|
90
|
+
manager.encodeOrchestrator.adopt(picture.outputKey, pictureRun);
|
|
91
|
+
manager.encodeOrchestrator.adopt(sound.outputKey, soundRun);
|
|
92
|
+
manager.outputs.set("picture", picture);
|
|
93
|
+
manager.outputs.set("sound", sound);
|
|
92
94
|
|
|
93
95
|
return { manager, picture, sound };
|
|
94
96
|
}
|
|
@@ -97,7 +99,7 @@ test("a soundtrack follows the picture to the instant the picture really began",
|
|
|
97
99
|
const { manager, picture, sound } = familyAtBoundaryTwo();
|
|
98
100
|
const runBefore = [...sound.runs][0];
|
|
99
101
|
|
|
100
|
-
manager.correctBoundaryFromSegment(picture, 2, 10.5);
|
|
102
|
+
manager.outputTimes.correctBoundaryFromSegment(picture, 2, 10.5);
|
|
101
103
|
|
|
102
104
|
assert.deepEqual(
|
|
103
105
|
picture.timeline.boundaries,
|
|
@@ -116,7 +118,11 @@ test("a soundtrack follows the picture to the instant the picture really began",
|
|
|
116
118
|
// it is known and no further: the run goes, the stretch it held returns to the
|
|
117
119
|
// map, and where the next one stands follows from where the viewers are.
|
|
118
120
|
assert.equal(runBefore.state, ENCODE_RUN_STATE.STOPPED, "the run in the wrong place goes");
|
|
119
|
-
assert.equal(
|
|
121
|
+
assert.equal(
|
|
122
|
+
manager.encodeOrchestrator.runsOn(sound.outputKey).filter((run) => run.isAlive).length,
|
|
123
|
+
0,
|
|
124
|
+
"and this path places nothing"
|
|
125
|
+
);
|
|
120
126
|
// The corrected instant reaches whatever the plan places there through the
|
|
121
127
|
// table, which every session of the file shares. It used to be passed as an
|
|
122
128
|
// argument from this one call site, so only a run started by that line ever
|
|
@@ -149,7 +155,7 @@ test("a correction the table already holds moves nobody", () => {
|
|
|
149
155
|
// Within the tolerance: the reading agrees with the table, so there is
|
|
150
156
|
// nothing to correct and nothing to move. This is what makes the repositioning
|
|
151
157
|
// converge instead of repeating on every produced segment.
|
|
152
|
-
manager.correctBoundaryFromSegment(picture, 2, 8.1);
|
|
158
|
+
manager.outputTimes.correctBoundaryFromSegment(picture, 2, 8.1);
|
|
153
159
|
assert.deepEqual(sound.timeline.boundaries, before);
|
|
154
160
|
});
|
|
155
161
|
|
|
@@ -161,8 +167,12 @@ test("a member that is not running is left alone", () => {
|
|
|
161
167
|
soundRun.stop("the viewer switched away");
|
|
162
168
|
// A stopped run is no longer live, so nothing of this session begins at #2
|
|
163
169
|
// any more — which is what "left alone" means here.
|
|
164
|
-
manager.correctBoundaryFromSegment(picture, 2, 10.5);
|
|
165
|
-
assert.equal(
|
|
170
|
+
manager.outputTimes.correctBoundaryFromSegment(picture, 2, 10.5);
|
|
171
|
+
assert.equal(
|
|
172
|
+
manager.encodeOrchestrator.runsOn(sound.outputKey).filter((run) => run.isAlive).length,
|
|
173
|
+
0,
|
|
174
|
+
"a stopped member is not started again for nobody"
|
|
175
|
+
);
|
|
166
176
|
assert.equal(soundRun.from, 2, "a stopped member keeps its place and its silence");
|
|
167
177
|
assert.equal(soundRun.state, ENCODE_RUN_STATE.STOPPED);
|
|
168
178
|
assert.notEqual(INITIAL_RUN_STATE, ENCODE_RUN_STATE.STOPPED);
|
|
@@ -183,7 +193,7 @@ test("a soundtrack does not move the grid the picture is cut on", () => {
|
|
|
183
193
|
// 1.951 s apart, each overwriting the other for as long as the film ran. The
|
|
184
194
|
// table never converged, so the guard that stops a correction the table
|
|
185
195
|
// already holds never fired.
|
|
186
|
-
manager.correctBoundaryFromSegment(sound, 2, 10.5);
|
|
196
|
+
manager.outputTimes.correctBoundaryFromSegment(sound, 2, 10.5);
|
|
187
197
|
|
|
188
198
|
assert.deepEqual(
|
|
189
199
|
picture.timeline.boundaries,
|
|
@@ -191,5 +201,9 @@ test("a soundtrack does not move the grid the picture is cut on", () => {
|
|
|
191
201
|
"the grid is the picture's cut list and a soundtrack may not move it"
|
|
192
202
|
);
|
|
193
203
|
assert.deepEqual(sound.timeline.boundaries, soundBefore);
|
|
194
|
-
assert.equal(
|
|
204
|
+
assert.equal(
|
|
205
|
+
manager.encodeOrchestrator.runsOn(picture.outputKey).filter((run) => run.isAlive).length,
|
|
206
|
+
1,
|
|
207
|
+
"and nothing is restarted on a soundtrack's say-so"
|
|
208
|
+
);
|
|
195
209
|
});
|
|
@@ -17,7 +17,7 @@ import assert from "node:assert/strict";
|
|
|
17
17
|
import test from "node:test";
|
|
18
18
|
import { WebSocketServer } from "ws";
|
|
19
19
|
|
|
20
|
-
import { createTunnelClient } from "../services/tunnel-client.js";
|
|
20
|
+
import { createTunnelClient } from "../services/transport/tunnel-client.js";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Wait for the thing being asserted, not for a length of time.
|
|
@@ -22,8 +22,8 @@ import { fakeProcess as fakeEncoder, startRunOn } from "./helpers/encode-run.js"
|
|
|
22
22
|
* @param {object} session
|
|
23
23
|
* @returns {boolean}
|
|
24
24
|
*/
|
|
25
|
-
function encoding(
|
|
26
|
-
return
|
|
25
|
+
function encoding(manager, output) {
|
|
26
|
+
return manager.encodeOrchestrator.runsOn(output?.outputKey ?? "").some((run) => run.isAlive);
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Whether anybody is still watching this output.
|
|
@@ -42,22 +42,23 @@ function encoding(session) {
|
|
|
42
42
|
* @param {object} session
|
|
43
43
|
* @returns {boolean}
|
|
44
44
|
*/
|
|
45
|
-
function watched(session) {
|
|
46
|
-
return
|
|
45
|
+
function watched(manager, session) {
|
|
46
|
+
return session ? manager.viewers.forOutput(session).size > 0 : false;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
import assert from "node:assert/strict";
|
|
50
|
-
import { SourceFile } from "../services/
|
|
51
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
50
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
51
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
52
52
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
53
53
|
import os from "node:os";
|
|
54
54
|
import path from "node:path";
|
|
55
|
-
import { audioRenditionKey
|
|
56
|
-
import {
|
|
57
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
58
|
-
import { Output } from "../services/output/Output.js";
|
|
55
|
+
import { audioRenditionKey } from "../services/encode/Renditions.js";
|
|
56
|
+
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
57
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
58
|
+
import { Output } from "../services/encode/output/Output.js";
|
|
59
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
59
60
|
|
|
60
|
-
const BASE_ID = "
|
|
61
|
+
const BASE_ID = "aaaaaaaabbbbcccc";
|
|
61
62
|
const SEGMENT_SECONDS = 4;
|
|
62
63
|
const FIRST = "viewer-one";
|
|
63
64
|
const SECOND = "viewer-two";
|
|
@@ -65,12 +66,35 @@ const SECOND = "viewer-two";
|
|
|
65
66
|
/**
|
|
66
67
|
* A session shaped like a live one, without the ffmpeg run behind it.
|
|
67
68
|
*
|
|
68
|
-
* @param {{ id: string, dirPath: string, audioTrackIndex?: number, transcodeAudio?: boolean }} params
|
|
69
|
+
* @param {{ id: string, dirPath: string, audioTrackIndex?: number, transcodeAudio?: boolean, audioOnly?: boolean, audioSeparate?: boolean }} params
|
|
69
70
|
* @returns {object}
|
|
70
71
|
*/
|
|
71
|
-
function fakeSession({
|
|
72
|
+
function fakeSession({
|
|
73
|
+
id,
|
|
74
|
+
dirPath,
|
|
75
|
+
audioTrackIndex = 0,
|
|
76
|
+
transcodeAudio = true,
|
|
77
|
+
audioOnly = false,
|
|
78
|
+
audioSeparate = false,
|
|
79
|
+
// THE SIZE IS PART OF THE IDENTITY, so two steps of different sizes are two
|
|
80
|
+
// addresses. Left out, every step of this fixture shared one key and the
|
|
81
|
+
// record of which output answers a height could not tell them apart — which
|
|
82
|
+
// is the fixture describing something production does not do, not a rule
|
|
83
|
+
// being too strict.
|
|
84
|
+
encodeHeight = 0
|
|
85
|
+
}) {
|
|
72
86
|
return {
|
|
73
87
|
id,
|
|
88
|
+
spec: outputSpec({
|
|
89
|
+
transcodeVideo: !audioOnly,
|
|
90
|
+
transcodeAudio,
|
|
91
|
+
audioOnly,
|
|
92
|
+
audioSeparate,
|
|
93
|
+
audioSourceTrackIndex: audioTrackIndex,
|
|
94
|
+
height: encodeHeight,
|
|
95
|
+
width: encodeHeight > 0 ? Math.round((encodeHeight * 16) / 9) : 0
|
|
96
|
+
}),
|
|
97
|
+
get outputKey() { return this.spec.toKey(); },
|
|
74
98
|
dirPath,
|
|
75
99
|
// Where this file is cut, held by the file. A fixture that stated it
|
|
76
100
|
// on the session was describing what production no longer does.
|
|
@@ -85,12 +109,9 @@ function fakeSession({ id, dirPath, audioTrackIndex = 0, transcodeAudio = true }
|
|
|
85
109
|
get inputFile() { return this.file; },
|
|
86
110
|
get audioFile() { return this.file; },
|
|
87
111
|
startedAt: Date.now(),
|
|
88
|
-
createEntryMs: Date.now(),
|
|
89
112
|
lastAccessedAt: Date.now(),
|
|
90
113
|
ffmpeg: null,
|
|
91
114
|
lastError: "",
|
|
92
|
-
consumers: new Set(),
|
|
93
|
-
viewers: new Map(),
|
|
94
115
|
|
|
95
116
|
segmentFormat: fmp4Format,
|
|
96
117
|
transcodeVideo: false,
|
|
@@ -115,31 +136,35 @@ function fakeSession({ id, dirPath, audioTrackIndex = 0, transcodeAudio = true }
|
|
|
115
136
|
};
|
|
116
137
|
}
|
|
117
138
|
|
|
139
|
+
function startManagedRun(manager, output, options = {}) {
|
|
140
|
+
const run = startRunOn(output, options);
|
|
141
|
+
manager.encodeOrchestrator.adopt(output.outputKey, run);
|
|
142
|
+
return run;
|
|
143
|
+
}
|
|
144
|
+
|
|
118
145
|
|
|
119
146
|
/**
|
|
120
147
|
* A base picture serving two viewers, with its audio published separately and
|
|
121
148
|
* every rendition created by a stub instead of an encoder.
|
|
122
149
|
*
|
|
123
|
-
* @returns {Promise<{ manager:
|
|
150
|
+
* @returns {Promise<{ manager: object, base: object, dirPath: string, renditions: Map<string, object> }>}
|
|
124
151
|
*/
|
|
125
152
|
async function pictureWithTwoViewers() {
|
|
126
153
|
const dirPath = await mkdtemp(path.join(os.tmpdir(), "two-viewers-"));
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
const base = fakeSession({ id: BASE_ID, dirPath });
|
|
134
|
-
base.audioSeparate = true;
|
|
135
|
-
base.consumers = new Set([FIRST, SECOND]);
|
|
154
|
+
// Its own store root — see `helpers/manager.js` for what sharing one cost.
|
|
155
|
+
const { manager } = managerWithOwnStore();
|
|
156
|
+
// These checks cover ownership changes made by viewer requests. Encoder
|
|
157
|
+
// placement is covered by the plan tests and must not run asynchronously in
|
|
158
|
+
// the middle of an assertion about the request path.
|
|
159
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
160
|
+
const base = fakeSession({ id: BASE_ID, dirPath, audioSeparate: true });
|
|
136
161
|
// Both viewers are watching the picture, which is what keeps their choices
|
|
137
162
|
// alive; a viewer whose head has expired holds no encoder.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
manager.
|
|
163
|
+
manager.viewers.of(base, FIRST).position = { segment: 3, seconds: 12, at: Date.now() };
|
|
164
|
+
manager.viewers.of(base, SECOND).position = { segment: 3, seconds: 12, at: Date.now() };
|
|
165
|
+
manager.viewers.of(base, FIRST).audio = { trackIndex: 0, transcode: true };
|
|
166
|
+
manager.viewers.of(base, SECOND).audio = { trackIndex: 1, transcode: true };
|
|
167
|
+
manager.outputs.set(BASE_ID, base);
|
|
143
168
|
manager.getCachedAudioTracks = () => [
|
|
144
169
|
{ index: 0, language: "rus", title: "Дубляж", isDefault: true, fileIndex: 0, sourceTrackIndex: 0 },
|
|
145
170
|
{ index: 1, language: "eng", title: "", isDefault: false, fileIndex: 0, sourceTrackIndex: 1 }
|
|
@@ -148,7 +173,7 @@ async function pictureWithTwoViewers() {
|
|
|
148
173
|
|
|
149
174
|
/** @type {Map<string, object>} */
|
|
150
175
|
const renditions = new Map();
|
|
151
|
-
manager.createOrGetSession = async (params) => {
|
|
176
|
+
manager.viewerRequests.createOrGetSession = async (params) => {
|
|
152
177
|
const key = audioRenditionKey(params.audioTrackIndex, params.transcodeAudio);
|
|
153
178
|
const existing = renditions.get(key);
|
|
154
179
|
if (existing) {
|
|
@@ -158,11 +183,12 @@ async function pictureWithTwoViewers() {
|
|
|
158
183
|
id: `rendition-${key}`,
|
|
159
184
|
dirPath,
|
|
160
185
|
audioTrackIndex: params.audioTrackIndex,
|
|
161
|
-
transcodeAudio: params.transcodeAudio
|
|
186
|
+
transcodeAudio: params.transcodeAudio,
|
|
187
|
+
audioOnly: true
|
|
162
188
|
});
|
|
163
|
-
rendition.
|
|
164
|
-
|
|
165
|
-
manager.
|
|
189
|
+
rendition.id = rendition.spec.toName();
|
|
190
|
+
startManagedRun(manager, rendition, { process: fakeEncoder() });
|
|
191
|
+
manager.outputs.set(rendition.id, rendition);
|
|
166
192
|
renditions.set(key, rendition);
|
|
167
193
|
return rendition;
|
|
168
194
|
};
|
|
@@ -172,12 +198,12 @@ async function pictureWithTwoViewers() {
|
|
|
172
198
|
test("one viewer fetching their soundtrack does not stop the other viewer's", async (t) => {
|
|
173
199
|
const { manager, renditions, dirPath } = await pictureWithTwoViewers();
|
|
174
200
|
t.after(async () => {
|
|
175
|
-
await manager.disposeAll();
|
|
201
|
+
await manager.lifecycle.disposeAll();
|
|
176
202
|
await rm(dirPath, { recursive: true, force: true });
|
|
177
203
|
});
|
|
178
204
|
|
|
179
|
-
const first = await manager.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
180
|
-
const second = await manager.resolveAudioRenditionFile(BASE_ID, 1, "segment-00003.mp4", SECOND);
|
|
205
|
+
const first = await manager.renditions.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
206
|
+
const second = await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00003.mp4", SECOND);
|
|
181
207
|
|
|
182
208
|
assert.notEqual(first.sessionId, second.sessionId, "two soundtracks are two encodes");
|
|
183
209
|
for (const [key, rendition] of renditions) {
|
|
@@ -186,8 +212,8 @@ test("one viewer fetching their soundtrack does not stop the other viewer's", as
|
|
|
186
212
|
}
|
|
187
213
|
|
|
188
214
|
// And it holds under the traffic that actually happens: they alternate.
|
|
189
|
-
await manager.resolveAudioRenditionFile(BASE_ID, 0, "segment-00004.mp4", FIRST);
|
|
190
|
-
await manager.resolveAudioRenditionFile(BASE_ID, 1, "segment-00004.mp4", SECOND);
|
|
215
|
+
await manager.renditions.resolveAudioRenditionFile(BASE_ID, 0, "segment-00004.mp4", FIRST);
|
|
216
|
+
await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00004.mp4", SECOND);
|
|
191
217
|
for (const [key, rendition] of renditions) {
|
|
192
218
|
assert.deepEqual([...rendition.runs][0]?.process?.signals ?? [], [], `nothing signalled ${key} on the second round`);
|
|
193
219
|
}
|
|
@@ -196,22 +222,20 @@ test("one viewer fetching their soundtrack does not stop the other viewer's", as
|
|
|
196
222
|
test("a soundtrack nobody is listening to any more is let go of", async (t) => {
|
|
197
223
|
const { manager, base, renditions, dirPath } = await pictureWithTwoViewers();
|
|
198
224
|
t.after(async () => {
|
|
199
|
-
await manager.disposeAll();
|
|
225
|
+
await manager.lifecycle.disposeAll();
|
|
200
226
|
await rm(dirPath, { recursive: true, force: true });
|
|
201
227
|
});
|
|
202
228
|
// One viewer only, so what they leave is left for nobody. This is the case
|
|
203
229
|
// the stop exists for: an encoder AND a reader holding pieces of the torrent.
|
|
204
|
-
base
|
|
205
|
-
base.viewers.delete(SECOND);
|
|
206
|
-
base.viewers.delete(SECOND);
|
|
230
|
+
manager.viewers.leaves(base, SECOND);
|
|
207
231
|
|
|
208
|
-
await manager.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
209
|
-
await manager.resolveAudioRenditionFile(BASE_ID, 1, "segment-00004.mp4", FIRST);
|
|
232
|
+
await manager.renditions.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
233
|
+
await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00004.mp4", FIRST);
|
|
210
234
|
|
|
211
235
|
const left = renditions.get(audioRenditionKey(0, true));
|
|
212
236
|
const moved = renditions.get(audioRenditionKey(1, true));
|
|
213
|
-
assert.equal(watched(left), false, "the track the viewer left is nobody's now");
|
|
214
|
-
assert.ok(watched(moved), "and the track they moved to is theirs");
|
|
237
|
+
assert.equal(watched(manager, left), false, "the track the viewer left is nobody's now");
|
|
238
|
+
assert.ok(watched(manager, moved), "and the track they moved to is theirs");
|
|
215
239
|
assert.deepEqual(
|
|
216
240
|
[...left.runs][0]?.process?.signals ?? [],
|
|
217
241
|
[],
|
|
@@ -222,35 +246,35 @@ test("a soundtrack nobody is listening to any more is let go of", async (t) => {
|
|
|
222
246
|
test("each viewer's browser decides for itself whether its soundtrack is re-encoded", async (t) => {
|
|
223
247
|
const { manager, base, renditions, dirPath } = await pictureWithTwoViewers();
|
|
224
248
|
t.after(async () => {
|
|
225
|
-
await manager.disposeAll();
|
|
249
|
+
await manager.lifecycle.disposeAll();
|
|
226
250
|
await rm(dirPath, { recursive: true, force: true });
|
|
227
251
|
});
|
|
228
252
|
// The same track, two browsers: one can decode it as it stands, the other
|
|
229
253
|
// cannot. Answering both from the session's own flag would leave the second
|
|
230
254
|
// viewer with silence.
|
|
231
|
-
|
|
232
|
-
|
|
255
|
+
manager.viewers.of(base, FIRST).audio = { trackIndex: 0, transcode: false };
|
|
256
|
+
manager.viewers.of(base, SECOND).audio = { trackIndex: 0, transcode: true };
|
|
233
257
|
|
|
234
|
-
const copied = await manager.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
235
|
-
const encoded = await manager.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", SECOND);
|
|
258
|
+
const copied = await manager.renditions.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
259
|
+
const encoded = await manager.renditions.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", SECOND);
|
|
236
260
|
|
|
237
261
|
assert.notEqual(copied.sessionId, encoded.sessionId);
|
|
238
262
|
assert.equal(renditions.get(audioRenditionKey(0, false)).transcodeAudio, false);
|
|
239
263
|
assert.equal(renditions.get(audioRenditionKey(0, true)).transcodeAudio, true);
|
|
240
264
|
// Both are wanted, so neither is stopped.
|
|
241
|
-
assert.ok(encoding(renditions.get(audioRenditionKey(0, false))));
|
|
242
|
-
assert.ok(encoding(renditions.get(audioRenditionKey(0, true))));
|
|
265
|
+
assert.ok(encoding(manager, renditions.get(audioRenditionKey(0, false))));
|
|
266
|
+
assert.ok(encoding(manager, renditions.get(audioRenditionKey(0, true))));
|
|
243
267
|
});
|
|
244
268
|
|
|
245
269
|
test("the master marks each viewer's own soundtrack as the default one", async (t) => {
|
|
246
270
|
const { manager, dirPath } = await pictureWithTwoViewers();
|
|
247
271
|
t.after(async () => {
|
|
248
|
-
await manager.disposeAll();
|
|
272
|
+
await manager.lifecycle.disposeAll();
|
|
249
273
|
await rm(dirPath, { recursive: true, force: true });
|
|
250
274
|
});
|
|
251
275
|
|
|
252
|
-
const forFirst = manager.buildMasterPlaylist(BASE_ID, FIRST);
|
|
253
|
-
const forSecond = manager.buildMasterPlaylist(BASE_ID, SECOND);
|
|
276
|
+
const forFirst = manager.renditions.buildMasterPlaylist(BASE_ID, FIRST);
|
|
277
|
+
const forSecond = manager.renditions.buildMasterPlaylist(BASE_ID, SECOND);
|
|
254
278
|
|
|
255
279
|
const defaultsOf = (master) =>
|
|
256
280
|
[...master.matchAll(/^#EXT-X-MEDIA:.*?NAME="([^"]+)".*?DEFAULT=(YES|NO)/gm)]
|
|
@@ -264,53 +288,62 @@ test("the master marks each viewer's own soundtrack as the default one", async (
|
|
|
264
288
|
test("one viewer changing quality does not take the other off their step", async (t) => {
|
|
265
289
|
const { manager, base, dirPath } = await pictureWithTwoViewers();
|
|
266
290
|
t.after(async () => {
|
|
267
|
-
await manager.disposeAll();
|
|
291
|
+
await manager.lifecycle.disposeAll();
|
|
268
292
|
await rm(dirPath, { recursive: true, force: true });
|
|
269
293
|
});
|
|
270
294
|
/** @type {Map<number, object>} */
|
|
271
295
|
const variants = new Map();
|
|
272
|
-
manager.createOrGetSession = async (params) => {
|
|
296
|
+
manager.viewerRequests.createOrGetSession = async (params) => {
|
|
273
297
|
const height = params.targetHeight;
|
|
274
298
|
const existing = variants.get(height);
|
|
275
299
|
if (existing) {
|
|
276
300
|
return existing;
|
|
277
301
|
}
|
|
278
|
-
|
|
302
|
+
// A STEP CARRIES WHAT ITS PICTURE CARRIES. This picture publishes its
|
|
303
|
+
// soundtrack separately, so its steps do too — which is what production
|
|
304
|
+
// passes (`audioRenditions: servesAudioSeparately(base)`). Built muxed, a
|
|
305
|
+
// step is not the same material as the picture it is a step of, and is
|
|
306
|
+
// rightly refused.
|
|
307
|
+
const variant = fakeSession({ id: `variant-${height}`, dirPath, encodeHeight: height, audioSeparate: true });
|
|
279
308
|
variant.transcodeVideo = true;
|
|
280
309
|
variant.output.encodeHeight = height;
|
|
281
310
|
variant.variantHeight = height;
|
|
282
311
|
variant.isStep = true;
|
|
312
|
+
manager.outputs.markStep(variant);
|
|
283
313
|
variant.file = base.file;
|
|
284
|
-
|
|
285
|
-
manager.
|
|
314
|
+
startManagedRun(manager, variant, { process: fakeEncoder() });
|
|
315
|
+
manager.outputs.set(variant.id, variant);
|
|
286
316
|
variants.set(height, variant);
|
|
287
317
|
return variant;
|
|
288
318
|
};
|
|
289
319
|
|
|
290
|
-
await manager.resolveVariantFile(BASE_ID, 720, "segment-00003.mp4", FIRST);
|
|
291
|
-
|
|
320
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 720, "segment-00003.mp4", FIRST);
|
|
321
|
+
manager.renditions.viewerPlays(BASE_ID, FIRST, 720, 12);
|
|
322
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00003.mp4", SECOND);
|
|
323
|
+
manager.renditions.viewerPlays(BASE_ID, SECOND, 540, 12);
|
|
292
324
|
// Both viewers go on watching their own step, which is what a player does
|
|
293
325
|
// every few seconds.
|
|
294
|
-
await manager.resolveVariantFile(BASE_ID, 720, "segment-00004.mp4", FIRST);
|
|
295
|
-
await manager.resolveVariantFile(BASE_ID, 540, "segment-00004.mp4", SECOND);
|
|
326
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 720, "segment-00004.mp4", FIRST);
|
|
327
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00004.mp4", SECOND);
|
|
296
328
|
|
|
297
|
-
assert.ok(encoding(variants.get(720)), "the first viewer's step is still encoding");
|
|
298
|
-
assert.ok(encoding(variants.get(540)), "and so is the second viewer's");
|
|
329
|
+
assert.ok(encoding(manager, variants.get(720)), "the first viewer's step is still encoding");
|
|
330
|
+
assert.ok(encoding(manager, variants.get(540)), "and so is the second viewer's");
|
|
299
331
|
|
|
300
332
|
// Now the first viewer steps down. Theirs is left for nobody and stops; the
|
|
301
333
|
// other viewer's is untouched.
|
|
302
|
-
await manager.resolveVariantFile(BASE_ID, 480, "segment-00005.mp4", FIRST);
|
|
334
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 480, "segment-00005.mp4", FIRST);
|
|
335
|
+
manager.renditions.viewerPlays(BASE_ID, FIRST, 480, 20);
|
|
303
336
|
|
|
304
|
-
assert.equal(watched(variants.get(720)), false, "the step nobody is on is nobody's");
|
|
305
|
-
assert.ok(watched(variants.get(540)), "the step the other viewer is watching stays theirs");
|
|
306
|
-
assert.ok(watched(variants.get(480)), "and the one they moved to is now theirs");
|
|
307
|
-
assert.ok(encoding(variants.get(540)), "and nothing here touched the other viewer's encoder");
|
|
337
|
+
assert.equal(watched(manager, variants.get(720)), false, "the step nobody is on is nobody's");
|
|
338
|
+
assert.ok(watched(manager, variants.get(540)), "the step the other viewer is watching stays theirs");
|
|
339
|
+
assert.ok(watched(manager, variants.get(480)), "and the one they moved to is now theirs");
|
|
340
|
+
assert.ok(encoding(manager, variants.get(540)), "and nothing here touched the other viewer's encoder");
|
|
308
341
|
});
|
|
309
342
|
|
|
310
343
|
test("a step somebody is watching is never withdrawn from the offer", async (t) => {
|
|
311
344
|
const { manager, base, dirPath } = await pictureWithTwoViewers();
|
|
312
345
|
t.after(async () => {
|
|
313
|
-
await manager.disposeAll();
|
|
346
|
+
await manager.lifecycle.disposeAll();
|
|
314
347
|
await rm(dirPath, { recursive: true, force: true });
|
|
315
348
|
});
|
|
316
349
|
// A host that can re-encode 240p and nothing above it — the shape of the
|
|
@@ -330,20 +363,20 @@ test("a step somebody is watching is never withdrawn from the offer", async (t)
|
|
|
330
363
|
// One file, two sessions of it.
|
|
331
364
|
variant.file = base.file;
|
|
332
365
|
variant.isStep = true;
|
|
366
|
+
manager.outputs.markStep(variant);
|
|
333
367
|
variant.file = base.file;
|
|
334
|
-
manager.
|
|
335
|
-
base.file.stepHeights.set(720, 720);
|
|
368
|
+
manager.outputs.set(variant.id, variant);
|
|
336
369
|
|
|
337
370
|
// Nobody on it: measured below realtime, it is withdrawn. This half is the
|
|
338
371
|
// control — without it the other half proves nothing.
|
|
339
|
-
const withoutAViewer = manager.offeredHeights(base);
|
|
372
|
+
const withoutAViewer = manager.quality.offeredHeights(base);
|
|
340
373
|
assert.ok(
|
|
341
374
|
!withoutAViewer.includes(720),
|
|
342
375
|
`a step nobody is on and that cannot keep up is withdrawn: ${withoutAViewer.join(" ")}`
|
|
343
376
|
);
|
|
344
377
|
|
|
345
|
-
|
|
346
|
-
const withAViewer = manager.offeredHeights(base);
|
|
378
|
+
manager.viewers.of(base, SECOND).activeVariantId = variant.id;
|
|
379
|
+
const withAViewer = manager.quality.offeredHeights(base);
|
|
347
380
|
|
|
348
381
|
assert.ok(
|
|
349
382
|
withAViewer.includes(720),
|
|
@@ -354,20 +387,20 @@ test("a step somebody is watching is never withdrawn from the offer", async (t)
|
|
|
354
387
|
test("a viewer whose picture has gone quiet holds no soundtrack encoder", async (t) => {
|
|
355
388
|
const { manager, base, renditions, dirPath } = await pictureWithTwoViewers();
|
|
356
389
|
t.after(async () => {
|
|
357
|
-
await manager.disposeAll();
|
|
390
|
+
await manager.lifecycle.disposeAll();
|
|
358
391
|
await rm(dirPath, { recursive: true, force: true });
|
|
359
392
|
});
|
|
360
|
-
await manager.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
361
|
-
await manager.resolveAudioRenditionFile(BASE_ID, 1, "segment-00003.mp4", SECOND);
|
|
393
|
+
await manager.renditions.resolveAudioRenditionFile(BASE_ID, 0, "segment-00003.mp4", FIRST);
|
|
394
|
+
await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00003.mp4", SECOND);
|
|
362
395
|
// The second viewer's tab is gone. Nothing releases the session when a
|
|
363
396
|
// channel closes (roadmap item 54), so what expires is their head on the
|
|
364
397
|
// picture — and with it their claim on an encoder.
|
|
365
|
-
|
|
398
|
+
manager.viewers.leaves(base, SECOND);
|
|
366
399
|
|
|
367
|
-
await manager.resolveAudioRenditionFile(BASE_ID, 1, "segment-00004.mp4", FIRST);
|
|
400
|
+
await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00004.mp4", FIRST);
|
|
368
401
|
|
|
369
402
|
assert.equal(
|
|
370
|
-
watched(renditions.get(audioRenditionKey(0, true))),
|
|
403
|
+
watched(manager, renditions.get(audioRenditionKey(0, true))),
|
|
371
404
|
false,
|
|
372
405
|
"the first viewer moved on, so their old track is nobody's"
|
|
373
406
|
);
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
|
-
import { decideUploadLimit, torrentsForUploadPolicy } from "../services/torrent-pool.js";
|
|
21
|
-
import { demandFor, forgetTorrent } from "../services/download/registry.js";
|
|
22
|
-
import { Urgency } from "../services/demand/index.js";
|
|
20
|
+
import { decideUploadLimit, torrentsForUploadPolicy } from "../services/torrent/torrent-pool.js";
|
|
21
|
+
import { demandFor, forgetTorrent } from "../services/torrent/download/registry.js";
|
|
22
|
+
import { Urgency } from "../services/torrent/demand/index.js";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* A torrent whose metadata has arrived and which nothing is wanted of.
|
|
@@ -11,7 +11,7 @@ import test from "node:test";
|
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
13
13
|
|
|
14
|
-
import { createUsrsctpStateReader, SCTPSTATE_SCRIPT_PATH } from "../services/usrsctp-state.js";
|
|
14
|
+
import { createUsrsctpStateReader, SCTPSTATE_SCRIPT_PATH } from "../services/transport/usrsctp-state.js";
|
|
15
15
|
|
|
16
16
|
class FakeChild extends EventEmitter {
|
|
17
17
|
constructor(command, args) {
|
package/test/video-facts.test.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import test from "node:test";
|
|
18
18
|
import assert from "node:assert/strict";
|
|
19
19
|
|
|
20
|
-
import { Container } from "../services/container/Container.js";
|
|
20
|
+
import { Container } from "../services/media/container/Container.js";
|
|
21
21
|
|
|
22
22
|
const banner = (fields) => ({ width: null, height: null, fps: null, isHdr: false, bitDepth: null, ...fields });
|
|
23
23
|
|