@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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The rule that decides whether one output's pieces may be played under
|
|
3
|
+
* another's header — and it is written BEFORE the experiment that will use it.
|
|
4
|
+
*
|
|
5
|
+
* The order matters and was a review finding: a criterion derived from the
|
|
6
|
+
* differences an experiment happened to produce proves only that those
|
|
7
|
+
* differences were handled. This one compares the whole header, names every
|
|
8
|
+
* difference by where it sits, and refuses anything no listed exception covers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { INIT_DIFFERENCE_EXCEPTIONS, compareInits, initLeaves } from "../services/encode/segment-formats/init-compat.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One ISO base media box.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} type
|
|
19
|
+
* @param {...(Buffer | number[])} parts
|
|
20
|
+
* @returns {Buffer}
|
|
21
|
+
*/
|
|
22
|
+
function box(type, ...parts) {
|
|
23
|
+
const body = Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part))));
|
|
24
|
+
const header = Buffer.alloc(8);
|
|
25
|
+
header.writeUInt32BE(body.length + 8, 0);
|
|
26
|
+
header.write(type, 4, "latin1");
|
|
27
|
+
return Buffer.concat([header, body]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A version-0 `mdhd`: version+flags, creation, modification, timescale,
|
|
32
|
+
* duration, language, quality.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} timescale
|
|
35
|
+
* @returns {Buffer}
|
|
36
|
+
*/
|
|
37
|
+
function mdhd(timescale) {
|
|
38
|
+
const body = Buffer.alloc(24);
|
|
39
|
+
body.writeUInt32BE(timescale, 12);
|
|
40
|
+
return box("mdhd", body);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A `btrt`: decoding buffer size, maximum rate, average rate.
|
|
45
|
+
*
|
|
46
|
+
* @param {number} maxBitrate
|
|
47
|
+
* @returns {Buffer}
|
|
48
|
+
*/
|
|
49
|
+
function btrt(maxBitrate) {
|
|
50
|
+
const body = Buffer.alloc(12);
|
|
51
|
+
body.writeUInt32BE(1875072, 0);
|
|
52
|
+
body.writeUInt32BE(maxBitrate, 4);
|
|
53
|
+
body.writeUInt32BE(maxBitrate, 8);
|
|
54
|
+
return box("btrt", body);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A `stsd` carrying one visual sample entry: its 78-byte preamble, then the
|
|
59
|
+
* codec configuration, then optionally a bit rate declaration.
|
|
60
|
+
*
|
|
61
|
+
* @param {{ entryType?: string, config?: number[], maxBitrate?: number | null, preamble?: number }} [how]
|
|
62
|
+
* @returns {Buffer}
|
|
63
|
+
*/
|
|
64
|
+
function stsd({ entryType = "avc1", config = [1, 100, 0, 31], maxBitrate = null, preamble = 78 } = {}) {
|
|
65
|
+
const versionAndCount = Buffer.alloc(8);
|
|
66
|
+
versionAndCount.writeUInt32BE(1, 4);
|
|
67
|
+
const children = [box("avcC", config)];
|
|
68
|
+
if (maxBitrate !== null) {
|
|
69
|
+
children.push(btrt(maxBitrate));
|
|
70
|
+
}
|
|
71
|
+
const entry = box(entryType, Buffer.alloc(preamble), ...children);
|
|
72
|
+
return box("stsd", versionAndCount, entry);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A whole init segment, shaped as ffmpeg writes one.
|
|
77
|
+
*
|
|
78
|
+
* @param {{ timescale?: number, tracks?: number } & Parameters<typeof stsd>[0]} [how]
|
|
79
|
+
* @returns {Buffer}
|
|
80
|
+
*/
|
|
81
|
+
function init({ timescale = 90000, tracks = 1, ...entry } = {}) {
|
|
82
|
+
const trak = (index) =>
|
|
83
|
+
box(
|
|
84
|
+
"trak",
|
|
85
|
+
box("tkhd", Buffer.alloc(84, index)),
|
|
86
|
+
box("mdia", mdhd(timescale), box("hdlr", Buffer.alloc(32)), box("minf", box("stbl", stsd(entry))))
|
|
87
|
+
);
|
|
88
|
+
const traks = [];
|
|
89
|
+
for (let index = 0; index < tracks; index += 1) {
|
|
90
|
+
traks.push(trak(index));
|
|
91
|
+
}
|
|
92
|
+
return Buffer.concat([
|
|
93
|
+
box("ftyp", Buffer.from("isom")),
|
|
94
|
+
box("moov", box("mvhd", Buffer.alloc(100)), ...traks, box("mvex", box("trex", Buffer.alloc(24))))
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ENTRY = "moov[0]/trak[0]/mdia[0]/minf[0]/stbl[0]/stsd[0]/avc1[0]";
|
|
99
|
+
|
|
100
|
+
test("every exception carries the reason it is one", () => {
|
|
101
|
+
for (const exception of INIT_DIFFERENCE_EXCEPTIONS) {
|
|
102
|
+
assert.ok(exception.why && exception.why.length > 40, `no reason given for ${JSON.stringify(exception)}`);
|
|
103
|
+
assert.ok(
|
|
104
|
+
exception.path !== undefined || exception.sampleEntryBox !== undefined,
|
|
105
|
+
"an exception says WHAT it covers"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("every leaf of the header is named by whose child it is, inside a sample entry too", () => {
|
|
111
|
+
const paths = [...initLeaves(init({ tracks: 2, maxBitrate: 3_000_000 })).keys()];
|
|
112
|
+
|
|
113
|
+
assert.ok(paths.includes("moov[0]/trak[0]/mdia[0]/mdhd[0]"), paths.join(" "));
|
|
114
|
+
assert.ok(paths.includes("moov[0]/trak[1]/mdia[0]/mdhd[0]"), "the second track is its own path");
|
|
115
|
+
assert.ok(paths.includes("moov[0]/mvex[0]/trex[0]"));
|
|
116
|
+
assert.ok(
|
|
117
|
+
paths.includes(`${ENTRY}/avcC[0]`),
|
|
118
|
+
"the codec configuration is a leaf of its own, so it can refuse on its own"
|
|
119
|
+
);
|
|
120
|
+
assert.ok(paths.includes(`${ENTRY}/btrt[0]`), "and so is the bit rate declaration");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("one header serves its own pieces", () => {
|
|
124
|
+
const answer = compareInits(init(), init());
|
|
125
|
+
|
|
126
|
+
assert.equal(answer.compatible, true);
|
|
127
|
+
assert.deepEqual(answer.differences, []);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("a bit rate declaration is the one difference that does not refuse", () => {
|
|
131
|
+
const answer = compareInits(init({ maxBitrate: 3_000_000 }), init({ maxBitrate: 1_200_000 }));
|
|
132
|
+
|
|
133
|
+
assert.equal(answer.compatible, true, answer.differences.join(" | "));
|
|
134
|
+
assert.deepEqual(answer.refusedPaths, []);
|
|
135
|
+
assert.ok(
|
|
136
|
+
answer.differences.some((line) => line.includes("max 3000000") && line.includes("max 1200000")),
|
|
137
|
+
`the difference is still reported, with its numbers: ${answer.differences.join(" | ")}`
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("allowing the bit rate does not allow the parameter sets beside it", () => {
|
|
142
|
+
const answer = compareInits(
|
|
143
|
+
init({ maxBitrate: 3_000_000, config: [1, 100, 0, 31] }),
|
|
144
|
+
init({ maxBitrate: 1_200_000, config: [1, 100, 0, 30] })
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
assert.equal(answer.compatible, false);
|
|
148
|
+
assert.deepEqual(answer.refusedPaths, [`${ENTRY}/avcC[0]`]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("the bit rate exception is scoped to a sample entry", () => {
|
|
152
|
+
const strayLeft = box("moov", box("trak", box("mdia", box("minf", box("stbl", btrt(3_000_000))))));
|
|
153
|
+
const strayRight = box("moov", box("trak", box("mdia", box("minf", box("stbl", btrt(1_200_000))))));
|
|
154
|
+
|
|
155
|
+
const answer = compareInits(strayLeft, strayRight);
|
|
156
|
+
|
|
157
|
+
assert.equal(answer.compatible, false, "a btrt that is nobody's sample entry is not the box the reason is about");
|
|
158
|
+
assert.deepEqual(answer.refusedPaths, ["moov[0]/trak[0]/mdia[0]/minf[0]/stbl[0]/btrt[0]"]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a sample entry whose children do not tile is compared whole", () => {
|
|
162
|
+
const answer = compareInits(init({ preamble: 8, config: [1, 100, 0, 31] }), init({ preamble: 8, config: [1, 100, 0, 30] }));
|
|
163
|
+
|
|
164
|
+
assert.equal(answer.compatible, false);
|
|
165
|
+
assert.deepEqual(
|
|
166
|
+
answer.refusedPaths,
|
|
167
|
+
[ENTRY],
|
|
168
|
+
"the descent is checked, and where it does not hold the whole entry refuses"
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a different media timescale is refused, and said in those words", () => {
|
|
173
|
+
const answer = compareInits(init({ timescale: 90000 }), init({ timescale: 48000 }));
|
|
174
|
+
|
|
175
|
+
assert.equal(answer.compatible, false);
|
|
176
|
+
assert.ok(
|
|
177
|
+
answer.differences.some((line) => line.includes("timescale 90000 against 48000")),
|
|
178
|
+
answer.differences.join(" | ")
|
|
179
|
+
);
|
|
180
|
+
assert.deepEqual(answer.refusedPaths, ["moov[0]/trak[0]/mdia[0]/mdhd[0]"]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a track present in one header and not the other is refused", () => {
|
|
184
|
+
const answer = compareInits(init({ tracks: 1 }), init({ tracks: 2 }));
|
|
185
|
+
|
|
186
|
+
assert.equal(answer.compatible, false);
|
|
187
|
+
assert.ok(
|
|
188
|
+
answer.differences.some((line) => line.includes("trak[1]") && line.includes("absent from the first")),
|
|
189
|
+
answer.differences.join(" | ")
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("an exact-path exception decides by path", () => {
|
|
194
|
+
const served = init({ timescale: 90000 });
|
|
195
|
+
const made = init({ timescale: 48000 });
|
|
196
|
+
|
|
197
|
+
const refused = compareInits(served, made);
|
|
198
|
+
const permitted = compareInits(served, made, {
|
|
199
|
+
exceptions: [{ path: "moov[0]/trak[0]/mdia[0]/mdhd[0]", why: "for this check only" }]
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
assert.equal(refused.compatible, false);
|
|
203
|
+
assert.equal(permitted.compatible, true, "the exception list is what decides, and it is asked by path");
|
|
204
|
+
assert.equal(permitted.differences.length, 1, "a permitted difference is still reported");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("allowing one path does not allow the same box of another track", () => {
|
|
208
|
+
const answer = compareInits(init({ tracks: 2, timescale: 90000 }), init({ tracks: 2, timescale: 48000 }), {
|
|
209
|
+
exceptions: [{ path: "moov[0]/trak[0]/mdia[0]/mdhd[0]", why: "for this check only" }]
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
assert.equal(answer.compatible, false);
|
|
213
|
+
assert.deepEqual(answer.refusedPaths, ["moov[0]/trak[1]/mdia[0]/mdhd[0]"]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("a missing header is never compatible", () => {
|
|
217
|
+
assert.equal(compareInits(init(), Buffer.alloc(0)).compatible, false);
|
|
218
|
+
assert.equal(compareInits(Buffer.alloc(0), init()).compatible, false);
|
|
219
|
+
});
|
|
@@ -20,7 +20,7 @@ import { EventEmitter } from "node:events";
|
|
|
20
20
|
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
21
21
|
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
22
22
|
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
23
|
-
import { EncodeOrchestrator } from "../services/
|
|
23
|
+
import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
|
|
24
24
|
|
|
25
25
|
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
26
26
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import test from "node:test";
|
|
16
16
|
import assert from "node:assert/strict";
|
|
17
|
-
import { isInputUnavailable } from "../services/
|
|
17
|
+
import { isInputUnavailable } from "../services/encode/EncodeRuns.js";
|
|
18
18
|
|
|
19
19
|
test("the messages the field produced when data went away are all temporary", () => {
|
|
20
20
|
for (const message of [
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import assert from "node:assert/strict";
|
|
14
|
-
import { IDLE_KEEP_MS } from "../services/
|
|
15
|
-
import { Returns } from "../services/
|
|
14
|
+
import { IDLE_KEEP_MS } from "../services/storage/keep.js";
|
|
15
|
+
import { Returns } from "../services/storage/returns.js";
|
|
16
16
|
|
|
17
17
|
const MINUTE = 60 * 1000;
|
|
18
18
|
|
|
@@ -38,8 +38,8 @@ test("one number, and both kinds of material read it", async () => {
|
|
|
38
38
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
39
39
|
const read = (relative) => readFileSync(path.join(here, "..", relative), "utf8");
|
|
40
40
|
|
|
41
|
-
assert.match(read("services/torrent-pool.js"), /TORRENT_IDLE_TTL_MS = IDLE_KEEP_MS/);
|
|
42
|
-
assert.match(read("services/
|
|
41
|
+
assert.match(read("services/torrent/torrent-pool.js"), /TORRENT_IDLE_TTL_MS = IDLE_KEEP_MS/);
|
|
42
|
+
assert.match(read("services/server/OutputLifecycle.js"), /SEGMENT_STORE_IDLE_MS = IDLE_KEEP_MS/);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
test("a session opened on material still held is a return, and its age is kept", () => {
|
|
@@ -11,93 +11,156 @@
|
|
|
11
11
|
* states where it truly begins, and it is already read whole in order to be
|
|
12
12
|
* stamped. Only boundaries that were actually produced are counted — the parts
|
|
13
13
|
* somebody watched.
|
|
14
|
+
*
|
|
15
|
+
* Counted ON THE FILE'S OWN TABLE, because that is whose fact it is: the same
|
|
16
|
+
* finding for every quality step of the film and for every later session of it.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import test from "node:test";
|
|
17
20
|
import assert from "node:assert/strict";
|
|
18
|
-
import { SourceFile } from "../services/
|
|
19
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
20
|
-
import {
|
|
21
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
22
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
23
|
+
import { KeyframeTable } from "../services/media/container/KeyframeTable.js";
|
|
24
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
25
|
+
|
|
26
|
+
const TOLERANCE = 0.25;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A table naming keyframes every four seconds, which is what the grid of a
|
|
30
|
+
* copied picture is built from.
|
|
31
|
+
*
|
|
32
|
+
* @returns {KeyframeTable}
|
|
33
|
+
*/
|
|
34
|
+
function fourSecondKeyframes() {
|
|
35
|
+
return new KeyframeTable().learn({
|
|
36
|
+
times: [0, 4, 8, 12, 16, 20],
|
|
37
|
+
format: "matroska"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
21
40
|
|
|
22
41
|
test("an index that describes its file exactly is reported as such", () => {
|
|
23
|
-
const
|
|
42
|
+
const table = fourSecondKeyframes();
|
|
24
43
|
|
|
25
44
|
for (let index = 0; index < 4; index += 1) {
|
|
26
|
-
|
|
45
|
+
table.witness({ index, trueStart: index * 4, deviationSec: 0, toleranceSec: TOLERANCE });
|
|
27
46
|
}
|
|
28
47
|
|
|
29
|
-
assert.equal(
|
|
30
|
-
assert.equal(
|
|
31
|
-
assert.equal(
|
|
48
|
+
assert.equal(table.evidence.checked, 4);
|
|
49
|
+
assert.equal(table.evidence.disagreed, 0, "nothing disagreed — which is a finding, not silence");
|
|
50
|
+
assert.equal(table.evidence.maxDeviationSec, 0);
|
|
32
51
|
});
|
|
33
52
|
|
|
34
53
|
test("a boundary the index placed wrongly is counted, with how far out it was", () => {
|
|
35
|
-
const
|
|
54
|
+
const table = fourSecondKeyframes();
|
|
36
55
|
|
|
37
|
-
|
|
56
|
+
table.witness({ index: 0, trueStart: 0, deviationSec: 0, toleranceSec: TOLERANCE });
|
|
38
57
|
// The measured shape: the playlist said 157.99 s, the file cut at 153.82 s.
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
table.witness({ index: 2, trueStart: 3.83, deviationSec: 4.17, toleranceSec: TOLERANCE });
|
|
59
|
+
table.witness({ index: 3, trueStart: 12.01, deviationSec: 0.01, toleranceSec: TOLERANCE });
|
|
41
60
|
|
|
42
|
-
assert.equal(
|
|
43
|
-
assert.equal(
|
|
44
|
-
assert.equal(
|
|
61
|
+
assert.equal(table.evidence.checked, 3);
|
|
62
|
+
assert.equal(table.evidence.disagreed, 1);
|
|
63
|
+
assert.equal(table.evidence.firstDisagreementIndex, 2);
|
|
45
64
|
assert.equal(
|
|
46
|
-
|
|
65
|
+
table.evidence.maxDeviationSec,
|
|
47
66
|
4.17,
|
|
48
67
|
"the size of the error is what decides whether a rung can be cut on this grid"
|
|
49
68
|
);
|
|
50
69
|
});
|
|
51
70
|
|
|
52
71
|
test("a deviation within tolerance is not a disagreement, but still shows in the worst case", () => {
|
|
53
|
-
const
|
|
72
|
+
const table = fourSecondKeyframes();
|
|
73
|
+
|
|
74
|
+
table.witness({ index: 0, trueStart: 0.2, deviationSec: 0.2, toleranceSec: TOLERANCE });
|
|
75
|
+
|
|
76
|
+
assert.equal(
|
|
77
|
+
table.evidence.disagreed,
|
|
78
|
+
0,
|
|
79
|
+
"rounding in a container's timestamps is not the index being wrong"
|
|
80
|
+
);
|
|
81
|
+
assert.equal(
|
|
82
|
+
table.evidence.maxDeviationSec,
|
|
83
|
+
0.2,
|
|
84
|
+
"and it is still worth knowing how close to the line it ran"
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a piece that began at ANOTHER time the table names is told apart from one that began nowhere", () => {
|
|
89
|
+
// The discriminator, and it decides which of two opposite faults this is. A
|
|
90
|
+
// piece that began at another keyframe of the same list was not mis-described
|
|
91
|
+
// by the table: the grid was built over a gap in it. One that began where the
|
|
92
|
+
// table names nothing is the table describing times the file does not have.
|
|
93
|
+
// The table answers it itself, because it is the only thing that holds the
|
|
94
|
+
// list — asked of the caller, it was one more fact travelling by hand.
|
|
95
|
+
const table = fourSecondKeyframes();
|
|
54
96
|
|
|
55
|
-
|
|
97
|
+
table.witness({ index: 1, trueStart: 8, deviationSec: 4, toleranceSec: TOLERANCE });
|
|
98
|
+
table.witness({ index: 2, trueStart: 9.7, deviationSec: 1.7, toleranceSec: TOLERANCE });
|
|
56
99
|
|
|
57
|
-
assert.equal(
|
|
58
|
-
assert.equal(
|
|
100
|
+
assert.equal(table.evidence.disagreed, 2);
|
|
101
|
+
assert.equal(table.evidence.landedOnAnotherKeyframe, 1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a table that has read nothing claims no evidence about itself", () => {
|
|
105
|
+
// It cannot be wrong about a file it has not described. `readable` is false,
|
|
106
|
+
// every picture of the file is re-encoded, and no copy can witness anything.
|
|
107
|
+
const table = new KeyframeTable().learn({ times: null, format: "mpegts" });
|
|
108
|
+
|
|
109
|
+
assert.equal(table.evidence, null);
|
|
110
|
+
assert.equal(table.names(4), false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a segment requested again is not new evidence", () => {
|
|
114
|
+
const table = fourSecondKeyframes();
|
|
115
|
+
|
|
116
|
+
for (let repeat = 0; repeat < 3; repeat += 1) {
|
|
117
|
+
table.witness({ index: 1, trueStart: 4.9, deviationSec: 0.9, toleranceSec: TOLERANCE });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
assert.equal(table.evidence.checked, 1, "a repeat request is the same boundary, counted once");
|
|
121
|
+
assert.equal(table.evidence.disagreed, 1);
|
|
59
122
|
});
|
|
60
123
|
|
|
61
124
|
test("a boundary the index got wrong is replaced by the time the file really has", async (t) => {
|
|
62
|
-
const {
|
|
63
|
-
const manager =
|
|
125
|
+
const { wireOutputs } = await import("../services/server/wire-outputs.js");
|
|
126
|
+
const manager = wireOutputs({
|
|
64
127
|
enabled: true,
|
|
65
128
|
ffmpegBin: "ffmpeg",
|
|
66
129
|
localBindHost: "127.0.0.1",
|
|
67
130
|
localPort: 9090
|
|
68
131
|
});
|
|
69
|
-
t.after(() => manager.disposeAll());
|
|
132
|
+
t.after(() => manager.lifecycle.disposeAll());
|
|
70
133
|
// ONE table for the film, held by both. It used to be a copy each, kept in
|
|
71
134
|
// step by writing the correction into every member — which is what the shared
|
|
72
135
|
// table replaces, and what drifted in the field.
|
|
73
136
|
const boundaries = [0, 10, 20, 30, 40];
|
|
74
137
|
const base = {
|
|
75
|
-
id: "
|
|
138
|
+
id: "aaaaaaaa11112222",
|
|
139
|
+
spec: outputSpec({ transcodeVideo: false }),
|
|
76
140
|
fileName: "film.mkv",
|
|
77
141
|
state: "ready",
|
|
78
|
-
transcodeVideo: false,
|
|
79
142
|
timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
|
|
80
143
|
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
|
|
81
144
|
segmentFormat: { segmentFileName: (index) => `segment-${index}.mp4` }
|
|
82
145
|
};
|
|
83
146
|
const rung = {
|
|
84
|
-
id: "
|
|
147
|
+
id: "bbbbbbbb11112222",
|
|
148
|
+
spec: outputSpec({ transcodeVideo: true, height: 540 }),
|
|
85
149
|
fileName: "film.mkv",
|
|
86
150
|
state: "ready",
|
|
87
|
-
transcodeVideo: true,
|
|
88
151
|
timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
|
|
89
152
|
// A step of the picture: the same file, and made as a step.
|
|
90
153
|
file: base.file,
|
|
91
154
|
variantHeight: 540,
|
|
92
155
|
isStep: true
|
|
93
156
|
};
|
|
94
|
-
|
|
95
|
-
manager.
|
|
96
|
-
manager.
|
|
157
|
+
manager.outputs.set(base.id, base);
|
|
158
|
+
manager.outputs.set(rung.id, rung);
|
|
159
|
+
manager.outputs.markStep(rung);
|
|
97
160
|
|
|
98
161
|
// The copy produced segment #2, and it really begins at 17.4 s — the index
|
|
99
162
|
// said 20. This is the shape reproduced from the field on 2026-08-12.
|
|
100
|
-
manager.correctBoundaryFromSegment(base, 2, 17.4);
|
|
163
|
+
manager.outputTimes.correctBoundaryFromSegment(base, 2, 17.4);
|
|
101
164
|
|
|
102
165
|
assert.equal(
|
|
103
166
|
base.timeline.boundaries[2],
|
|
@@ -118,19 +181,8 @@ test("a boundary the index got wrong is replaced by the time the file really has
|
|
|
118
181
|
// A reading that cannot be a boundary is not evidence about one. It comes
|
|
119
182
|
// from a run that started somewhere else, and applying it would leave the
|
|
120
183
|
// table describing nothing.
|
|
121
|
-
manager.correctBoundaryFromSegment(base, 2, 35);
|
|
122
|
-
manager.correctBoundaryFromSegment(base, 2, 5);
|
|
123
|
-
manager.correctBoundaryFromSegment(base, 0, 3);
|
|
184
|
+
manager.outputTimes.correctBoundaryFromSegment(base, 2, 35);
|
|
185
|
+
manager.outputTimes.correctBoundaryFromSegment(base, 2, 5);
|
|
186
|
+
manager.outputTimes.correctBoundaryFromSegment(base, 0, 3);
|
|
124
187
|
assert.deepEqual(base.timeline.boundaries, [0, 10, 17.4, 30, 40], "out-of-order readings are refused");
|
|
125
188
|
});
|
|
126
|
-
|
|
127
|
-
test("a segment requested again is not new evidence", () => {
|
|
128
|
-
const check = newIndexCheck();
|
|
129
|
-
|
|
130
|
-
noteIndexDeviation(check, 1, 0.9);
|
|
131
|
-
noteIndexDeviation(check, 1, 0.9);
|
|
132
|
-
noteIndexDeviation(check, 1, 0.9);
|
|
133
|
-
|
|
134
|
-
assert.equal(check.checked, 1, "a repeat request is the same boundary, counted once");
|
|
135
|
-
assert.equal(check.disagreed, 1);
|
|
136
|
-
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file How long
|
|
2
|
+
* @file How long anybody waits for the file's keyframe table.
|
|
3
3
|
*
|
|
4
4
|
* With the table a picture is copied; without it the whole picture is
|
|
5
5
|
* re-encoded, which on a weak host is the difference between almost free and
|
|
@@ -16,66 +16,62 @@
|
|
|
16
16
|
|
|
17
17
|
import test from "node:test";
|
|
18
18
|
import assert from "node:assert/strict";
|
|
19
|
-
import {
|
|
19
|
+
import { KeyframeTables } from "../services/media/KeyframeTables.js";
|
|
20
|
+
|
|
21
|
+
const QUIET = { info: () => {}, warn: () => {} };
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* @param {number} budgetMs
|
|
23
|
-
* @
|
|
25
|
+
* @param {(params: object) => Promise<object | null>} readTable
|
|
26
|
+
* @returns {KeyframeTables}
|
|
24
27
|
*/
|
|
25
|
-
function
|
|
26
|
-
return new
|
|
27
|
-
enabled: true,
|
|
28
|
-
ffmpegBin: "ffmpeg",
|
|
29
|
-
localBindHost: "127.0.0.1",
|
|
30
|
-
localPort: 9090,
|
|
31
|
-
keyframeTableBudgetMs: budgetMs
|
|
32
|
-
});
|
|
28
|
+
function tables(budgetMs, readTable) {
|
|
29
|
+
return new KeyframeTables({ readTable, budgetMs, logger: QUIET });
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
const FILE = { sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" };
|
|
36
33
|
|
|
37
34
|
test("a table that arrives inside the budget is the answer", async () => {
|
|
38
|
-
const
|
|
39
|
-
sessions.getContainerKeyframes = async () => ({ times: [0, 4, 8], tolerance: 0, format: "matroska" });
|
|
35
|
+
const keyframes = tables(1_000, async () => ({ times: [0, 4, 8], tolerance: 0, format: "matroska" }));
|
|
40
36
|
|
|
41
|
-
const
|
|
37
|
+
const { table, arrived } = await keyframes.within(FILE);
|
|
42
38
|
|
|
43
|
-
assert.deepEqual(
|
|
44
|
-
assert.equal(
|
|
39
|
+
assert.deepEqual(table.times, [0, 4, 8]);
|
|
40
|
+
assert.equal(arrived, true, "the file has answered, so the picture may be copied");
|
|
41
|
+
assert.equal(table.readable, true);
|
|
45
42
|
});
|
|
46
43
|
|
|
47
44
|
test("a table that has not arrived gives up on copying rather than on the session", async () => {
|
|
48
|
-
const sessions = manager(60);
|
|
49
45
|
// The bytes it needs are still coming off the swarm. On the field host this
|
|
50
46
|
// is a torrent with one peer: the read was still waiting after two minutes.
|
|
51
|
-
|
|
47
|
+
const keyframes = tables(60, () => new Promise(() => {}));
|
|
52
48
|
|
|
53
49
|
const startedAt = Date.now();
|
|
54
|
-
const
|
|
50
|
+
const { table, arrived } = await keyframes.within(FILE);
|
|
55
51
|
const waited = Date.now() - startedAt;
|
|
56
52
|
|
|
57
|
-
assert.equal(
|
|
53
|
+
assert.equal(table.readable, false, "no table, so this session cannot copy the picture");
|
|
58
54
|
assert.equal(
|
|
59
|
-
|
|
55
|
+
table.answered,
|
|
60
56
|
false,
|
|
61
|
-
"and
|
|
62
|
-
"
|
|
57
|
+
"and the file has NOT answered — an absence recorded as an answer would make a passing " +
|
|
58
|
+
"shortage of bytes look like a property of the bytes"
|
|
63
59
|
);
|
|
60
|
+
assert.equal(arrived, false);
|
|
64
61
|
assert.ok(waited < 2_000, `the wait ended at the bound, not at the read (${waited}ms)`);
|
|
65
62
|
});
|
|
66
63
|
|
|
67
64
|
test("the read goes on after the budget, so the next session of the file gets the copy", async () => {
|
|
68
|
-
const sessions = manager(40);
|
|
69
65
|
let reads = 0;
|
|
70
66
|
let answerLate = null;
|
|
71
|
-
|
|
67
|
+
const keyframes = tables(40, () => {
|
|
72
68
|
reads += 1;
|
|
73
69
|
return new Promise((resolve) => {
|
|
74
70
|
answerLate = resolve;
|
|
75
71
|
});
|
|
76
|
-
};
|
|
72
|
+
});
|
|
77
73
|
|
|
78
|
-
const first = await
|
|
74
|
+
const first = await keyframes.within(FILE);
|
|
79
75
|
assert.equal(first.arrived, false);
|
|
80
76
|
|
|
81
77
|
answerLate({ times: [0, 5, 10], tolerance: 0, format: "matroska" });
|
|
@@ -87,20 +83,19 @@ test("the read goes on after the budget, so the next session of the file gets th
|
|
|
87
83
|
// is the very thing the last assertion here is about.
|
|
88
84
|
await new Promise((resolve) => { setImmediate(resolve); });
|
|
89
85
|
|
|
90
|
-
const second = await
|
|
91
|
-
assert.deepEqual(second.times, [0, 5, 10], "the late answer was kept, not thrown away");
|
|
86
|
+
const second = await keyframes.within(FILE);
|
|
87
|
+
assert.deepEqual(second.table.times, [0, 5, 10], "the late answer was kept, not thrown away");
|
|
92
88
|
assert.equal(second.arrived, true);
|
|
93
89
|
assert.equal(reads, 1, "and it was not read a second time");
|
|
94
90
|
});
|
|
95
91
|
|
|
96
92
|
test("a read that fails is not turned into a bounded wait's silence", async () => {
|
|
97
|
-
const
|
|
98
|
-
sessions.getContainerKeyframes = async () => {
|
|
93
|
+
const keyframes = tables(1_000, async () => {
|
|
99
94
|
throw new Error("the head is not downloaded");
|
|
100
|
-
};
|
|
95
|
+
});
|
|
101
96
|
|
|
102
97
|
await assert.rejects(
|
|
103
|
-
() =>
|
|
98
|
+
() => keyframes.read(FILE),
|
|
104
99
|
/the head is not downloaded/,
|
|
105
100
|
"a read that threw is a different thing from a read that is still running"
|
|
106
101
|
);
|