@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
|
@@ -8,8 +8,8 @@ import assert from "node:assert/strict";
|
|
|
8
8
|
import { mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import { directoryNameFor, SegmentStore } from "../services/
|
|
12
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
11
|
+
import { directoryNameFor, SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
12
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @returns {{ store: SegmentStore, root: string, lines: string[] }}
|
|
@@ -37,7 +37,14 @@ const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
|
37
37
|
|
|
38
38
|
test("two viewers of one output are given the same directory", (t) => {
|
|
39
39
|
const { store, root } = storeInATempRoot();
|
|
40
|
-
t.after(() =>
|
|
40
|
+
t.after(() => {
|
|
41
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
42
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
43
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
44
|
+
// open watch cannot be removed on Windows.
|
|
45
|
+
store.dropAll("the check is over");
|
|
46
|
+
rmSync(root, { recursive: true, force: true });
|
|
47
|
+
});
|
|
41
48
|
|
|
42
49
|
// Which viewer asked never enters it — the address is what the segments are.
|
|
43
50
|
assert.equal(store.directoryFor(KEY), store.directoryFor(KEY));
|
|
@@ -46,7 +53,14 @@ test("two viewers of one output are given the same directory", (t) => {
|
|
|
46
53
|
|
|
47
54
|
test("the directory says what it holds, so a later process can tell", (t) => {
|
|
48
55
|
const { store, root } = storeInATempRoot();
|
|
49
|
-
t.after(() =>
|
|
56
|
+
t.after(() => {
|
|
57
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
58
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
59
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
60
|
+
// open watch cannot be removed on Windows.
|
|
61
|
+
store.dropAll("the check is over");
|
|
62
|
+
rmSync(root, { recursive: true, force: true });
|
|
63
|
+
});
|
|
50
64
|
|
|
51
65
|
const dir = store.directoryFor(KEY);
|
|
52
66
|
const names = readdirSync(dir);
|
|
@@ -55,7 +69,14 @@ test("the directory says what it holds, so a later process can tell", (t) => {
|
|
|
55
69
|
|
|
56
70
|
test("a piece under its served name is finished, and the last one too", (t) => {
|
|
57
71
|
const { store, root } = storeInATempRoot();
|
|
58
|
-
t.after(() =>
|
|
72
|
+
t.after(() => {
|
|
73
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
74
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
75
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
76
|
+
// open watch cannot be removed on Windows.
|
|
77
|
+
store.dropAll("the check is over");
|
|
78
|
+
rmSync(root, { recursive: true, force: true });
|
|
79
|
+
});
|
|
59
80
|
|
|
60
81
|
const dir = store.directoryFor(KEY);
|
|
61
82
|
store.useFormat(KEY, fmp4Format);
|
|
@@ -72,7 +93,14 @@ test("a piece under its served name is finished, and the last one too", (t) => {
|
|
|
72
93
|
|
|
73
94
|
test("a piece still being written is not a piece, and its name says so", (t) => {
|
|
74
95
|
const { store, root } = storeInATempRoot();
|
|
75
|
-
t.after(() =>
|
|
96
|
+
t.after(() => {
|
|
97
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
98
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
99
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
100
|
+
// open watch cannot be removed on Windows.
|
|
101
|
+
store.dropAll("the check is over");
|
|
102
|
+
rmSync(root, { recursive: true, force: true });
|
|
103
|
+
});
|
|
76
104
|
|
|
77
105
|
const dir = store.directoryFor(KEY);
|
|
78
106
|
store.useFormat(KEY, fmp4Format);
|
|
@@ -91,7 +119,14 @@ test("a piece still being written is not a piece, and its name says so", (t) =>
|
|
|
91
119
|
|
|
92
120
|
test("a file of no bytes is not a segment, whatever it is called", (t) => {
|
|
93
121
|
const { store, root } = storeInATempRoot();
|
|
94
|
-
t.after(() =>
|
|
122
|
+
t.after(() => {
|
|
123
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
124
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
125
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
126
|
+
// open watch cannot be removed on Windows.
|
|
127
|
+
store.dropAll("the check is over");
|
|
128
|
+
rmSync(root, { recursive: true, force: true });
|
|
129
|
+
});
|
|
95
130
|
|
|
96
131
|
const dir = store.directoryFor(KEY);
|
|
97
132
|
store.useFormat(KEY, fmp4Format);
|
|
@@ -109,7 +144,14 @@ test("a file of no bytes is not a segment, whatever it is called", (t) => {
|
|
|
109
144
|
|
|
110
145
|
test("clearing up after one run leaves every other run's work alone", (t) => {
|
|
111
146
|
const { store, root } = storeInATempRoot();
|
|
112
|
-
t.after(() =>
|
|
147
|
+
t.after(() => {
|
|
148
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
149
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
150
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
151
|
+
// open watch cannot be removed on Windows.
|
|
152
|
+
store.dropAll("the check is over");
|
|
153
|
+
rmSync(root, { recursive: true, force: true });
|
|
154
|
+
});
|
|
113
155
|
|
|
114
156
|
const dir = store.directoryFor(KEY);
|
|
115
157
|
store.useFormat(KEY, fmp4Format);
|
|
@@ -134,7 +176,14 @@ test("clearing up after one run leaves every other run's work alone", (t) => {
|
|
|
134
176
|
|
|
135
177
|
test("a directory that has not moved is not read again", (t) => {
|
|
136
178
|
const { store, root } = storeInATempRoot();
|
|
137
|
-
t.after(() =>
|
|
179
|
+
t.after(() => {
|
|
180
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
181
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
182
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
183
|
+
// open watch cannot be removed on Windows.
|
|
184
|
+
store.dropAll("the check is over");
|
|
185
|
+
rmSync(root, { recursive: true, force: true });
|
|
186
|
+
});
|
|
138
187
|
|
|
139
188
|
const dir = store.directoryFor(KEY);
|
|
140
189
|
store.useFormat(KEY, fmp4Format);
|
|
@@ -148,7 +197,14 @@ test("a directory that has not moved is not read again", (t) => {
|
|
|
148
197
|
|
|
149
198
|
test("what a killed process left is found, named and counted", (t) => {
|
|
150
199
|
const { store, root, lines } = storeInATempRoot();
|
|
151
|
-
t.after(() =>
|
|
200
|
+
t.after(() => {
|
|
201
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
202
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
203
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
204
|
+
// open watch cannot be removed on Windows.
|
|
205
|
+
store.dropAll("the check is over");
|
|
206
|
+
rmSync(root, { recursive: true, force: true });
|
|
207
|
+
});
|
|
152
208
|
|
|
153
209
|
const dir = path.join(root, directoryNameFor(KEY));
|
|
154
210
|
mkdirSync(dir, { recursive: true });
|
|
@@ -169,7 +225,14 @@ test("what a killed process left is found, named and counted", (t) => {
|
|
|
169
225
|
|
|
170
226
|
test("what survived a kill is taken back whole, minus what was still being written", (t) => {
|
|
171
227
|
const { store, root } = storeInATempRoot();
|
|
172
|
-
t.after(() =>
|
|
228
|
+
t.after(() => {
|
|
229
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
230
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
231
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
232
|
+
// open watch cannot be removed on Windows.
|
|
233
|
+
store.dropAll("the check is over");
|
|
234
|
+
rmSync(root, { recursive: true, force: true });
|
|
235
|
+
});
|
|
173
236
|
|
|
174
237
|
const dir = path.join(root, directoryNameFor(KEY));
|
|
175
238
|
mkdirSync(dir, { recursive: true });
|
|
@@ -195,7 +258,14 @@ test("what survived a kill is taken back whole, minus what was still being writt
|
|
|
195
258
|
|
|
196
259
|
test("a directory that cannot name itself is thrown away rather than served", (t) => {
|
|
197
260
|
const { store, root } = storeInATempRoot();
|
|
198
|
-
t.after(() =>
|
|
261
|
+
t.after(() => {
|
|
262
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
263
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
264
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
265
|
+
// open watch cannot be removed on Windows.
|
|
266
|
+
store.dropAll("the check is over");
|
|
267
|
+
rmSync(root, { recursive: true, force: true });
|
|
268
|
+
});
|
|
199
269
|
|
|
200
270
|
const orphan = path.join(root, "0123456789abcdef");
|
|
201
271
|
mkdirSync(orphan, { recursive: true });
|
|
@@ -210,7 +280,14 @@ test("a directory that cannot name itself is thrown away rather than served", (t
|
|
|
210
280
|
|
|
211
281
|
test("an output this proxy can no longer serve is thrown away too", (t) => {
|
|
212
282
|
const { store, root } = storeInATempRoot();
|
|
213
|
-
t.after(() =>
|
|
283
|
+
t.after(() => {
|
|
284
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
285
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
286
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
287
|
+
// open watch cannot be removed on Windows.
|
|
288
|
+
store.dropAll("the check is over");
|
|
289
|
+
rmSync(root, { recursive: true, force: true });
|
|
290
|
+
});
|
|
214
291
|
|
|
215
292
|
const dir = path.join(root, directoryNameFor(KEY));
|
|
216
293
|
mkdirSync(dir, { recursive: true });
|
|
@@ -225,7 +302,14 @@ test("an output this proxy can no longer serve is thrown away too", (t) => {
|
|
|
225
302
|
|
|
226
303
|
test("what the store weighs is reported, and dropping one output frees it", (t) => {
|
|
227
304
|
const { store, root } = storeInATempRoot();
|
|
228
|
-
t.after(() =>
|
|
305
|
+
t.after(() => {
|
|
306
|
+
// The store lets its directories go, and only then is the root removed: it
|
|
307
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
308
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
309
|
+
// open watch cannot be removed on Windows.
|
|
310
|
+
store.dropAll("the check is over");
|
|
311
|
+
rmSync(root, { recursive: true, force: true });
|
|
312
|
+
});
|
|
229
313
|
|
|
230
314
|
const dir = store.directoryFor(KEY);
|
|
231
315
|
store.useFormat(KEY, fmp4Format);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import test from "node:test";
|
|
17
17
|
import assert from "node:assert/strict";
|
|
18
|
-
import { readSelfContainedStartSeconds } from "../services/segment-formats/mp4-boxes.js";
|
|
18
|
+
import { readSelfContainedStartSeconds } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* A minimal MP4 carrying `moov > mvhd` and `moov > trak > edts > elst`, with a
|
|
@@ -15,24 +15,25 @@
|
|
|
15
15
|
|
|
16
16
|
import test from "node:test";
|
|
17
17
|
import assert from "node:assert/strict";
|
|
18
|
-
import { SourceFile } from "../services/
|
|
18
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
19
19
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import path from "node:path";
|
|
22
|
-
import {
|
|
23
|
-
import { SegmentStore } from "../services/
|
|
24
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
25
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
22
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
23
|
+
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
24
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
25
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
26
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
26
27
|
|
|
27
28
|
const OUTPUT_KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
* @returns {{ manager:
|
|
31
|
+
* @returns {{ manager: object, store: SegmentStore, root: string }}
|
|
31
32
|
*/
|
|
32
33
|
function managerOverATempStore() {
|
|
33
34
|
const root = mkdtempSync(path.join(os.tmpdir(), "shared-segments-"));
|
|
34
35
|
const store = new SegmentStore({ root });
|
|
35
|
-
const manager =
|
|
36
|
+
const manager = wireOutputs({
|
|
36
37
|
enabled: true,
|
|
37
38
|
ffmpegBin: "ffmpeg",
|
|
38
39
|
localBindHost: "127.0.0.1",
|
|
@@ -52,6 +53,7 @@ function managerOverATempStore() {
|
|
|
52
53
|
function sessionOn({ id, dirPath, outputKey }) {
|
|
53
54
|
return {
|
|
54
55
|
id,
|
|
56
|
+
spec: outputSpec({ transcodeVideo: false }),
|
|
55
57
|
outputKey,
|
|
56
58
|
dirPath,
|
|
57
59
|
state: "ready",
|
|
@@ -63,7 +65,6 @@ function sessionOn({ id, dirPath, outputKey }) {
|
|
|
63
65
|
segmentFormat: fmp4Format,
|
|
64
66
|
// Where the file is cut, which every live session holds.
|
|
65
67
|
timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "keyframe" }),
|
|
66
|
-
consumers: new Set(),
|
|
67
68
|
lastAccessedAt: Date.now()
|
|
68
69
|
};
|
|
69
70
|
}
|
|
@@ -93,12 +94,12 @@ test("each session serves the segments the other one's encoder made", (t) => {
|
|
|
93
94
|
|
|
94
95
|
const viewerOne = sessionOn({ id: "s-one", dirPath, outputKey: OUTPUT_KEY });
|
|
95
96
|
const viewerTwo = sessionOn({ id: "s-two", dirPath, outputKey: OUTPUT_KEY });
|
|
96
|
-
manager.
|
|
97
|
-
manager.
|
|
97
|
+
manager.outputs.set(viewerOne.id, viewerOne);
|
|
98
|
+
manager.outputs.set(viewerTwo.id, viewerTwo);
|
|
98
99
|
|
|
99
100
|
const sorted = (numbers) => [...numbers].sort((left, right) => left - right);
|
|
100
|
-
const heldByOne = sorted(manager.producedSegmentNumbers(viewerOne));
|
|
101
|
-
const heldByTwo = sorted(manager.producedSegmentNumbers(viewerTwo));
|
|
101
|
+
const heldByOne = sorted(manager.serving.producedSegmentNumbers(viewerOne));
|
|
102
|
+
const heldByTwo = sorted(manager.serving.producedSegmentNumbers(viewerTwo));
|
|
102
103
|
|
|
103
104
|
assert.deepEqual(heldByOne, [0, 100]);
|
|
104
105
|
assert.deepEqual(heldByTwo, [0, 100], "neither viewer's session is the owner of either run");
|
|
@@ -114,11 +115,11 @@ test("a session leaving does not take the segments with it", async (t) => {
|
|
|
114
115
|
|
|
115
116
|
const viewerOne = sessionOn({ id: "s-one", dirPath, outputKey: OUTPUT_KEY });
|
|
116
117
|
const viewerTwo = sessionOn({ id: "s-two", dirPath, outputKey: OUTPUT_KEY });
|
|
117
|
-
manager.
|
|
118
|
-
manager.
|
|
118
|
+
manager.outputs.set(viewerOne.id, viewerOne);
|
|
119
|
+
manager.outputs.set(viewerTwo.id, viewerTwo);
|
|
119
120
|
|
|
120
|
-
await manager.disposeSession(viewerOne.id);
|
|
121
|
-
await manager.disposeSession(viewerTwo.id);
|
|
121
|
+
await manager.lifecycle.disposeSession(viewerOne.id);
|
|
122
|
+
await manager.lifecycle.disposeSession(viewerTwo.id);
|
|
122
123
|
|
|
123
124
|
// Both sessions are gone and the material is still here. That is the whole
|
|
124
125
|
// decoupling: a session ending says nothing about whether anybody will ask
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { bodySender } from "../services/data-channel-handler.js";
|
|
3
|
+
import { bodySender } from "../services/transport/data-channel-handler.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Drives the sender over a body arriving in the given read sizes, and answers
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file An output already made serves a viewer when the rules decided with the
|
|
3
|
+
* user 2026-09-16 allow it, through the paths a real request takes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { writeFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
11
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
12
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
13
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
14
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
15
|
+
import { Output } from "../services/encode/output/Output.js";
|
|
16
|
+
|
|
17
|
+
const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
|
|
18
|
+
|
|
19
|
+
function hostWithA1080pOutput(t) {
|
|
20
|
+
const { manager, store, cleanup } = managerWithOwnStore({
|
|
21
|
+
startupWaitMs: 0,
|
|
22
|
+
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
23
|
+
});
|
|
24
|
+
t.after(async () => {
|
|
25
|
+
await manager.lifecycle.disposeAll();
|
|
26
|
+
cleanup();
|
|
27
|
+
});
|
|
28
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
29
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
30
|
+
manager.getCachedMediaInfo = () => ({ durationSeconds: 400, width: 1920, height: 1080, fps: 24 });
|
|
31
|
+
const spec = outputSpec({
|
|
32
|
+
sourceKey: TORRENT,
|
|
33
|
+
transcodeVideo: true,
|
|
34
|
+
transcodeAudio: false,
|
|
35
|
+
encoder: "h264_vaapi",
|
|
36
|
+
width: 1920,
|
|
37
|
+
height: 1080,
|
|
38
|
+
fps: 24,
|
|
39
|
+
cutGrid: "uniform"
|
|
40
|
+
});
|
|
41
|
+
const key = spec.toKey();
|
|
42
|
+
store.useFormat(key, fmp4Format);
|
|
43
|
+
// The piece at the start of the film is made.
|
|
44
|
+
writeFileSync(path.join(store.directoryFor(key), fmp4Format.segmentFileName(0)), Buffer.alloc(100));
|
|
45
|
+
const file = new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }).learn({ width: 1920, height: 1080, durationSeconds: 400 });
|
|
46
|
+
const made = {
|
|
47
|
+
id: spec.toName(),
|
|
48
|
+
spec,
|
|
49
|
+
get outputKey() { return this.spec.toKey(); },
|
|
50
|
+
file,
|
|
51
|
+
timeline: new Timeline({ boundaries: Array.from({ length: 101 }, (_, index) => index * 4), cutGrid: "uniform" }),
|
|
52
|
+
output: new Output({ encodeWidth: 1920, encodeHeight: 1080, outputFps: 24 }),
|
|
53
|
+
segmentFormat: fmp4Format,
|
|
54
|
+
useSyntheticPlaylist: true
|
|
55
|
+
};
|
|
56
|
+
manager.outputs.set(made.id, made);
|
|
57
|
+
return { manager, made };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function request(over) {
|
|
61
|
+
return {
|
|
62
|
+
sourceKey: TORRENT,
|
|
63
|
+
fileIndex: 0,
|
|
64
|
+
fileName: "film.mkv",
|
|
65
|
+
transcodeVideo: true,
|
|
66
|
+
transcodeAudio: false,
|
|
67
|
+
segmentFormatId: "fmp4",
|
|
68
|
+
...over
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("the automatic choice is served by a better output whose piece at the viewer's position is made", async (t) => {
|
|
73
|
+
const { manager, made } = hostWithA1080pOutput(t);
|
|
74
|
+
const answered = await manager.viewerRequests.createOrGetSession(
|
|
75
|
+
request({ consumerId: "auto-viewer", targetWidth: 1280, targetHeight: 720 })
|
|
76
|
+
);
|
|
77
|
+
assert.equal(answered, made);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a size picked by hand is not served by another size", async (t) => {
|
|
81
|
+
const { manager, made } = hostWithA1080pOutput(t);
|
|
82
|
+
let answered = null;
|
|
83
|
+
try {
|
|
84
|
+
answered = await manager.viewerRequests.createOrGetSession(
|
|
85
|
+
request({ consumerId: "picker", targetWidth: 1280, targetHeight: 720, exactSize: true })
|
|
86
|
+
);
|
|
87
|
+
} catch {
|
|
88
|
+
answered = null;
|
|
89
|
+
}
|
|
90
|
+
assert.notEqual(answered, made);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a step asked for by an automatic viewer and served by the picture leaves the picture a picture", async (t) => {
|
|
94
|
+
const { manager, made } = hostWithA1080pOutput(t);
|
|
95
|
+
manager.viewers.of(made, "auto-viewer").qualityMode = "auto";
|
|
96
|
+
|
|
97
|
+
const variant = await manager.renditions.resolveVariantSession(made.id, 720, 0, "auto-viewer");
|
|
98
|
+
|
|
99
|
+
assert.equal(variant, made, "the picture already made at 1080p serves the 720p request");
|
|
100
|
+
assert.notEqual(made.isStep, true, "and is not turned into a step of itself");
|
|
101
|
+
assert.ok(manager.viewers.forOutput(made).has("auto-viewer"));
|
|
102
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which existing output serves a viewer: the rules decided with the user
|
|
3
|
+
* 2026-09-16, with the viewer's own link asked in every mode and on every
|
|
4
|
+
* branch (roadmap item 97, step 11).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { chooseServingOutput } from "../services/encode/quality/serving-output.js";
|
|
10
|
+
import { linkCouldCarry, loadOf, PEAK_CLASS } from "../services/encode/quality/link-budget.js";
|
|
11
|
+
|
|
12
|
+
const WANTED = { width: 1824, height: 1026 };
|
|
13
|
+
const NEXT_LOWER = 1280 * 720;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} key
|
|
17
|
+
* @param {number} width
|
|
18
|
+
* @param {number} height
|
|
19
|
+
* @param {{ mbps?: number | null, peakClass?: string, readyHere?: boolean }} [over]
|
|
20
|
+
*/
|
|
21
|
+
function candidate(key, width, height, { mbps = 5, peakClass = PEAK_CLASS.KNOWN, readyHere = true } = {}) {
|
|
22
|
+
return { key, width, height, readyHere, load: loadOf({ mbps, peakClass: mbps === null ? PEAK_CLASS.UNKNOWN : peakClass }, null) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} over
|
|
27
|
+
* @param {number | null} [linkMbps] - What this viewer's link measured.
|
|
28
|
+
*/
|
|
29
|
+
function choose(over, linkMbps = null) {
|
|
30
|
+
return chooseServingOutput({
|
|
31
|
+
mode: "auto",
|
|
32
|
+
wanted: WANTED,
|
|
33
|
+
nextLowerArea: NEXT_LOWER,
|
|
34
|
+
atRisk: false,
|
|
35
|
+
judge: (one) => linkCouldCarry(linkMbps, one.load),
|
|
36
|
+
candidates: [],
|
|
37
|
+
...over
|
|
38
|
+
})?.key ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("the same quality or higher is served, the nearest of them", () => {
|
|
42
|
+
assert.equal(choose({ candidates: [candidate("2160", 3840, 2160), candidate("1080", 1920, 1080)] }), "1080");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("higher quality is not served over a link it does not fit", () => {
|
|
46
|
+
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { mbps: 12 })] }, 10), null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a little lower — above the next rung down — is served, and only if the link admits it", () => {
|
|
50
|
+
assert.equal(choose({ candidates: [candidate("1000", 1776, 1000)] }), "1000");
|
|
51
|
+
assert.equal(
|
|
52
|
+
choose({ candidates: [candidate("1000", 1776, 1000, { mbps: 12 })] }, 10),
|
|
53
|
+
null,
|
|
54
|
+
"a smaller picture proves nothing by itself: its own load is asked"
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("the next rung down or lower is served only when making the wanted format risks a wait, and only if it fits", () => {
|
|
59
|
+
const lower = [candidate("720", 1280, 720), candidate("480", 854, 480)];
|
|
60
|
+
assert.equal(choose({ candidates: lower }), null);
|
|
61
|
+
assert.equal(choose({ candidates: lower, atRisk: true }), "720");
|
|
62
|
+
assert.equal(
|
|
63
|
+
choose({ candidates: [candidate("720", 1280, 720, { mbps: 12 }), candidate("480", 854, 480, { mbps: 3 })], atRisk: true }, 10),
|
|
64
|
+
"480",
|
|
65
|
+
"the larger one does not fit this link, so it is not taken"
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an output without the piece at the viewer's position spares them nothing", () => {
|
|
70
|
+
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { readyHere: false })] }), null);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a size chosen by hand is served only exactly, and only if the link admits it", () => {
|
|
74
|
+
const candidates = [candidate("1080", 1920, 1080), candidate("exact", 1824, 1026)];
|
|
75
|
+
assert.equal(choose({ mode: "manual", candidates }), "exact");
|
|
76
|
+
assert.equal(choose({ mode: "manual", candidates: [candidate("1080", 1920, 1080)], atRisk: true }), null);
|
|
77
|
+
assert.equal(
|
|
78
|
+
choose({ mode: "manual", candidates: [candidate("exact", 1824, 1026, { mbps: 12 })] }, 10),
|
|
79
|
+
null,
|
|
80
|
+
"the size they picked, but more than their link carries: not handed over"
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("of two outputs of the size picked by hand, the higher limit the link admits is served", () => {
|
|
85
|
+
const candidates = [candidate("2800", 1824, 1026, { mbps: 3.64 }), candidate("1400", 1824, 1026, { mbps: 1.82 })];
|
|
86
|
+
assert.equal(choose({ mode: "manual", candidates }, 10), "2800");
|
|
87
|
+
assert.equal(choose({ mode: "manual", candidates }, 3), "1400", "the link admits only the lower limit");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a bound is taken before an average, even when the average's limit is higher", () => {
|
|
91
|
+
const candidates = [
|
|
92
|
+
candidate("estimated", 1824, 1026, { mbps: 6, peakClass: PEAK_CLASS.ESTIMATED }),
|
|
93
|
+
candidate("known", 1824, 1026, { mbps: 4, peakClass: PEAK_CLASS.KNOWN })
|
|
94
|
+
];
|
|
95
|
+
assert.equal(choose({ mode: "manual", candidates }, 10), "known");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("an output with no bound is not served against a measured link, and is while nothing measured it", () => {
|
|
99
|
+
const candidates = [candidate("hardware", 1824, 1026, { mbps: null })];
|
|
100
|
+
assert.equal(choose({ mode: "manual", candidates }, 50), null);
|
|
101
|
+
assert.equal(choose({ mode: "manual", candidates }, null), "hardware");
|
|
102
|
+
});
|
|
@@ -12,7 +12,7 @@ import assert from "node:assert/strict";
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { SharedPieceStore } from "../services/piece-store/shared-piece-store.js";
|
|
15
|
+
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
16
16
|
|
|
17
17
|
const CHUNK = 64 * 1024;
|
|
18
18
|
|
package/test/source-file.test.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import test from "node:test";
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
|
-
import { SourceFile, SourceFiles, sourceDecodeCharacteristics } from "../services/
|
|
15
|
+
import { SourceFile, SourceFiles, sourceDecodeCharacteristics } from "../services/media/SourceFile.js";
|
|
16
16
|
|
|
17
17
|
test("a file's key is the pair that identifies it, spelled in one place", () => {
|
|
18
18
|
const file = new SourceFile({ sourceKey: "abc123", fileIndex: 4 });
|
|
@@ -19,31 +19,31 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
|
-
import { SourceFile } from "../services/
|
|
23
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
22
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
23
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
24
24
|
import test from "node:test";
|
|
25
25
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
26
26
|
import os from "node:os";
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
|
|
29
|
-
import {
|
|
29
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
30
30
|
import { startRunOn } from "./helpers/encode-run.js";
|
|
31
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
31
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
32
32
|
|
|
33
33
|
const SEGMENT_SECONDS = 4;
|
|
34
34
|
const RUN_STARTS_AT = 373;
|
|
35
35
|
const BEHIND_INDEX = 371;
|
|
36
|
-
const SESSION_ID = "
|
|
36
|
+
const SESSION_ID = "2222222233334444";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* A live session whose run begins at #373 and whose directory is empty, so any
|
|
40
40
|
* segment request is a request for something not yet produced.
|
|
41
41
|
*
|
|
42
|
-
* @returns {Promise<{ manager:
|
|
42
|
+
* @returns {Promise<{ manager: object, session: object, dirPath: string }>}
|
|
43
43
|
*/
|
|
44
44
|
async function sessionWithRunAt373() {
|
|
45
45
|
const dirPath = await mkdtemp(path.join(os.tmpdir(), "stale-request-"));
|
|
46
|
-
const manager =
|
|
46
|
+
const manager = wireOutputs({
|
|
47
47
|
enabled: true,
|
|
48
48
|
ffmpegBin: "ffmpeg",
|
|
49
49
|
localBindHost: "127.0.0.1",
|
|
@@ -63,7 +63,6 @@ async function sessionWithRunAt373() {
|
|
|
63
63
|
segmentFormat: fmp4Format,
|
|
64
64
|
segmentCount: 900,
|
|
65
65
|
startedAt: Date.now(),
|
|
66
|
-
createEntryMs: Date.now(),
|
|
67
66
|
lastAccessedAt: Date.now(),
|
|
68
67
|
runs: new Set(),
|
|
69
68
|
consumers: new Set(),
|
|
@@ -72,16 +71,15 @@ async function sessionWithRunAt373() {
|
|
|
72
71
|
useSyntheticPlaylist: true,
|
|
73
72
|
playlistText: "#EXTM3U\n",
|
|
74
73
|
lastError: "",
|
|
75
|
-
firstSegmentLogged: false,
|
|
76
74
|
waitEpoch: 0
|
|
77
75
|
};
|
|
78
|
-
manager.
|
|
76
|
+
manager.outputs.set(SESSION_ID, session);
|
|
79
77
|
startRunOn(session, { from: RUN_STARTS_AT, usesExplicitCuts: true, speedX: 2 });
|
|
80
78
|
return { manager, session, dirPath };
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
/**
|
|
84
|
-
* @param {
|
|
82
|
+
* @param {object} manager
|
|
85
83
|
* @param {object} session
|
|
86
84
|
* @param {string} dirPath
|
|
87
85
|
*/
|
|
@@ -90,7 +88,7 @@ async function tidy(manager, session, dirPath) {
|
|
|
90
88
|
clearTimeout(session.seekSettleTimer);
|
|
91
89
|
session.seekSettleTimer = null;
|
|
92
90
|
}
|
|
93
|
-
manager.
|
|
91
|
+
manager.outputs.clear();
|
|
94
92
|
manager.stop?.();
|
|
95
93
|
await rm(dirPath, { recursive: true, force: true });
|
|
96
94
|
}
|
|
@@ -102,12 +100,11 @@ test("a request behind where the viewer said they are does not move the encoder"
|
|
|
102
100
|
});
|
|
103
101
|
|
|
104
102
|
// The viewer stated their position: 2083.4 s, which is segment #520 here.
|
|
105
|
-
manager.requestSeek(SESSION_ID, 2083.4, "viewer-1");
|
|
103
|
+
manager.viewerRequests.requestSeek(SESSION_ID, 2083.4, "viewer-1");
|
|
106
104
|
|
|
107
|
-
await manager.getFileStream(
|
|
105
|
+
await manager.serving.getFileStream(
|
|
108
106
|
SESSION_ID,
|
|
109
|
-
fmp4Format.segmentFileName(BEHIND_INDEX)
|
|
110
|
-
{ requestSeq: 1 }
|
|
107
|
+
fmp4Format.segmentFileName(BEHIND_INDEX)
|
|
111
108
|
);
|
|
112
109
|
|
|
113
110
|
// The field log's line was `seek settle → restart at segment #370`. Nothing
|
|
@@ -136,10 +133,9 @@ test("the same traffic moves nothing when the viewer has said nothing either", a
|
|
|
136
133
|
// spoken. That exception is the thing that was removed: a request is evidence
|
|
137
134
|
// about what a player is reading, never about where a person is, and an
|
|
138
135
|
// encoder placed from it is placed from a number the player picked.
|
|
139
|
-
await manager.getFileStream(
|
|
136
|
+
await manager.serving.getFileStream(
|
|
140
137
|
SESSION_ID,
|
|
141
|
-
fmp4Format.segmentFileName(BEHIND_INDEX)
|
|
142
|
-
{ requestSeq: 1 }
|
|
138
|
+
fmp4Format.segmentFileName(BEHIND_INDEX)
|
|
143
139
|
);
|
|
144
140
|
|
|
145
141
|
assert.equal(session.seekTarget ?? null, null);
|
|
@@ -152,8 +148,8 @@ test("a request cannot move the viewer's position backwards", async (t) => {
|
|
|
152
148
|
await tidy(manager, session, dirPath);
|
|
153
149
|
});
|
|
154
150
|
|
|
155
|
-
manager.requestSeek(SESSION_ID, 2083.4, "viewer-1");
|
|
156
|
-
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(BEHIND_INDEX)
|
|
151
|
+
manager.viewerRequests.requestSeek(SESSION_ID, 2083.4, "viewer-1");
|
|
152
|
+
await manager.serving.getFileStream(SESSION_ID, fmp4Format.segmentFileName(BEHIND_INDEX));
|
|
157
153
|
|
|
158
154
|
assert.ok(
|
|
159
155
|
Math.abs((manager.viewers.get("viewer-1")?.positionSeconds() ?? -1) - 2083.4) < 1,
|