@torrent-tv/proxy 2.86.0 → 2.87.1
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 +2142 -1915
- package/CLAUDE.md +322 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/browser-proxy-contract.md +94 -0
- 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 +9 -6
- package/routes/transcode/session-file/get.js +52 -268
- package/routes/transcode/variant-file/get.js +46 -7
- package/routes/transcode/variant-warm/get.js +19 -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 +307 -29
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1229 -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 +755 -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 +935 -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/http-responses.js +60 -0
- package/services/server/transcode-session-files.js +325 -0
- package/services/server/wire-outputs.js +983 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -45
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -515
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +46 -9
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -202
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9608
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/services/viewer/link-readings.js +0 -56
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A piece short of a track is judged against ITS OWN header.
|
|
3
|
+
*
|
|
4
|
+
* The question "does this piece carry every track" has two possible answers and
|
|
5
|
+
* they call for opposite actions, which is why asking it against the session's
|
|
6
|
+
* cached header alone was not enough: a piece that disagrees with that header
|
|
7
|
+
* may be short, or the header may be the odd one out. A self-contained piece
|
|
8
|
+
* carries a `moov` of its own, so it can be compared with itself, and that
|
|
9
|
+
* comparison tells the two apart without asking anything about encoders.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THIS REPLACED. The action used to be chosen by "is any run of this
|
|
12
|
+
* session alive?" — a question about the SESSION, asked about a fact of the
|
|
13
|
+
* BYTES. A run anywhere in the film meant "still being written; waiting for
|
|
14
|
+
* it", and nothing re-judged or re-made the piece afterwards, so one
|
|
15
|
+
* misjudgement became permanent.
|
|
16
|
+
*
|
|
17
|
+
* Field 2026-09-14: `segment-00033.mp4` of a copied picture, already served
|
|
18
|
+
* whole to one viewer at 5 099 421 bytes, was refused to a second viewer for
|
|
19
|
+
* five minutes — four holds of sixty seconds each, then 503 — while the run in
|
|
20
|
+
* force was at #192 and beyond. That viewer's picture stood still 196.2 s and
|
|
21
|
+
* then a further 56.2 s.
|
|
22
|
+
*
|
|
23
|
+
* The run question is also unanswerable and does not need asking: a piece being
|
|
24
|
+
* written is called `making-<tag>-NNNNN.mp4` and takes its served name by a
|
|
25
|
+
* rename, which is atomic. A file under its served name is never being written.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import test from "node:test";
|
|
29
|
+
import assert from "node:assert/strict";
|
|
30
|
+
import { access, writeFile } from "node:fs/promises";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
33
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
34
|
+
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
35
|
+
import { startRunOn } from "./helpers/encode-run.js";
|
|
36
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
37
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
38
|
+
|
|
39
|
+
const SESSION_ID = "3333333344445555";
|
|
40
|
+
const OUTPUT_KEY = "own-header:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
41
|
+
const MOVIE_TIMESCALE = 1000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} type
|
|
45
|
+
* @param {Buffer} body
|
|
46
|
+
* @returns {Buffer}
|
|
47
|
+
*/
|
|
48
|
+
function box(type, body) {
|
|
49
|
+
const head = Buffer.alloc(8);
|
|
50
|
+
head.writeUInt32BE(8 + body.length, 0);
|
|
51
|
+
head.write(type, 4, "latin1");
|
|
52
|
+
return Buffer.concat([head, body]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {number} trackId
|
|
57
|
+
* @returns {Buffer}
|
|
58
|
+
*/
|
|
59
|
+
function trak(trackId) {
|
|
60
|
+
const tkhdBody = Buffer.alloc(84);
|
|
61
|
+
tkhdBody.writeUInt32BE(trackId, 12);
|
|
62
|
+
const mdhdBody = Buffer.alloc(20);
|
|
63
|
+
mdhdBody.writeUInt32BE(90_000, 12);
|
|
64
|
+
return box("trak", Buffer.concat([
|
|
65
|
+
box("tkhd", tkhdBody),
|
|
66
|
+
box("mdia", box("mdhd", mdhdBody))
|
|
67
|
+
]));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A header declaring `tracks` tracks.
|
|
72
|
+
*
|
|
73
|
+
* @param {number} tracks
|
|
74
|
+
* @returns {Buffer}
|
|
75
|
+
*/
|
|
76
|
+
function moovOf(tracks) {
|
|
77
|
+
const mvhdBody = Buffer.alloc(100);
|
|
78
|
+
mvhdBody.writeUInt32BE(MOVIE_TIMESCALE, 12);
|
|
79
|
+
const traks = [];
|
|
80
|
+
for (let id = 1; id <= tracks; id += 1) {
|
|
81
|
+
traks.push(trak(id));
|
|
82
|
+
}
|
|
83
|
+
return box("moov", Buffer.concat([box("mvhd", mvhdBody), ...traks]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {number} trackId
|
|
88
|
+
* @returns {Buffer}
|
|
89
|
+
*/
|
|
90
|
+
function traf(trackId) {
|
|
91
|
+
const tfhdBody = Buffer.alloc(8);
|
|
92
|
+
tfhdBody.writeUInt32BE(trackId, 4);
|
|
93
|
+
const tfdtBody = Buffer.alloc(12);
|
|
94
|
+
tfdtBody.writeUInt8(1, 0);
|
|
95
|
+
tfdtBody.writeBigUInt64BE(0n, 4);
|
|
96
|
+
return box("traf", Buffer.concat([box("tfhd", tfhdBody), box("tfdt", tfdtBody)]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A piece shaped like the `segment` muxer's output.
|
|
101
|
+
*
|
|
102
|
+
* @param {{ declares: number, carries: number }} shape - How many tracks its own
|
|
103
|
+
* header declares, and how many its fragments actually carry. The two differ
|
|
104
|
+
* only in a piece a run closed short.
|
|
105
|
+
* @returns {Buffer}
|
|
106
|
+
*/
|
|
107
|
+
function selfContainedPiece({ declares, carries }) {
|
|
108
|
+
const trafs = [];
|
|
109
|
+
for (let id = 1; id <= carries; id += 1) {
|
|
110
|
+
trafs.push(traf(id));
|
|
111
|
+
}
|
|
112
|
+
return Buffer.concat([
|
|
113
|
+
box("ftyp", Buffer.alloc(16, 0)),
|
|
114
|
+
moovOf(declares),
|
|
115
|
+
box("moof", Buffer.concat(trafs)),
|
|
116
|
+
box("mdat", Buffer.alloc(64, 0x5a)),
|
|
117
|
+
box("mfra", Buffer.alloc(24, 0))
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A manager with one session whose first two pieces are already on disk.
|
|
123
|
+
*
|
|
124
|
+
* @param {{ piece: Buffer, sessionHeader: Buffer }} params - The piece written
|
|
125
|
+
* for every number, and what the session has cached as its header.
|
|
126
|
+
* @returns {Promise<{ manager: object, dirPath: string }>}
|
|
127
|
+
*/
|
|
128
|
+
async function managerHolding({ piece, sessionHeader }) {
|
|
129
|
+
const { manager } = managerWithOwnStore();
|
|
130
|
+
manager.segmentStore.useFormat(OUTPUT_KEY, fmp4Format);
|
|
131
|
+
const dirPath = manager.segmentStore.directoryFor(OUTPUT_KEY);
|
|
132
|
+
await writeFile(path.join(dirPath, "segment-00000.mp4"), piece);
|
|
133
|
+
await writeFile(path.join(dirPath, "segment-00001.mp4"), piece);
|
|
134
|
+
|
|
135
|
+
const session = {
|
|
136
|
+
id: SESSION_ID,
|
|
137
|
+
spec: outputSpec({ transcodeVideo: false }),
|
|
138
|
+
outputKey: OUTPUT_KEY,
|
|
139
|
+
dirPath,
|
|
140
|
+
timeline: new Timeline({ boundaries: [0, 12.5, 25, 37.5, 50, 62.5], cutGrid: "uniform" }),
|
|
141
|
+
state: "ready",
|
|
142
|
+
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" })
|
|
143
|
+
.learn({ durationSeconds: 62.5 }),
|
|
144
|
+
get inputFile() { return this.file; },
|
|
145
|
+
get audioFile() { return this.file; },
|
|
146
|
+
lastAloneSpeed: 2,
|
|
147
|
+
startedAt: Date.now(),
|
|
148
|
+
lastAccessedAt: Date.now(),
|
|
149
|
+
runs: new Set(),
|
|
150
|
+
lastError: "",
|
|
151
|
+
segmentCount: 5,
|
|
152
|
+
segmentFormat: fmp4Format,
|
|
153
|
+
useSyntheticPlaylist: true,
|
|
154
|
+
playlistText: "#EXTM3U\n",
|
|
155
|
+
initBytes: sessionHeader,
|
|
156
|
+
waitEpoch: 0
|
|
157
|
+
};
|
|
158
|
+
manager.outputs.set(SESSION_ID, session);
|
|
159
|
+
// No plan runs here: this file is about the path that answers a request.
|
|
160
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
161
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
162
|
+
// A LIVE RUN, deliberately. Under the rule this replaced, a live run anywhere
|
|
163
|
+
// was what turned a refusal into a permanent one — so both cases below are
|
|
164
|
+
// asked with one running, which is the state the field failure happened in.
|
|
165
|
+
startRunOn(session, { from: 0, usesExplicitCuts: true, speedX: 2 });
|
|
166
|
+
return { manager, dirPath };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
test("a piece that agrees with its own header is served, whatever the session's header says", async () => {
|
|
170
|
+
// Two tracks declared, two carried: whole by its own account. The session's
|
|
171
|
+
// cached header declares three — the state that cannot be told from a short
|
|
172
|
+
// piece without asking the piece itself.
|
|
173
|
+
const piece = selfContainedPiece({ declares: 2, carries: 2 });
|
|
174
|
+
const { manager, dirPath } = await managerHolding({
|
|
175
|
+
piece,
|
|
176
|
+
sessionHeader: moovOf(3)
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const answer = await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
180
|
+
|
|
181
|
+
assert.equal(answer.kind, "file", "the piece is whole and must reach the viewer");
|
|
182
|
+
await access(path.join(dirPath, "segment-00000.mp4"));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("a piece that disagrees with its own header is removed, so it can be made again", async () => {
|
|
186
|
+
// Two tracks declared, one carried: closed short by a run that was killed
|
|
187
|
+
// mid-piece, which is what this check exists to catch.
|
|
188
|
+
const piece = selfContainedPiece({ declares: 2, carries: 1 });
|
|
189
|
+
const { manager, dirPath } = await managerHolding({
|
|
190
|
+
piece,
|
|
191
|
+
sessionHeader: moovOf(2)
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const answer = await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
195
|
+
|
|
196
|
+
assert.equal(answer.kind, "warming-up", "a short piece is not servable");
|
|
197
|
+
await assert.rejects(
|
|
198
|
+
() => access(path.join(dirPath, "segment-00000.mp4")),
|
|
199
|
+
/ENOENT/,
|
|
200
|
+
"and it is removed, or nothing will ever make it again"
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("a live run elsewhere in the film does not make a short piece permanent", async () => {
|
|
205
|
+
const piece = selfContainedPiece({ declares: 2, carries: 1 });
|
|
206
|
+
const { manager, dirPath } = await managerHolding({
|
|
207
|
+
piece,
|
|
208
|
+
sessionHeader: moovOf(2)
|
|
209
|
+
});
|
|
210
|
+
// The field state exactly: the run in force is hundreds of segments ahead of
|
|
211
|
+
// the piece being asked for.
|
|
212
|
+
startRunOn(manager.outputs.get(SESSION_ID), { from: 192, usesExplicitCuts: true, speedX: 2 });
|
|
213
|
+
|
|
214
|
+
await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
215
|
+
|
|
216
|
+
await assert.rejects(
|
|
217
|
+
() => access(path.join(dirPath, "segment-00000.mp4")),
|
|
218
|
+
/ENOENT/,
|
|
219
|
+
"a run a thousand segments away says nothing about these bytes"
|
|
220
|
+
);
|
|
221
|
+
});
|
|
@@ -17,20 +17,23 @@
|
|
|
17
17
|
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
|
-
import { SourceFile } from "../services/
|
|
21
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
20
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
21
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
22
|
+
import { Output } from "../services/encode/output/Output.js";
|
|
23
|
+
import { KeyframeTable } from "../services/media/container/KeyframeTable.js";
|
|
22
24
|
import { rm, writeFile } from "node:fs/promises";
|
|
23
25
|
import path from "node:path";
|
|
24
|
-
import {
|
|
26
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
25
27
|
import { ENCODE_RUN_STATE } from "../services/encode/encode-run-state.js";
|
|
26
28
|
import { startRunOn } from "./helpers/encode-run.js";
|
|
27
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
29
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
30
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
28
31
|
|
|
29
32
|
const MOVIE_TIMESCALE = 1000;
|
|
30
33
|
const VIDEO_TIMESCALE = 90_000;
|
|
31
34
|
const AUDIO_TIMESCALE = 48_000;
|
|
32
35
|
const SEGMENT_START_SECONDS = 12.5;
|
|
33
|
-
const SESSION_ID = "
|
|
36
|
+
const SESSION_ID = "1111111122223333";
|
|
34
37
|
// Its own address, so this file's directory in the shared store is nobody
|
|
35
38
|
// else's.
|
|
36
39
|
const OUTPUT_KEY = "serve-wiring:fmt=fmp4:grid=uniform:video-only:v=0/copy";
|
|
@@ -118,11 +121,11 @@ function selfContainedPiece(offsetSeconds) {
|
|
|
118
121
|
* A manager holding one session whose segments are already on disk, cut at
|
|
119
122
|
* explicit times — the ordinary keyframe-cut path.
|
|
120
123
|
*
|
|
121
|
-
* @param {{ segmentFormat?: object }} [overrides]
|
|
122
|
-
* @returns {Promise<{ manager:
|
|
124
|
+
* @param {{ segmentFormat?: object, audioOnly?: boolean, withRun?: boolean }} [overrides]
|
|
125
|
+
* @returns {Promise<{ manager: object, session: object, dirPath: string }>}
|
|
123
126
|
*/
|
|
124
127
|
async function managerWithReadySegment(overrides = {}) {
|
|
125
|
-
const manager =
|
|
128
|
+
const manager = wireOutputs({
|
|
126
129
|
enabled: true,
|
|
127
130
|
ffmpegBin: "ffmpeg",
|
|
128
131
|
localBindHost: "127.0.0.1",
|
|
@@ -143,6 +146,11 @@ async function managerWithReadySegment(overrides = {}) {
|
|
|
143
146
|
|
|
144
147
|
const session = {
|
|
145
148
|
id: SESSION_ID,
|
|
149
|
+
spec: outputSpec({
|
|
150
|
+
transcodeVideo: overrides.transcodeVideo === true,
|
|
151
|
+
audioOnly: overrides.audioOnly === true,
|
|
152
|
+
cutGrid: "uniform"
|
|
153
|
+
}),
|
|
146
154
|
outputKey: OUTPUT_KEY,
|
|
147
155
|
dirPath,
|
|
148
156
|
// Where this file is cut, held by the file. A fixture that stated it
|
|
@@ -168,7 +176,6 @@ async function managerWithReadySegment(overrides = {}) {
|
|
|
168
176
|
// and the plan takes the encoder away for changing nothing.
|
|
169
177
|
lastAloneSpeed: 2,
|
|
170
178
|
startedAt: Date.now(),
|
|
171
|
-
createEntryMs: Date.now(),
|
|
172
179
|
lastAccessedAt: Date.now(),
|
|
173
180
|
runs: new Set(),
|
|
174
181
|
lastError: "",
|
|
@@ -181,11 +188,26 @@ async function managerWithReadySegment(overrides = {}) {
|
|
|
181
188
|
segmentFormat: overrides.segmentFormat ?? fmp4Format,
|
|
182
189
|
useSyntheticPlaylist: true,
|
|
183
190
|
playlistText: "#EXTM3U\n",
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
// WHAT IS BEING PRODUCED, which is also where a produced piece's landing is
|
|
192
|
+
// recorded. Per output, so one output's reading of a piece number cannot
|
|
193
|
+
// silence another output's reading of its own.
|
|
194
|
+
output: new Output({ encodeWidth: 0, encodeHeight: 0, outputFps: 25, softwarePreset: null, applyTonemap: false }),
|
|
195
|
+
// WHERE THIS FILE'S KEYFRAMES ARE, held by the file. A copied picture is cut
|
|
196
|
+
// at these and nowhere else, which is what makes its landing evidence about
|
|
197
|
+
// this table — and what makes a soundtrack's landing evidence about nothing
|
|
198
|
+
// of the sort.
|
|
199
|
+
keyframes: new KeyframeTable().learn({ times: [0, 12.5, 25, 37.5, 50], format: "matroska" }),
|
|
200
|
+
audioOnly: overrides.audioOnly === true,
|
|
186
201
|
waitEpoch: 0
|
|
187
202
|
};
|
|
188
|
-
|
|
203
|
+
// THE INIT IS THE STORE'S, and a player fetches it before any segment — so a
|
|
204
|
+
// fixture that wants segments served as production serves them has to put it
|
|
205
|
+
// there first. It used to sit on the session, which nothing reads any more:
|
|
206
|
+
// `prepareSegmentBytes` has no timescales without it and hands the piece back
|
|
207
|
+
// unstamped, which is silent and is exactly the fault stamping exists to
|
|
208
|
+
// prevent.
|
|
209
|
+
manager.segmentStore.keepInit(OUTPUT_KEY, fmp4Format.extractInit(piece));
|
|
210
|
+
manager.outputs.set(SESSION_ID, session);
|
|
189
211
|
// SOMEBODY IS WATCHING IT. A segment is requested by a viewer, so a fixture
|
|
190
212
|
// that asks for one without stating a viewer describes a state production
|
|
191
213
|
// never reaches — and an output nobody is watching has every encoder on it
|
|
@@ -195,38 +217,73 @@ async function managerWithReadySegment(overrides = {}) {
|
|
|
195
217
|
// question with its own checks, and letting it run inside these would spawn
|
|
196
218
|
// real ffmpeg processes and replace the very run each test is watching — the
|
|
197
219
|
// test would then be about the plan, and about it at its least stable.
|
|
198
|
-
manager.planEncodersNow = () => {};
|
|
199
|
-
manager.planEncodersSoon = () => {};
|
|
220
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
221
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
200
222
|
// The run in force. The fixture's segments live directly in the session
|
|
201
223
|
// directory, which is exactly what one run's directory is here.
|
|
202
|
-
|
|
224
|
+
if (overrides.withRun !== false) {
|
|
225
|
+
const run = startRunOn(session, { from: 0, usesExplicitCuts: true, speedX: 2 });
|
|
226
|
+
manager.encodeOrchestrator.adopt(OUTPUT_KEY, run);
|
|
227
|
+
}
|
|
203
228
|
return { manager, session, dirPath };
|
|
204
229
|
}
|
|
205
230
|
|
|
206
231
|
test("serving a segment records what its real start says about the container's index", async (t) => {
|
|
207
232
|
const { manager, session, dirPath } = await managerWithReadySegment();
|
|
208
233
|
t.after(async () => {
|
|
209
|
-
await manager.disposeAll();
|
|
234
|
+
await manager.lifecycle.disposeAll();
|
|
210
235
|
await rm(dirPath, { recursive: true, force: true });
|
|
211
236
|
});
|
|
212
237
|
// The tally is counted in the module tests; what this pins is that serving a
|
|
213
238
|
// segment reaches it at all. A counter nothing increments reports a clean
|
|
214
|
-
//
|
|
215
|
-
session.
|
|
239
|
+
// grid for every output forever, which is worse than no measurement.
|
|
240
|
+
assert.equal(session.output.landing, null, "nothing has been produced yet");
|
|
241
|
+
|
|
242
|
+
await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
243
|
+
|
|
244
|
+
assert.equal(session.output.landing.checked, 1, "the piece that was just produced must have been examined");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("only a copied picture's landing is taken as evidence about the file's keyframe table", async (t) => {
|
|
248
|
+
// The one reading, sent to the two owners the manager decides between. A
|
|
249
|
+
// soundtrack is cut exactly where it is asked to be and has no keyframes, so
|
|
250
|
+
// its landing is about its own output and about nothing else; sent to the
|
|
251
|
+
// file's table it would report the file's index wrong on the strength of a
|
|
252
|
+
// stream that never consulted it.
|
|
253
|
+
const sound = await managerWithReadySegment({ audioOnly: true });
|
|
254
|
+
t.after(async () => {
|
|
255
|
+
await sound.manager.lifecycle.disposeAll();
|
|
256
|
+
await rm(sound.dirPath, { recursive: true, force: true });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await sound.manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
260
|
+
|
|
261
|
+
assert.equal(sound.session.output.landing.checked, 1, "the sound's own landing is recorded");
|
|
262
|
+
assert.equal(
|
|
263
|
+
sound.session.keyframes.evidence,
|
|
264
|
+
null,
|
|
265
|
+
"and nothing it produced is evidence about where the file's keyframes are"
|
|
266
|
+
);
|
|
216
267
|
|
|
217
|
-
|
|
268
|
+
const picture = await managerWithReadySegment();
|
|
269
|
+
t.after(async () => {
|
|
270
|
+
await picture.manager.lifecycle.disposeAll();
|
|
271
|
+
await rm(picture.dirPath, { recursive: true, force: true });
|
|
272
|
+
});
|
|
218
273
|
|
|
219
|
-
|
|
274
|
+
await picture.manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
275
|
+
|
|
276
|
+
assert.equal(picture.session.keyframes.evidence.checked, 1, "a copy's landing does reach the file's table");
|
|
220
277
|
});
|
|
221
278
|
|
|
222
279
|
test("a segment that exists is served, not reported as still being produced", async (t) => {
|
|
223
280
|
const { manager, dirPath, session } = await managerWithReadySegment();
|
|
224
281
|
t.after(async () => {
|
|
225
|
-
await manager.disposeAll();
|
|
282
|
+
await manager.lifecycle.disposeAll();
|
|
226
283
|
await rm(dirPath, { recursive: true, force: true });
|
|
227
284
|
});
|
|
228
285
|
|
|
229
|
-
const result = await manager.getFileStream(SESSION_ID, "segment-00000.mp4"
|
|
286
|
+
const result = await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
230
287
|
|
|
231
288
|
assert.equal(result.kind, "file", "a finished segment on disk must come back as bytes");
|
|
232
289
|
assert.equal(result.contentType, fmp4Format.segmentContentType);
|
|
@@ -251,9 +308,9 @@ test("a segment that exists is served, not reported as still being produced", as
|
|
|
251
308
|
"the segment must be stamped with where it really begins"
|
|
252
309
|
);
|
|
253
310
|
assert.equal(
|
|
254
|
-
session.
|
|
311
|
+
session.output.landing.checked,
|
|
255
312
|
1,
|
|
256
|
-
"and the piece's own position must still have been read, or nothing measures the
|
|
313
|
+
"and the piece's own position must still have been read, or nothing measures the landing"
|
|
257
314
|
);
|
|
258
315
|
});
|
|
259
316
|
|
|
@@ -266,11 +323,11 @@ test("a fault while preparing an existing segment is named, not turned into a wa
|
|
|
266
323
|
};
|
|
267
324
|
const { manager, dirPath } = await managerWithReadySegment({ segmentFormat: broken });
|
|
268
325
|
t.after(async () => {
|
|
269
|
-
await manager.disposeAll();
|
|
326
|
+
await manager.lifecycle.disposeAll();
|
|
270
327
|
await rm(dirPath, { recursive: true, force: true });
|
|
271
328
|
});
|
|
272
329
|
|
|
273
|
-
const result = await manager.getFileStream(SESSION_ID, "segment-00000.mp4"
|
|
330
|
+
const result = await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
274
331
|
|
|
275
332
|
assert.equal(
|
|
276
333
|
result.kind,
|
|
@@ -283,7 +340,7 @@ test("a fault while preparing an existing segment is named, not turned into a wa
|
|
|
283
340
|
test("a run's FIRST segment is served once the encoder has passed it, without waiting for a next one", async (t) => {
|
|
284
341
|
const { manager, dirPath } = await managerWithReadySegment();
|
|
285
342
|
t.after(async () => {
|
|
286
|
-
await manager.disposeAll();
|
|
343
|
+
await manager.lifecycle.disposeAll();
|
|
287
344
|
await rm(dirPath, { recursive: true, force: true });
|
|
288
345
|
});
|
|
289
346
|
// The shape a resume takes: a run begun mid-file, so its first segment has no
|
|
@@ -297,7 +354,7 @@ test("a run's FIRST segment is served once the encoder has passed it, without wa
|
|
|
297
354
|
// encoder's reported position — a different question, where it has read to
|
|
298
355
|
// rather than what it has closed.
|
|
299
356
|
|
|
300
|
-
const result = await manager.getFileStream(SESSION_ID, "segment-00000.mp4"
|
|
357
|
+
const result = await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
301
358
|
|
|
302
359
|
assert.equal(
|
|
303
360
|
result.kind,
|
|
@@ -309,16 +366,17 @@ test("a run's FIRST segment is served once the encoder has passed it, without wa
|
|
|
309
366
|
test("a segment an earlier run made is served by whatever run is going now", async (t) => {
|
|
310
367
|
const { manager, session, dirPath } = await managerWithReadySegment();
|
|
311
368
|
t.after(async () => {
|
|
312
|
-
await manager.disposeAll();
|
|
369
|
+
await manager.lifecycle.disposeAll();
|
|
313
370
|
await rm(dirPath, { recursive: true, force: true });
|
|
314
371
|
});
|
|
315
372
|
// Every run of an output writes into that output's own directory — there is
|
|
316
373
|
// one, and runs are kept apart by their stretches instead. So a viewer who
|
|
317
374
|
// seeks back is served what an earlier run made, with no restart and no
|
|
318
375
|
// search: the run going now began at #40 and #0 is simply there.
|
|
319
|
-
startRunOn(session, { from: 40, usesExplicitCuts: true });
|
|
376
|
+
const run = startRunOn(session, { from: 40, usesExplicitCuts: true });
|
|
377
|
+
manager.encodeOrchestrator.adopt(OUTPUT_KEY, run);
|
|
320
378
|
|
|
321
|
-
const result = await manager.getFileStream(SESSION_ID, "segment-00000.mp4"
|
|
379
|
+
const result = await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
322
380
|
|
|
323
381
|
assert.equal(result.kind, "file", "what any run of this output made is what this output holds");
|
|
324
382
|
const chunks = [];
|
|
@@ -329,9 +387,9 @@ test("a segment an earlier run made is served by whatever run is going now", asy
|
|
|
329
387
|
});
|
|
330
388
|
|
|
331
389
|
test("serving a run's own segment moves the run out of STARTING", async (t) => {
|
|
332
|
-
const { manager, session, dirPath } = await managerWithReadySegment();
|
|
390
|
+
const { manager, session, dirPath } = await managerWithReadySegment({ withRun: false });
|
|
333
391
|
t.after(async () => {
|
|
334
|
-
await manager.disposeAll();
|
|
392
|
+
await manager.lifecycle.disposeAll();
|
|
335
393
|
await rm(dirPath, { recursive: true, force: true });
|
|
336
394
|
});
|
|
337
395
|
// The table lives in `encode/encode-run-state.js` and is tested there as a graph.
|
|
@@ -341,17 +399,17 @@ test("serving a run's own segment moves the run out of STARTING", async (t) => {
|
|
|
341
399
|
// A run that has not yet made anything: starting. The fixture's own run is
|
|
342
400
|
// let go first, because this test is about ONE run and what serving does to
|
|
343
401
|
// it.
|
|
344
|
-
session.runs.clear();
|
|
345
402
|
// ITS OWN PIECE, AND FILM STILL TO MAKE BEHIND IT. A run standing on material
|
|
346
403
|
// that already exists is moved forward — correctly — so a fixture that wants a
|
|
347
404
|
// live run has to put it where the work is. #1 is what it is making; #2..#4
|
|
348
405
|
// are unmade, so it is wanted; and #1 stands under its served name, which is
|
|
349
406
|
// what makes the piece servable at all.
|
|
350
407
|
const run = startRunOn(session, { from: 1, producing: false, usesExplicitCuts: true, speedX: 2 });
|
|
408
|
+
manager.encodeOrchestrator.adopt(OUTPUT_KEY, run);
|
|
351
409
|
assert.equal(run.state, ENCODE_RUN_STATE.STARTING);
|
|
352
410
|
assert.deepEqual(run.produced, [], "it has made nothing yet");
|
|
353
411
|
|
|
354
|
-
await manager.getFileStream(SESSION_ID, "segment-00001.mp4"
|
|
412
|
+
await manager.serving.getFileStream(SESSION_ID, "segment-00001.mp4");
|
|
355
413
|
|
|
356
414
|
// ASKED OF WHAT THE SERVE WROTE, not of the state that happens to follow it.
|
|
357
415
|
// The plan owns a run's life and may end it in the same turn for reasons of
|
|
@@ -364,9 +422,9 @@ test("serving a run's own segment moves the run out of STARTING", async (t) => {
|
|
|
364
422
|
});
|
|
365
423
|
|
|
366
424
|
test("a segment behind a run's own start does not claim that run has produced", async (t) => {
|
|
367
|
-
const { manager, session, dirPath } = await managerWithReadySegment();
|
|
425
|
+
const { manager, session, dirPath } = await managerWithReadySegment({ withRun: false });
|
|
368
426
|
t.after(async () => {
|
|
369
|
-
await manager.disposeAll();
|
|
427
|
+
await manager.lifecycle.disposeAll();
|
|
370
428
|
await rm(dirPath, { recursive: true, force: true });
|
|
371
429
|
});
|
|
372
430
|
// What moves a run out of starting is ITS OWN first segment. An earlier run's
|
|
@@ -374,11 +432,44 @@ test("a segment behind a run's own start does not claim that run has produced",
|
|
|
374
432
|
// lies ahead of the new run's beginning — so the question cannot be asked of
|
|
375
433
|
// the number alone, and a run believed to be producing is one the look-ahead
|
|
376
434
|
// may suspend and the seek path may wave through as "already covered".
|
|
377
|
-
session.runs.clear();
|
|
378
435
|
const run = startRunOn(session, { from: 5, producing: false, usesExplicitCuts: true, speedX: 2 });
|
|
436
|
+
manager.encodeOrchestrator.adopt(OUTPUT_KEY, run);
|
|
379
437
|
|
|
380
|
-
await manager.getFileStream(SESSION_ID, "segment-00000.mp4"
|
|
438
|
+
await manager.serving.getFileStream(SESSION_ID, "segment-00000.mp4");
|
|
381
439
|
|
|
382
440
|
assert.deepEqual(run.produced, [], "#0 is somebody else's work and says nothing about this run");
|
|
383
441
|
assert.equal(run.head, 5, "which is still where it began");
|
|
384
442
|
});
|
|
443
|
+
|
|
444
|
+
test("a request behind the run that nobody is coming for is answered absent, not held", async (t) => {
|
|
445
|
+
// A HOLD IS ONLY RIGHT WHILE SOMETHING IS COMING. Field 2026-08-15: hls.js
|
|
446
|
+
// asked a freshly prepared track for segment #0 while the run stood at #354,
|
|
447
|
+
// the request was held for the whole minute, and only when it failed did the
|
|
448
|
+
// player ask for the segment it actually needed — 63 s of spinner after a
|
|
449
|
+
// track that had been ready in 7. Answered absent, the player moves on at
|
|
450
|
+
// once.
|
|
451
|
+
//
|
|
452
|
+
// Whether anybody is coming is the encoding's answer, from the map it was
|
|
453
|
+
// given. An EMPTY map is not that answer: a session created a moment ago has
|
|
454
|
+
// no map yet, and read as "nobody is coming" every request behind the run
|
|
455
|
+
// would be refused for as long as that lasted.
|
|
456
|
+
const { manager, session, dirPath } = await managerWithReadySegment({ withRun: false });
|
|
457
|
+
t.after(async () => {
|
|
458
|
+
await manager.lifecycle.disposeAll();
|
|
459
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
460
|
+
});
|
|
461
|
+
const run = startRunOn(session, { from: 3, producing: true, usesExplicitCuts: true, speedX: 2 });
|
|
462
|
+
manager.encodeOrchestrator.adopt(OUTPUT_KEY, run);
|
|
463
|
+
|
|
464
|
+
// No map yet: the request is HELD, because nothing has said otherwise.
|
|
465
|
+
const held = await manager.serving.getFileStream(SESSION_ID, "segment-00002.mp4");
|
|
466
|
+
assert.notEqual(held.kind, "not-found", "a session with no map waits, it does not refuse");
|
|
467
|
+
|
|
468
|
+
// Now the map says where the viewers are, and #2 is not in it.
|
|
469
|
+
manager.encodeOrchestrator.notePriorityMap(OUTPUT_KEY, [
|
|
470
|
+
{ from: 3, to: 4, priority: 100, withinSeconds: 0 }
|
|
471
|
+
]);
|
|
472
|
+
const refused = await manager.serving.getFileStream(SESSION_ID, "segment-00002.mp4");
|
|
473
|
+
|
|
474
|
+
assert.equal(refused.kind, "not-found");
|
|
475
|
+
});
|
|
@@ -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 { SegmentStore } from "../services/
|
|
15
|
+
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
16
16
|
|
|
17
17
|
const SEGMENT = 1024;
|
|
18
18
|
|
|
@@ -213,19 +213,34 @@ test("told nothing about viewers, it falls back to the oldest directory", async
|
|
|
213
213
|
}
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
test("a clean exit leaves nothing of ours,
|
|
216
|
+
test("a clean exit leaves nothing of ours, and nothing of anybody else's either", async () => {
|
|
217
217
|
const { store, root } = await makeStore();
|
|
218
218
|
try {
|
|
219
219
|
await fill(store, "one", [0, 1, 2]);
|
|
220
220
|
await fill(store, "two", [0, 1]);
|
|
221
|
-
// A directory this
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
// A directory this store never adopted. It is either an orphan of a killed
|
|
222
|
+
// process or ANOTHER PROXY'S LIVE OUTPUT, and from here the two are
|
|
223
|
+
// indistinguishable — the root is a fixed path on the machine, not this
|
|
224
|
+
// process's property. So it is left alone: an orphan gets an owner at the
|
|
225
|
+
// next startup sweep and goes with the exit after that, while a live output
|
|
226
|
+
// of another process must not be taken from under it.
|
|
227
|
+
//
|
|
228
|
+
// What this replaced removed the ROOT, recursively, which took both.
|
|
229
|
+
// Measured 2026-09-14 under `node --test`, where a second process is
|
|
230
|
+
// ordinary: 2 to 4 checks of a neighbouring file failed per run, never the
|
|
231
|
+
// same ones, reporting segments missing that they had just written.
|
|
232
|
+
await fs.mkdir(path.join(root, "not-ours"), { recursive: true });
|
|
233
|
+
await fs.writeFile(path.join(root, "not-ours", "segment-00000.mp4"), Buffer.alloc(SEGMENT));
|
|
225
234
|
|
|
226
235
|
assert.equal(store.dropAll("the proxy is shutting down"), 2);
|
|
227
236
|
|
|
228
|
-
|
|
237
|
+
assert.deepEqual(await heldNumbers(store, "one"), [], "what this store made is gone");
|
|
238
|
+
assert.deepEqual(await heldNumbers(store, "two"), []);
|
|
239
|
+
assert.ok((await fs.stat(root)).isDirectory(), "the root is the machine's, and it stays");
|
|
240
|
+
assert.ok(
|
|
241
|
+
(await fs.stat(path.join(root, "not-ours", "segment-00000.mp4"))).size > 0,
|
|
242
|
+
"a directory this store never adopted is not its to remove"
|
|
243
|
+
);
|
|
229
244
|
} finally {
|
|
230
245
|
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
231
246
|
}
|