@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,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What a viewer's own assignment to an output must do.
|
|
3
|
+
*
|
|
4
|
+
* The property under all of it: one viewer's link, seek or choice never moves
|
|
5
|
+
* another viewer's output. That is roadmap item 97, and the record it replaces
|
|
6
|
+
* — one entry per (file, height) for everybody — could not express it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { Assignments } from "../services/viewer/Assignments.js";
|
|
12
|
+
|
|
13
|
+
const WINDOW_MS = 30_000;
|
|
14
|
+
const make = () => new Assignments({ acceptWindowMs: WINDOW_MS });
|
|
15
|
+
|
|
16
|
+
test("the acceptance window is given, not invented", () => {
|
|
17
|
+
assert.throws(() => new Assignments({}), TypeError);
|
|
18
|
+
assert.throws(() => new Assignments({ acceptWindowMs: -1 }), TypeError);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a choice stands until the rule moves it, and a request never moves it", () => {
|
|
22
|
+
const mine = make();
|
|
23
|
+
|
|
24
|
+
mine.choose(720, "out-a");
|
|
25
|
+
mine.give(mine.generation, 720, 4, "out-a");
|
|
26
|
+
|
|
27
|
+
assert.equal(mine.chosenFor(720), "out-a");
|
|
28
|
+
mine.give(mine.generation, 720, 5, "out-b");
|
|
29
|
+
assert.equal(mine.chosenFor(720), "out-a", "being given something else does not re-choose");
|
|
30
|
+
mine.choose(720, "out-b");
|
|
31
|
+
assert.equal(mine.chosenFor(720), "out-b");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("two viewers do not share an answer", () => {
|
|
35
|
+
const thin = make();
|
|
36
|
+
const thick = make();
|
|
37
|
+
|
|
38
|
+
thin.choose(720, "out-540-slow");
|
|
39
|
+
thick.choose(720, "out-720-full");
|
|
40
|
+
|
|
41
|
+
assert.equal(thin.chosenFor(720), "out-540-slow");
|
|
42
|
+
assert.equal(thick.chosenFor(720), "out-720-full", "the other viewer's link decided nothing here");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a repeat of a request is answered by whatever answered it, not decided again", () => {
|
|
46
|
+
const mine = make();
|
|
47
|
+
const first = mine.generation;
|
|
48
|
+
|
|
49
|
+
mine.give(first, 720, 9, "out-a");
|
|
50
|
+
|
|
51
|
+
assert.equal(mine.givenFor(first, 720, 9), "out-a");
|
|
52
|
+
assert.equal(mine.givenFor(first, 720, 10), "", "a segment nothing answered says so");
|
|
53
|
+
assert.equal(mine.givenFor(first + 1, 720, 9), "", "and a later generation is not the same question");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a request carries its own generation, so a late one is not read against the current", () => {
|
|
57
|
+
const mine = make();
|
|
58
|
+
const made = mine.generation;
|
|
59
|
+
|
|
60
|
+
mine.statedGeneration(1, 1_000);
|
|
61
|
+
|
|
62
|
+
assert.equal(mine.generation, 1);
|
|
63
|
+
assert.notEqual(mine.generation, made);
|
|
64
|
+
assert.equal(mine.accepts(made, 1_000 + WINDOW_MS), true, "inside the window it is still answered");
|
|
65
|
+
assert.equal(mine.accepts(made, 1_000 + WINDOW_MS + 1), false, "past it, a new request is not");
|
|
66
|
+
assert.equal(mine.accepts(mine.generation, 1_000 + 10 * WINDOW_MS), true, "the current one never runs out");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an accepted response keeps its output past its own generation's window", () => {
|
|
70
|
+
const mine = make();
|
|
71
|
+
const made = mine.generation;
|
|
72
|
+
mine.give(made, 720, 3, "out-a");
|
|
73
|
+
const token = mine.accept("out-a");
|
|
74
|
+
|
|
75
|
+
mine.statedGeneration(1, 1_000);
|
|
76
|
+
mine.choose(720, "out-b");
|
|
77
|
+
|
|
78
|
+
const longAfter = 1_000 + 10 * WINDOW_MS;
|
|
79
|
+
assert.equal(mine.accepts(made, longAfter), false, "no NEW request of that generation is taken");
|
|
80
|
+
assert.ok(
|
|
81
|
+
mine.heldKeys(longAfter).has("out-a"),
|
|
82
|
+
"but the output whose bytes are still going out is held — dropping it is what the second deadline exists to prevent"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
mine.release(token);
|
|
86
|
+
assert.equal(mine.heldKeys(longAfter).has("out-a"), false, "and released once the response is done");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("what was given inside a live generation holds its output, and is forgotten past the window", () => {
|
|
90
|
+
const mine = make();
|
|
91
|
+
const made = mine.generation;
|
|
92
|
+
mine.give(made, 720, 3, "out-a");
|
|
93
|
+
mine.statedGeneration(1, 1_000);
|
|
94
|
+
|
|
95
|
+
assert.ok(mine.heldKeys(1_000 + WINDOW_MS).has("out-a"));
|
|
96
|
+
assert.equal(
|
|
97
|
+
mine.heldKeys(1_000 + WINDOW_MS + 1).has("out-a"),
|
|
98
|
+
false,
|
|
99
|
+
"past the window nothing stands on it, so it may be let go"
|
|
100
|
+
);
|
|
101
|
+
assert.equal(
|
|
102
|
+
mine.givenFor(made, 720, 3),
|
|
103
|
+
"",
|
|
104
|
+
"and the generation is pruned rather than kept for the life of the viewing"
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a viewer leaving holds nothing, response in flight included", () => {
|
|
109
|
+
const mine = make();
|
|
110
|
+
mine.choose(720, "out-a");
|
|
111
|
+
mine.accept("out-b");
|
|
112
|
+
mine.give(mine.generation, 720, 1, "out-c");
|
|
113
|
+
|
|
114
|
+
mine.clear();
|
|
115
|
+
|
|
116
|
+
assert.equal(mine.heldKeys().size, 0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("the page owns the number, so a lower one does not take the viewer back", () => {
|
|
120
|
+
const mine = make();
|
|
121
|
+
mine.statedGeneration(4, 1_000);
|
|
122
|
+
|
|
123
|
+
assert.equal(mine.statedGeneration(2, 2_000), 4, "a reordered or repeated message changes nothing");
|
|
124
|
+
assert.equal(mine.statedGeneration(4, 2_000), 4, "and neither does the one already current");
|
|
125
|
+
assert.equal(mine.generation, 4);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a request ahead of what the page has announced is still answered", () => {
|
|
129
|
+
const mine = make();
|
|
130
|
+
|
|
131
|
+
// The page raises its own number before it sends anything, so a segment
|
|
132
|
+
// request can overtake the seek that announces the new viewing.
|
|
133
|
+
assert.equal(mine.accepts(1), true, "being behind the page means this side has not heard yet");
|
|
134
|
+
assert.equal(mine.accepts(7), true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a request that states no generation is answered", () => {
|
|
138
|
+
const mine = make();
|
|
139
|
+
mine.statedGeneration(3, 1_000);
|
|
140
|
+
|
|
141
|
+
assert.equal(mine.accepts(Number.NaN, 1_000 + 10 * WINDOW_MS), true);
|
|
142
|
+
assert.equal(mine.accepts(undefined, 1_000 + 10 * WINDOW_MS), true, "a transport with no loader of ours builds no such parameter");
|
|
143
|
+
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file What a viewer is watching, and what happens to it when they leave.
|
|
3
3
|
*
|
|
4
|
-
* The relation "this person watches this output" is
|
|
5
|
-
*
|
|
6
|
-
* hold the three things that go wrong when the two indexes come apart.
|
|
4
|
+
* The relation "this person watches this output" is stored on the viewer, and
|
|
5
|
+
* these checks hold the operations that derive the opposite direction.
|
|
7
6
|
*
|
|
8
7
|
* 1. The viewer must never be dropped from the PICTURE. Their chosen
|
|
9
8
|
* soundtrack, their position and their step are recorded there, and the
|
|
@@ -20,22 +19,24 @@
|
|
|
20
19
|
* leaves a claim that nothing can reach.
|
|
21
20
|
*/
|
|
22
21
|
|
|
22
|
+
import { chooseOutput } from "../services/viewer/choices.js";
|
|
23
23
|
import test from "node:test";
|
|
24
24
|
import assert from "node:assert/strict";
|
|
25
25
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
26
26
|
import os from "node:os";
|
|
27
27
|
import path from "node:path";
|
|
28
|
-
import {
|
|
28
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
29
29
|
import { Viewers } from "../services/viewer/Viewers.js";
|
|
30
|
-
import { SourceFile } from "../services/
|
|
31
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
32
|
-
import { Output } from "../services/output/Output.js";
|
|
33
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
30
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
31
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
32
|
+
import { Output } from "../services/encode/output/Output.js";
|
|
33
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
34
34
|
import { fakeProcess as fakeEncoder, startRunOn } from "./helpers/encode-run.js";
|
|
35
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
35
36
|
|
|
36
|
-
const BASE_ID = "
|
|
37
|
-
const STEP_ID = "
|
|
38
|
-
const AUDIO_ID = "
|
|
37
|
+
const BASE_ID = "aaaaaaaabbbbcccc";
|
|
38
|
+
const STEP_ID = "1111111122223333";
|
|
39
|
+
const AUDIO_ID = "9999999988887777";
|
|
39
40
|
const VIEWER = "viewer-one";
|
|
40
41
|
const SEGMENT_SECONDS = 4;
|
|
41
42
|
|
|
@@ -49,6 +50,13 @@ const SEGMENT_SECONDS = 4;
|
|
|
49
50
|
function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, isStep = false }) {
|
|
50
51
|
return {
|
|
51
52
|
id,
|
|
53
|
+
spec: outputSpec({
|
|
54
|
+
sourceKey: file.sourceKey,
|
|
55
|
+
fileIndex: file.fileIndex,
|
|
56
|
+
transcodeVideo: !audioOnly && encodeHeight > 0,
|
|
57
|
+
audioOnly,
|
|
58
|
+
height: encodeHeight
|
|
59
|
+
}),
|
|
52
60
|
dirPath,
|
|
53
61
|
timeline: new Timeline({
|
|
54
62
|
boundaries: Array.from({ length: 101 }, (_, index) => index * SEGMENT_SECONDS),
|
|
@@ -59,11 +67,9 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
|
|
|
59
67
|
get inputFile() { return this.file; },
|
|
60
68
|
get audioFile() { return this.file; },
|
|
61
69
|
startedAt: Date.now(),
|
|
62
|
-
createEntryMs: Date.now(),
|
|
63
70
|
lastAccessedAt: Date.now(),
|
|
64
71
|
ffmpeg: null,
|
|
65
72
|
lastError: "",
|
|
66
|
-
consumers: new Set(),
|
|
67
73
|
segmentFormat: fmp4Format,
|
|
68
74
|
transcodeVideo: !audioOnly,
|
|
69
75
|
transcodeAudio: true,
|
|
@@ -88,7 +94,6 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
|
|
|
88
94
|
seekTarget: null,
|
|
89
95
|
waitEpoch: 0,
|
|
90
96
|
runs: new Set(),
|
|
91
|
-
viewers: new Map(),
|
|
92
97
|
usesExplicitCuts: false,
|
|
93
98
|
useSyntheticPlaylist: true,
|
|
94
99
|
playlistText: "#EXTM3U\n",
|
|
@@ -102,12 +107,12 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
|
|
|
102
107
|
* of one file, and one viewer watching all three — which is what an ordinary
|
|
103
108
|
* session looks like once the viewer has picked a language and a quality.
|
|
104
109
|
*
|
|
105
|
-
* @returns {Promise<{ manager:
|
|
110
|
+
* @returns {Promise<{ manager: object, base: object, step: object,
|
|
106
111
|
* audio: object, dirPath: string, released: string[] }>}
|
|
107
112
|
*/
|
|
108
113
|
async function pictureWithStepAndSoundtrack() {
|
|
109
114
|
const dirPath = await mkdtemp(path.join(os.tmpdir(), "viewer-outputs-"));
|
|
110
|
-
const manager =
|
|
115
|
+
const manager = wireOutputs({
|
|
111
116
|
enabled: true,
|
|
112
117
|
ffmpegBin: "ffmpeg",
|
|
113
118
|
localBindHost: "127.0.0.1",
|
|
@@ -118,15 +123,12 @@ async function pictureWithStepAndSoundtrack() {
|
|
|
118
123
|
const base = fakeSession({ id: BASE_ID, dirPath, file, encodeHeight: 812 });
|
|
119
124
|
const step = fakeSession({ id: STEP_ID, dirPath, file, encodeHeight: 540, isStep: true });
|
|
120
125
|
const audio = fakeSession({ id: AUDIO_ID, dirPath, file, audioOnly: true });
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
// browser never learns these two ids.
|
|
124
|
-
step.consumers = new Set([variantConsumerId(BASE_ID)]);
|
|
125
|
-
audio.consumers = new Set([variantConsumerId(BASE_ID)]);
|
|
126
|
+
// The browser never learns these two ids: what keeps them is who watches
|
|
127
|
+
// them, and nothing else.
|
|
126
128
|
for (const session of [base, step, audio]) {
|
|
127
|
-
manager.
|
|
129
|
+
manager.outputs.set(session.id, session);
|
|
128
130
|
}
|
|
129
|
-
|
|
131
|
+
manager.outputs.markStep(step);
|
|
130
132
|
return { manager, base, step, audio, dirPath };
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -137,12 +139,17 @@ test("a viewer who steps down, back to the picture's own height and down again k
|
|
|
137
139
|
const viewer = manager.viewers.of(base, VIEWER);
|
|
138
140
|
viewer.audio = { trackIndex: 1, transcode: true };
|
|
139
141
|
viewer.position = { segment: 25, seconds: 100, at: Date.now() };
|
|
142
|
+
// The rule chose the step for them at 540p; the record is theirs.
|
|
143
|
+
chooseOutput(manager.viewers, VIEWER, 540, manager.outputs.get(STEP_ID).outputKey);
|
|
140
144
|
|
|
141
|
-
await manager.resolveVariantFile(BASE_ID, 540, "segment-00025.mp4", VIEWER);
|
|
142
|
-
|
|
143
|
-
await manager.resolveVariantFile(BASE_ID,
|
|
145
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00025.mp4", VIEWER);
|
|
146
|
+
manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 100);
|
|
147
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00026.mp4", VIEWER);
|
|
148
|
+
manager.renditions.viewerPlays(BASE_ID, VIEWER, 812, 104);
|
|
149
|
+
await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00027.mp4", VIEWER);
|
|
150
|
+
manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 108);
|
|
144
151
|
|
|
145
|
-
const known =
|
|
152
|
+
const known = manager.viewers.forOutput(base).get(VIEWER);
|
|
146
153
|
assert.ok(known, "the picture is the one id the browser holds — a viewer is never dropped from it");
|
|
147
154
|
assert.deepEqual(
|
|
148
155
|
known.audio,
|
|
@@ -166,16 +173,16 @@ test("a viewer leaving is subtracted from every output, and one nobody is left w
|
|
|
166
173
|
"one viewer, one object, and it knows all three outputs it is watching"
|
|
167
174
|
);
|
|
168
175
|
|
|
169
|
-
await manager.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
|
|
176
|
+
await manager.lifecycle.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
|
|
170
177
|
|
|
171
|
-
assert.equal(manager.
|
|
178
|
+
assert.equal(manager.outputs.has(BASE_ID), false, "the picture goes with its last consumer");
|
|
172
179
|
assert.equal(
|
|
173
|
-
manager.
|
|
180
|
+
manager.outputs.has(STEP_ID),
|
|
174
181
|
false,
|
|
175
182
|
"and so does the quality step: nobody is watching it, and no one outside this class knows its id"
|
|
176
183
|
);
|
|
177
184
|
assert.equal(
|
|
178
|
-
manager.
|
|
185
|
+
manager.outputs.has(AUDIO_ID),
|
|
179
186
|
false,
|
|
180
187
|
"and the soundtrack, for the same reason — it used to sit for half an hour holding an encoder, a directory and a claim on the torrent"
|
|
181
188
|
);
|
|
@@ -186,7 +193,6 @@ test("an output somebody else is still watching is kept when one viewer leaves",
|
|
|
186
193
|
const { manager, base, step, audio, dirPath } = await pictureWithStepAndSoundtrack();
|
|
187
194
|
t.after(() => rm(dirPath, { recursive: true, force: true }));
|
|
188
195
|
const second = "viewer-two";
|
|
189
|
-
base.consumers.add(second);
|
|
190
196
|
manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
|
|
191
197
|
manager.viewers.of(step, VIEWER);
|
|
192
198
|
manager.viewers.of(audio, VIEWER);
|
|
@@ -195,25 +201,25 @@ test("an output somebody else is still watching is kept when one viewer leaves",
|
|
|
195
201
|
manager.viewers.of(base, second).position = { segment: 25, seconds: 100, at: Date.now() };
|
|
196
202
|
manager.viewers.of(audio, second);
|
|
197
203
|
|
|
198
|
-
await manager.releaseSessionConsumer(BASE_ID, VIEWER, "the first viewer left");
|
|
204
|
+
await manager.lifecycle.releaseSessionConsumer(BASE_ID, VIEWER, "the first viewer left");
|
|
199
205
|
|
|
200
|
-
assert.equal(manager.
|
|
206
|
+
assert.equal(manager.outputs.has(BASE_ID), true, "the picture stays: it still has a consumer");
|
|
201
207
|
assert.equal(
|
|
202
|
-
manager.
|
|
208
|
+
manager.outputs.has(AUDIO_ID),
|
|
203
209
|
true,
|
|
204
210
|
"and the soundtrack stays, because having no listeners is what kills it and it has one"
|
|
205
211
|
);
|
|
206
212
|
assert.equal(
|
|
207
|
-
|
|
213
|
+
manager.viewers.forOutput(base).has(VIEWER),
|
|
208
214
|
false,
|
|
209
215
|
"the viewer who left is gone from the picture"
|
|
210
216
|
);
|
|
211
217
|
assert.equal(
|
|
212
|
-
|
|
218
|
+
manager.viewers.forOutput(audio).has(VIEWER),
|
|
213
219
|
false,
|
|
214
220
|
"and from the soundtrack, which is the half of the relation the viewer holds"
|
|
215
221
|
);
|
|
216
|
-
assert.equal(
|
|
222
|
+
assert.equal(manager.viewers.forOutput(step).size, 0, "the step they had is watched by nobody");
|
|
217
223
|
});
|
|
218
224
|
|
|
219
225
|
test("nothing is left wanting production once the last viewer has left", async (t) => {
|
|
@@ -223,7 +229,7 @@ test("nothing is left wanting production once the last viewer has left", async (
|
|
|
223
229
|
manager.viewers.of(step, VIEWER);
|
|
224
230
|
manager.viewers.of(audio, VIEWER);
|
|
225
231
|
|
|
226
|
-
await manager.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
|
|
232
|
+
await manager.lifecycle.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
|
|
227
233
|
|
|
228
234
|
// NOTHING HAS TO BE RELEASED, and that is the point of the shape. A claim per
|
|
229
235
|
// viewer per output used to be held in the encoding layer and taken back one
|
|
@@ -232,20 +238,20 @@ test("nothing is left wanting production once the last viewer has left", async (
|
|
|
232
238
|
// with nobody's name in it, rebuilt from whoever is watching; a viewer who has
|
|
233
239
|
// left is simply not in the next one.
|
|
234
240
|
for (const id of [BASE_ID, STEP_ID, AUDIO_ID]) {
|
|
235
|
-
const session = manager.
|
|
241
|
+
const session = manager.outputs.get(id);
|
|
236
242
|
assert.equal(session, undefined, `${id.slice(0, 8)} is let go with its last viewer`);
|
|
237
243
|
}
|
|
238
|
-
manager.planEncodersNow();
|
|
244
|
+
manager.encodeRuns.planEncodersNow();
|
|
239
245
|
for (const address of [base.outputKey, step.outputKey, audio.outputKey]) {
|
|
240
246
|
assert.deepEqual(
|
|
241
|
-
manager.encodeOrchestrator.
|
|
247
|
+
manager.encodeOrchestrator.wantedSegmentsOn(address),
|
|
242
248
|
[],
|
|
243
249
|
"and no output is left asking for an encoder"
|
|
244
250
|
);
|
|
245
251
|
}
|
|
246
252
|
});
|
|
247
253
|
|
|
248
|
-
test("the
|
|
254
|
+
test("the reverse view is derived from the relation stored on the viewer", () => {
|
|
249
255
|
const viewers = new Viewers();
|
|
250
256
|
const picture = { id: "picture" };
|
|
251
257
|
const soundtrack = { id: "soundtrack" };
|
|
@@ -254,24 +260,26 @@ test("the two indexes of one relation are written together", () => {
|
|
|
254
260
|
viewers.of(soundtrack, VIEWER);
|
|
255
261
|
assert.equal(viewers.of(soundtrack, VIEWER), viewer, "one person is one object, whatever they are watching");
|
|
256
262
|
assert.deepEqual([...viewer.outputs].sort(), ["picture", "soundtrack"]);
|
|
257
|
-
assert.equal(picture.
|
|
263
|
+
assert.equal(viewers.forOutput(picture).get(VIEWER), viewer);
|
|
258
264
|
|
|
259
265
|
viewers.leaves(soundtrack, VIEWER);
|
|
260
|
-
assert.deepEqual([...viewer.outputs], ["picture"], "
|
|
261
|
-
assert.equal(soundtrack.
|
|
266
|
+
assert.deepEqual([...viewer.outputs], ["picture"], "the stored relation changes once");
|
|
267
|
+
assert.equal(viewers.forOutput(soundtrack).has(VIEWER), false);
|
|
262
268
|
assert.equal(viewers.size, 1, "and the person is still known, because they are still watching something");
|
|
263
269
|
|
|
264
270
|
viewers.leaves(picture, VIEWER);
|
|
265
271
|
assert.equal(viewers.size, 0, "watching nothing, they are forgotten");
|
|
266
272
|
});
|
|
267
273
|
|
|
268
|
-
test("a
|
|
274
|
+
test("a request that names nobody makes no viewer", () => {
|
|
275
|
+
// A nameless viewer used to belong to whichever output met it: the same
|
|
276
|
+
// person was a different object on each output, and two nameless people on
|
|
277
|
+
// one output were one object. Every viewer has a name now.
|
|
269
278
|
const viewers = new Viewers();
|
|
270
279
|
const oneFilm = { id: "one" };
|
|
271
|
-
const another = { id: "another" };
|
|
272
280
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
assert.
|
|
276
|
-
assert.equal(viewers.
|
|
281
|
+
assert.throws(() => viewers.of(oneFilm, ""), TypeError);
|
|
282
|
+
assert.equal(viewers.size, 0, "nothing was registered");
|
|
283
|
+
assert.equal(viewers.forOutput(oneFilm).size, 0, "and nobody watches the output");
|
|
284
|
+
assert.equal(viewers.getForOutput(oneFilm, ""), null);
|
|
277
285
|
});
|
|
@@ -17,12 +17,13 @@ import assert from "node:assert/strict";
|
|
|
17
17
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import path from "node:path";
|
|
20
|
-
import {
|
|
21
|
-
import { SourceFile } from "../services/
|
|
20
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
21
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
22
22
|
import { Viewers } from "../services/viewer/Viewers.js";
|
|
23
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
23
24
|
|
|
24
|
-
const PICTURE = "
|
|
25
|
-
const SOUND = "
|
|
25
|
+
const PICTURE = "aaaaaaaa00004000";
|
|
26
|
+
const SOUND = "aaaaaaaa00004001";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* @param {string} id
|
|
@@ -32,13 +33,13 @@ const SOUND = "aaaaaaaa-0000-4000-8000-000000000002";
|
|
|
32
33
|
function outputOn(id, dirPath) {
|
|
33
34
|
return {
|
|
34
35
|
id,
|
|
36
|
+
spec: outputSpec(),
|
|
35
37
|
dirPath,
|
|
36
38
|
state: "live",
|
|
37
39
|
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
|
|
38
40
|
get inputFile() { return this.file; },
|
|
39
41
|
get audioFile() { return this.file; },
|
|
40
|
-
|
|
41
|
-
viewers: new Map(),
|
|
42
|
+
claims: new Set(),
|
|
42
43
|
lastAccessedAt: Date.now()
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -46,7 +47,7 @@ function outputOn(id, dirPath) {
|
|
|
46
47
|
test("asking for a viewer of an output is that viewer watching it", () => {
|
|
47
48
|
let changes = 0;
|
|
48
49
|
const viewers = new Viewers({ onChange: () => { changes += 1; } });
|
|
49
|
-
const output = { id: "out-1"
|
|
50
|
+
const output = { id: "out-1" };
|
|
50
51
|
|
|
51
52
|
const first = viewers.of(output, "someone");
|
|
52
53
|
assert.equal(changes, 1, "a new relation is a change");
|
|
@@ -55,7 +56,7 @@ test("asking for a viewer of an output is that viewer watching it", () => {
|
|
|
55
56
|
viewers.of(output, "someone");
|
|
56
57
|
assert.equal(changes, 1, "asking again about the same output changes nothing");
|
|
57
58
|
|
|
58
|
-
const second = { id: "out-2"
|
|
59
|
+
const second = { id: "out-2" };
|
|
59
60
|
viewers.of(second, "someone");
|
|
60
61
|
assert.equal(changes, 2, "a second output is a change");
|
|
61
62
|
assert.equal(viewers.get("someone")?.outputs.size, 2);
|
|
@@ -63,20 +64,18 @@ test("asking for a viewer of an output is that viewer watching it", () => {
|
|
|
63
64
|
|
|
64
65
|
test("a connection closing takes the person off every output at once", () => {
|
|
65
66
|
const viewers = new Viewers();
|
|
66
|
-
const picture = { id: "picture"
|
|
67
|
-
const sound = { id: "sound"
|
|
68
|
-
const byId = new Map([["picture", picture], ["sound", sound]]);
|
|
69
|
-
|
|
67
|
+
const picture = { id: "picture" };
|
|
68
|
+
const sound = { id: "sound" };
|
|
70
69
|
viewers.of(picture, "watcher");
|
|
71
70
|
viewers.of(sound, "watcher");
|
|
72
71
|
viewers.of(picture, "other");
|
|
73
72
|
|
|
74
|
-
const left = viewers.hasGone("watcher"
|
|
73
|
+
const left = viewers.hasGone("watcher");
|
|
75
74
|
|
|
76
75
|
assert.deepEqual(left.sort(), ["picture", "sound"], "both, not the one the browser holds an id for");
|
|
77
|
-
assert.equal(picture.
|
|
78
|
-
assert.equal(sound.
|
|
79
|
-
assert.equal(picture.
|
|
76
|
+
assert.equal(viewers.forOutput(picture).has("watcher"), false);
|
|
77
|
+
assert.equal(viewers.forOutput(sound).has("watcher"), false);
|
|
78
|
+
assert.equal(viewers.forOutput(picture).has("other"), true, "and nobody else goes with them");
|
|
80
79
|
assert.equal(viewers.get("watcher"), null, "the registry does not keep what it has let go");
|
|
81
80
|
});
|
|
82
81
|
|
|
@@ -85,7 +84,7 @@ test("a viewer whose connection closed is let go of every output they were watch
|
|
|
85
84
|
t.after(async () => {
|
|
86
85
|
await rm(dirPath, { recursive: true, force: true });
|
|
87
86
|
});
|
|
88
|
-
const manager =
|
|
87
|
+
const manager = wireOutputs({
|
|
89
88
|
enabled: true,
|
|
90
89
|
ffmpegBin: "ffmpeg",
|
|
91
90
|
localBindHost: "127.0.0.1",
|
|
@@ -94,26 +93,24 @@ test("a viewer whose connection closed is let go of every output they were watch
|
|
|
94
93
|
|
|
95
94
|
const picture = outputOn(PICTURE, dirPath);
|
|
96
95
|
const sound = outputOn(SOUND, dirPath);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
manager.sessionsById.set(PICTURE, picture);
|
|
100
|
-
manager.sessionsById.set(SOUND, sound);
|
|
96
|
+
manager.outputs.set(PICTURE, picture);
|
|
97
|
+
manager.outputs.set(SOUND, sound);
|
|
101
98
|
manager.viewers.of(picture, "watcher");
|
|
102
99
|
manager.viewers.of(sound, "watcher");
|
|
103
100
|
|
|
104
|
-
const released = await manager.viewerHasGone("watcher", "the connection closed");
|
|
101
|
+
const released = await manager.lifecycle.viewerHasGone("watcher", "the connection closed");
|
|
105
102
|
|
|
106
103
|
assert.equal(released, 2, "the picture and the soundtrack, from one statement");
|
|
107
104
|
assert.equal(manager.viewers.get("watcher"), null);
|
|
108
105
|
});
|
|
109
106
|
|
|
110
107
|
test("a viewer nobody knows costs nothing to let go of", async () => {
|
|
111
|
-
const manager =
|
|
108
|
+
const manager = wireOutputs({
|
|
112
109
|
enabled: true,
|
|
113
110
|
ffmpegBin: "ffmpeg",
|
|
114
111
|
localBindHost: "127.0.0.1",
|
|
115
112
|
localPort: 9090
|
|
116
113
|
});
|
|
117
|
-
assert.equal(await manager.viewerHasGone("never-seen"), 0);
|
|
118
|
-
assert.equal(await manager.viewerHasGone(""), 0);
|
|
114
|
+
assert.equal(await manager.lifecycle.viewerHasGone("never-seen"), 0);
|
|
115
|
+
assert.equal(await manager.lifecycle.viewerHasGone(""), 0);
|
|
119
116
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Opening an output admits and places its viewer in one synchronous call.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
|
|
8
|
+
import { ViewerRequests } from "../services/server/ViewerRequests.js";
|
|
9
|
+
|
|
10
|
+
test("the first opening holds capacity before a concurrent opening is admitted", async () => {
|
|
11
|
+
const records = new Map();
|
|
12
|
+
const outputCosts = new Map([
|
|
13
|
+
["source-a:0", 0.6],
|
|
14
|
+
["source-b:0", 0.6]
|
|
15
|
+
]);
|
|
16
|
+
const viewers = {
|
|
17
|
+
get: () => null,
|
|
18
|
+
forOutput(output) {
|
|
19
|
+
return new Set(records.get(output.outputKey)?.keys() ?? []);
|
|
20
|
+
},
|
|
21
|
+
of(output, consumerId) {
|
|
22
|
+
let viewersOnOutput = records.get(output.outputKey);
|
|
23
|
+
if (!viewersOnOutput) {
|
|
24
|
+
viewersOnOutput = new Map();
|
|
25
|
+
records.set(output.outputKey, viewersOnOutput);
|
|
26
|
+
}
|
|
27
|
+
let viewer = viewersOnOutput.get(consumerId);
|
|
28
|
+
if (!viewer) {
|
|
29
|
+
viewer = {
|
|
30
|
+
position: null,
|
|
31
|
+
linkReading: () => null,
|
|
32
|
+
noteVisiblePicture: () => {}
|
|
33
|
+
};
|
|
34
|
+
viewersOnOutput.set(consumerId, viewer);
|
|
35
|
+
}
|
|
36
|
+
return viewer;
|
|
37
|
+
},
|
|
38
|
+
watchedAddresses() {
|
|
39
|
+
return new Set(
|
|
40
|
+
[...records]
|
|
41
|
+
.filter(([, members]) => [...members.values()].some((viewer) => viewer.position !== null))
|
|
42
|
+
.map(([address]) => address)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const admission = new EncodeAdmission({
|
|
47
|
+
liveRunsByAddress: () => new Map(),
|
|
48
|
+
preparedAddresses: () => new Set(),
|
|
49
|
+
watchedAddresses: () => viewers.watchedAddresses(),
|
|
50
|
+
loadOf: (address) => {
|
|
51
|
+
const costSec = outputCosts.get(address);
|
|
52
|
+
return costSec === undefined ? null : { costSec, fileKey: address, fileCostSec: 0 };
|
|
53
|
+
},
|
|
54
|
+
availability: () => ({ share: 1, known: true }),
|
|
55
|
+
finished: () => false
|
|
56
|
+
});
|
|
57
|
+
const opening = {
|
|
58
|
+
async open({ sourceKey, fileIndex, claim }) {
|
|
59
|
+
const output = { id: `${sourceKey}:${fileIndex}`, outputKey: `${sourceKey}:${fileIndex}` };
|
|
60
|
+
const answer = claim(output);
|
|
61
|
+
if (!answer.admitted) {
|
|
62
|
+
const error = new Error(answer.reason);
|
|
63
|
+
error.code = "OUTPUT_NO_CAPACITY";
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return { output, existed: false, audio: null, verdict: null };
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const requests = new ViewerRequests({
|
|
70
|
+
opening,
|
|
71
|
+
viewers,
|
|
72
|
+
admitsWatching: (output) => admission.admitsWatching(output.outputKey),
|
|
73
|
+
outputTimes: { segmentIndexForTime: (_output, seconds) => Math.floor(seconds / 4) },
|
|
74
|
+
planEncodersSoon: () => {},
|
|
75
|
+
outputs: { touch: () => {} },
|
|
76
|
+
noteServingVerdict: () => {},
|
|
77
|
+
waitUntilReady: async () => {}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const first = requests.createOrGetSession({ sourceKey: "source-a", fileIndex: 0, consumerId: "viewer-a" });
|
|
81
|
+
const second = requests.createOrGetSession({ sourceKey: "source-b", fileIndex: 0, consumerId: "viewer-b" });
|
|
82
|
+
const [firstResult, secondResult] = await Promise.allSettled([first, second]);
|
|
83
|
+
|
|
84
|
+
assert.equal(firstResult.status, "fulfilled");
|
|
85
|
+
assert.equal(secondResult.status, "rejected");
|
|
86
|
+
assert.match(secondResult.reason.message, /would make 0\.83x/);
|
|
87
|
+
assert.equal(viewers.forOutput({ outputKey: "source-a:0" }).has("viewer-a"), true);
|
|
88
|
+
assert.equal(viewers.forOutput({ outputKey: "source-b:0" }).has("viewer-b"), false);
|
|
89
|
+
});
|