@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
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The tracks a file declares, read from the file itself on the thread that owns
|
|
3
|
-
* the torrent.
|
|
4
|
-
*
|
|
5
|
-
* This exists because the container layer (`services/container/`) reads bytes,
|
|
6
|
-
* and the bytes live here: the torrent client runs on this worker thread, so the
|
|
7
|
-
* main thread cannot open a read stream on one of its files. The main thread
|
|
8
|
-
* asks over the channel (`Command.CONTAINER_TRACKS`) and gets plain objects
|
|
9
|
-
* back, because a class instance does not survive the boundary as a class.
|
|
10
|
-
*
|
|
11
|
-
* It reads whatever container the file is — the same `ContainerFactory` that
|
|
12
|
-
* serves the picture serves a `.mka` beside it, since a `.mka` IS Matroska and
|
|
13
|
-
* differs only in having no video track. That is the whole reason no new class
|
|
14
|
-
* was needed for a soundtrack shipped as its own file.
|
|
15
|
-
*
|
|
16
|
-
* Unlike the subtitle walk, the reads here DO wait on the swarm. A sidecar file
|
|
17
|
-
* has usually had nothing downloaded at all when this is first asked, and its
|
|
18
|
-
* header is a few hundred kilobytes; the alternative is offering the viewer a
|
|
19
|
-
* soundtrack with nothing known about it.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { containerOrchestrator } from "../orchestrators/ContainerOrchestrator.js";
|
|
23
|
-
import { logger } from "../../utils/logger.js";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* How long one range read may take before it is given up.
|
|
27
|
-
*
|
|
28
|
-
* Not a measurement and nothing is derived from it: it is the point past which a
|
|
29
|
-
* read of a file nobody is playing is presumed lost, so that one stream which
|
|
30
|
-
* never ends cannot hold the answer — and with it the viewer's audio menu — for
|
|
31
|
-
* the rest of the session.
|
|
32
|
-
*/
|
|
33
|
-
const READ_ABANDON_MS = 60_000;
|
|
34
|
-
|
|
35
|
-
/** Bytes of the head a container's track table lives in. */
|
|
36
|
-
const HEAD_BYTES = 256 * 1024;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Tracks already read, by `sourceKey:fileIndex`. Only a non-empty reading is
|
|
40
|
-
* kept: an empty one usually means the header has not arrived yet, and caching
|
|
41
|
-
* that would hide the file's tracks for the life of the process.
|
|
42
|
-
*
|
|
43
|
-
* @type {Map<string, object[]>}
|
|
44
|
-
*/
|
|
45
|
-
const byFile = new Map();
|
|
46
|
-
|
|
47
|
-
/** @type {Map<string, Promise<object[]>>} */
|
|
48
|
-
const inFlight = new Map();
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Read a byte range of a file, fetching what is missing.
|
|
52
|
-
*
|
|
53
|
-
* @param {object} file
|
|
54
|
-
* @param {number} start
|
|
55
|
-
* @param {number} end - Inclusive.
|
|
56
|
-
* @returns {Promise<Buffer | null>}
|
|
57
|
-
*/
|
|
58
|
-
function readFetching(file, start, end) {
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
const chunks = [];
|
|
61
|
-
let stream;
|
|
62
|
-
try {
|
|
63
|
-
stream = file.createReadStream({ start, end });
|
|
64
|
-
} catch {
|
|
65
|
-
resolve(null);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
let settled = false;
|
|
69
|
-
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
70
|
-
let abandon = null;
|
|
71
|
-
const settle = (value) => {
|
|
72
|
-
if (settled) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
settled = true;
|
|
76
|
-
if (abandon !== null) {
|
|
77
|
-
clearTimeout(abandon);
|
|
78
|
-
}
|
|
79
|
-
if (value === null) {
|
|
80
|
-
stream.destroy?.();
|
|
81
|
-
}
|
|
82
|
-
resolve(value);
|
|
83
|
-
};
|
|
84
|
-
abandon = setTimeout(() => {
|
|
85
|
-
logger.info(
|
|
86
|
-
`container-tracks: a read of ${start}-${end} in "${String(file.name).slice(0, 40)}" ` +
|
|
87
|
-
`did not finish in ${READ_ABANDON_MS / 1000}s and was given up`
|
|
88
|
-
);
|
|
89
|
-
settle(null);
|
|
90
|
-
}, READ_ABANDON_MS);
|
|
91
|
-
abandon.unref?.();
|
|
92
|
-
stream.on("data", (chunk) => chunks.push(chunk));
|
|
93
|
-
stream.on("end", () => settle(Buffer.concat(chunks)));
|
|
94
|
-
stream.on("error", () => settle(null));
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* One track as it crosses the thread boundary: the fields the inventory and the
|
|
100
|
-
* master playlist read, and nothing that would not survive being cloned.
|
|
101
|
-
*
|
|
102
|
-
* @param {import("../tracks/index.js").ContainerTrack} track
|
|
103
|
-
* @returns {object}
|
|
104
|
-
*/
|
|
105
|
-
function plainTrack(track) {
|
|
106
|
-
return {
|
|
107
|
-
type: track.type,
|
|
108
|
-
trackNumber: track.trackNumber,
|
|
109
|
-
declaredIndex: track.declaredIndex,
|
|
110
|
-
codecId: track.codecId,
|
|
111
|
-
language: track.resolvedLanguage(),
|
|
112
|
-
languageBcp47: track.languageBcp47,
|
|
113
|
-
name: track.name,
|
|
114
|
-
isEnabled: track.isEnabled,
|
|
115
|
-
isDefault: track.isDefault,
|
|
116
|
-
declaresDefault: track.declaresDefault,
|
|
117
|
-
// Audio-only, per RFC 9559 — absent on other types, and read as false there
|
|
118
|
-
// rather than being placed on the base track where they do not belong.
|
|
119
|
-
isOriginal: track.isOriginal === true,
|
|
120
|
-
isCommentary: track.isCommentary === true,
|
|
121
|
-
isVisualImpaired: track.isVisualImpaired === true,
|
|
122
|
-
channels: Number.isFinite(track.channels) ? track.channels : null,
|
|
123
|
-
samplingFrequency: Number.isFinite(track.samplingFrequency) ? track.samplingFrequency : null
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Every track one file declares, in container order.
|
|
129
|
-
*
|
|
130
|
-
* @param {object} torrent
|
|
131
|
-
* @param {number} fileIndex
|
|
132
|
-
* @param {string} sourceKey
|
|
133
|
-
* @param {{ prefetchEdges?: () => Promise<unknown> }} [options]
|
|
134
|
-
* @returns {Promise<object[]>}
|
|
135
|
-
*/
|
|
136
|
-
export async function containerTracksOf(torrent, fileIndex, sourceKey, options = {}) {
|
|
137
|
-
const key = `${sourceKey}:${fileIndex}`;
|
|
138
|
-
const held = byFile.get(key);
|
|
139
|
-
if (Array.isArray(held)) {
|
|
140
|
-
return held;
|
|
141
|
-
}
|
|
142
|
-
const running = inFlight.get(key);
|
|
143
|
-
if (running) {
|
|
144
|
-
return running;
|
|
145
|
-
}
|
|
146
|
-
const work = readTracks(torrent, fileIndex, sourceKey, options)
|
|
147
|
-
.then((tracks) => {
|
|
148
|
-
if (tracks.length > 0) {
|
|
149
|
-
byFile.set(key, tracks);
|
|
150
|
-
}
|
|
151
|
-
return tracks;
|
|
152
|
-
})
|
|
153
|
-
.finally(() => {
|
|
154
|
-
inFlight.delete(key);
|
|
155
|
-
});
|
|
156
|
-
inFlight.set(key, work);
|
|
157
|
-
return work;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @param {object} torrent
|
|
162
|
-
* @param {number} fileIndex
|
|
163
|
-
* @param {string} sourceKey
|
|
164
|
-
* @param {{ prefetchEdges?: () => Promise<unknown> }} options
|
|
165
|
-
* @returns {Promise<object[]>}
|
|
166
|
-
*/
|
|
167
|
-
async function readTracks(torrent, fileIndex, sourceKey, options) {
|
|
168
|
-
const file = torrent?.files?.[fileIndex];
|
|
169
|
-
if (!file || !Number.isFinite(file.length) || file.length <= 0) {
|
|
170
|
-
return [];
|
|
171
|
-
}
|
|
172
|
-
if (typeof options.prefetchEdges === "function") {
|
|
173
|
-
try {
|
|
174
|
-
await options.prefetchEdges();
|
|
175
|
-
} catch {
|
|
176
|
-
// A prefetch that failed is not a reason to skip the read: the read
|
|
177
|
-
// fetches what it needs itself, only more slowly.
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const readRange = async (start, end) =>
|
|
181
|
-
readFetching(file, start, Math.min(end, file.length - 1));
|
|
182
|
-
try {
|
|
183
|
-
const tracks = await containerOrchestrator.getTracks({
|
|
184
|
-
sourceKey,
|
|
185
|
-
fileIndex,
|
|
186
|
-
readRange,
|
|
187
|
-
fileSize: file.length,
|
|
188
|
-
label: String(file.name ?? "")
|
|
189
|
-
});
|
|
190
|
-
const plain = tracks.map(plainTrack);
|
|
191
|
-
if (plain.length > 0) {
|
|
192
|
-
logger.info(
|
|
193
|
-
`container-tracks: "${String(file.name).slice(0, 40)}" declares ` +
|
|
194
|
-
`${plain.filter((track) => track.type === "video").length} video, ` +
|
|
195
|
-
`${plain.filter((track) => track.type === "audio").length} audio, ` +
|
|
196
|
-
`${plain.filter((track) => track.type === "subtitle").length} subtitle track(s)`
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
return plain;
|
|
200
|
-
} catch (error) {
|
|
201
|
-
logger.warn(
|
|
202
|
-
`container-tracks: "${String(file.name).slice(0, 40)}" could not be read: ${error?.message ?? error}`
|
|
203
|
-
);
|
|
204
|
-
return [];
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Bytes of a file's head worth fetching before its track table is read. */
|
|
209
|
-
export const CONTAINER_HEAD_BYTES = HEAD_BYTES;
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* How much of the file to pull in under the viewer's resume position.
|
|
213
|
-
*
|
|
214
|
-
* One piece of a video torrent is 4-16 MB and a resume lands anywhere inside
|
|
215
|
-
* one, so anything smaller would still leave the encoder waiting for the piece
|
|
216
|
-
* it starts in. Eight megabytes covers that piece and usually the next.
|
|
217
|
-
*/
|
|
218
|
-
const RESUME_REGION_BYTES = 8 * 1024 * 1024;
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Where a position in seconds falls in a file, in bytes.
|
|
222
|
-
*
|
|
223
|
-
* Proportional, and therefore approximate on a variable bitrate — which is what
|
|
224
|
-
* it is for: a prefetch that puts the swarm to work on roughly the right place
|
|
225
|
-
* while the plan and the session are still being built. The encoder's own read
|
|
226
|
-
* asks for the exact bytes a moment later and corrects it.
|
|
227
|
-
*
|
|
228
|
-
* A position past the end is clamped to the end rather than refused: a resume
|
|
229
|
-
* position can outlive the file it was recorded against, and reading the last
|
|
230
|
-
* bytes is harmless where reading past them is an error.
|
|
231
|
-
*
|
|
232
|
-
* @param {number} fileLength
|
|
233
|
-
* @param {number} durationSeconds
|
|
234
|
-
* @param {number} positionSeconds
|
|
235
|
-
* @returns {number}
|
|
236
|
-
*/
|
|
237
|
-
export function resumeByteOffset(fileLength, durationSeconds, positionSeconds) {
|
|
238
|
-
if (!(fileLength > 0) || !(durationSeconds > 0) || !(positionSeconds > 0)) {
|
|
239
|
-
return 0;
|
|
240
|
-
}
|
|
241
|
-
const within = Math.min(positionSeconds, durationSeconds);
|
|
242
|
-
return Math.min(fileLength - 1, Math.floor((fileLength * within) / durationSeconds));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Start fetching the region a viewer is about to resume at.
|
|
247
|
-
*
|
|
248
|
-
* Where that region IS can only be worked out from two numbers the file itself
|
|
249
|
-
* holds — its length and its duration — so this belongs beside the container
|
|
250
|
-
* read rather than in the route: the route knows a position in seconds and
|
|
251
|
-
* nothing else. The conversion is proportional and therefore approximate on a
|
|
252
|
-
* variable bitrate; it is a prefetch, and the encoder's own read corrects it.
|
|
253
|
-
*
|
|
254
|
-
* @param {object} torrent
|
|
255
|
-
* @param {number} fileIndex
|
|
256
|
-
* @param {string} sourceKey
|
|
257
|
-
* @param {number} positionSeconds
|
|
258
|
-
* @param {{ prefetchEdges?: () => Promise<unknown>, fetchRegion?: (start: number, bytes: number) => Promise<unknown> }} options
|
|
259
|
-
* @returns {Promise<boolean>} Whether a region was asked for.
|
|
260
|
-
*/
|
|
261
|
-
export async function warmResumePosition(torrent, fileIndex, sourceKey, positionSeconds, options = {}) {
|
|
262
|
-
const file = torrent?.files?.[fileIndex];
|
|
263
|
-
if (!file || !(positionSeconds > 0) || typeof options.fetchRegion !== "function") {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
const info = await containerMediaInfoOf(torrent, fileIndex, sourceKey, options);
|
|
267
|
-
const duration = info?.durationSeconds;
|
|
268
|
-
if (!Number.isFinite(duration) || duration <= 0) {
|
|
269
|
-
logger.info(
|
|
270
|
-
`warm ${sourceKey.slice(0, 8)}: "${String(file.name).slice(0, 40)}" does not declare its ` +
|
|
271
|
-
"duration, so where the viewer's position falls in it cannot be worked out — " +
|
|
272
|
-
"the region under it is left to the encoder's own read"
|
|
273
|
-
);
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
const at = resumeByteOffset(file.length, duration, positionSeconds);
|
|
277
|
-
logger.info(
|
|
278
|
-
`warm ${sourceKey.slice(0, 8)}: fetching ${(RESUME_REGION_BYTES / (1024 * 1024)).toFixed(0)}MB under the ` +
|
|
279
|
-
`viewer's position ${positionSeconds.toFixed(1)}s of ${duration.toFixed(1)}s, which is ` +
|
|
280
|
-
`${(at / (1024 * 1024)).toFixed(1)}MB into "${String(file.name).slice(0, 40)}"`
|
|
281
|
-
);
|
|
282
|
-
await options.fetchRegion(at, RESUME_REGION_BYTES);
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Where one file's keyframes are, from the container's own table.
|
|
288
|
-
*
|
|
289
|
-
* A property of immutable bytes, like the duration and the track list, and read
|
|
290
|
-
* here for the same reason they are: the container is cached per file, so the
|
|
291
|
-
* table is read ONCE however many sessions ask for it, and two sessions created
|
|
292
|
-
* in the same moment join one read instead of making two. That is the two-viewer
|
|
293
|
-
* case exactly — measured 2026-09-03, two pictures of one file were created 13 ms
|
|
294
|
-
* apart, and each read the table for itself over the proxy's own HTTP.
|
|
295
|
-
*
|
|
296
|
-
* It decides which branch a copy takes: a picture can only be cut where the
|
|
297
|
-
* source already has a keyframe, so a file with no readable index is re-encoded
|
|
298
|
-
* instead. That decision belongs to the FILE, and it is the same for everybody
|
|
299
|
-
* watching it.
|
|
300
|
-
*
|
|
301
|
-
* @param {object} torrent
|
|
302
|
-
* @param {number} fileIndex
|
|
303
|
-
* @param {string} sourceKey
|
|
304
|
-
* @param {{ prefetchEdges?: () => Promise<unknown> }} [options]
|
|
305
|
-
* @returns {Promise<{ times: number[], tolerance: number } | null>}
|
|
306
|
-
*/
|
|
307
|
-
export async function containerKeyframesOf(torrent, fileIndex, sourceKey, options = {}) {
|
|
308
|
-
const file = torrent?.files?.[fileIndex];
|
|
309
|
-
if (!file || !Number.isFinite(file.length) || file.length <= 0) {
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
// Timed apart from the read below, and reported apart from it, because the
|
|
313
|
-
// two are different quantities and reading them as one led to a wrong
|
|
314
|
-
// conclusion on 2026-09-04: this wait is the SWARM delivering the head and
|
|
315
|
-
// tail of the file, and it reached 60 s on a torrent with one peer, while the
|
|
316
|
-
// read that follows it is parsing bytes already in hand and never passed
|
|
317
|
-
// 8.4 s across four containers. A bound belongs on the first, not the second.
|
|
318
|
-
const edgesStartedAt = Date.now();
|
|
319
|
-
if (typeof options.prefetchEdges === "function") {
|
|
320
|
-
try {
|
|
321
|
-
await options.prefetchEdges();
|
|
322
|
-
} catch {
|
|
323
|
-
// A prefetch that failed is not a reason to skip the read: the read
|
|
324
|
-
// fetches what it needs itself, only more slowly.
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
const edgesMs = Date.now() - edgesStartedAt;
|
|
328
|
-
const readRange = async (start, end) =>
|
|
329
|
-
readFetching(file, start, Math.min(end, file.length - 1));
|
|
330
|
-
const startedAt = Date.now();
|
|
331
|
-
const params = {
|
|
332
|
-
sourceKey,
|
|
333
|
-
fileIndex,
|
|
334
|
-
readRange,
|
|
335
|
-
fileSize: file.length,
|
|
336
|
-
label: String(file.name ?? "")
|
|
337
|
-
};
|
|
338
|
-
try {
|
|
339
|
-
const index = await containerOrchestrator.getKeyframeIndex(params);
|
|
340
|
-
// Which container answered, reported whether or not it produced anything:
|
|
341
|
-
// how often an index disagrees with its own file is a question about the
|
|
342
|
-
// CONTAINER, and a measurement that does not say which one cannot answer
|
|
343
|
-
// it. Free — the container is the cached one that has just read the table.
|
|
344
|
-
const format = (await containerOrchestrator.getContainer(params))?.formatName ?? "unrecognised";
|
|
345
|
-
const times = Array.isArray(index?.times) && index.times.length > 0 ? index.times : null;
|
|
346
|
-
logger.info(
|
|
347
|
-
`container-keyframes: "${String(file.name).slice(0, 40)}" ` +
|
|
348
|
-
(times
|
|
349
|
-
? `${times.length} keyframes from the ${format} index in ${Date.now() - startedAt}ms`
|
|
350
|
-
: `has no readable index (${format}, ${Date.now() - startedAt}ms)`) +
|
|
351
|
-
`, after ${edgesMs}ms waiting for the file's edges`
|
|
352
|
-
);
|
|
353
|
-
return {
|
|
354
|
-
times,
|
|
355
|
-
format,
|
|
356
|
-
tolerance: Number.isFinite(index?.tolerance) ? index.tolerance : 0
|
|
357
|
-
};
|
|
358
|
-
} catch (error) {
|
|
359
|
-
logger.warn(
|
|
360
|
-
`container-keyframes: "${String(file.name).slice(0, 40)}" could not be read: ${error?.message ?? error}`
|
|
361
|
-
);
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* What one file declares about itself as a whole.
|
|
368
|
-
*
|
|
369
|
-
* @param {object} torrent
|
|
370
|
-
* @param {number} fileIndex
|
|
371
|
-
* @param {string} sourceKey
|
|
372
|
-
* @param {{ prefetchEdges?: () => Promise<unknown> }} [options]
|
|
373
|
-
* @returns {Promise<import("../container/Container.js").ContainerMediaInfo | null>}
|
|
374
|
-
*/
|
|
375
|
-
export async function containerMediaInfoOf(torrent, fileIndex, sourceKey, options = {}) {
|
|
376
|
-
const file = torrent?.files?.[fileIndex];
|
|
377
|
-
if (!file || !Number.isFinite(file.length) || file.length <= 0) {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
if (typeof options.prefetchEdges === "function") {
|
|
381
|
-
try {
|
|
382
|
-
await options.prefetchEdges();
|
|
383
|
-
} catch {
|
|
384
|
-
// A prefetch that failed is not a reason to skip the read: the read
|
|
385
|
-
// fetches what it needs itself, only more slowly.
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const readRange = async (start, end) =>
|
|
389
|
-
readFetching(file, start, Math.min(end, file.length - 1));
|
|
390
|
-
try {
|
|
391
|
-
const info = await containerOrchestrator.getMediaInfo({
|
|
392
|
-
sourceKey,
|
|
393
|
-
fileIndex,
|
|
394
|
-
readRange,
|
|
395
|
-
fileSize: file.length,
|
|
396
|
-
label: String(file.name ?? "")
|
|
397
|
-
});
|
|
398
|
-
if (info) {
|
|
399
|
-
logger.info(
|
|
400
|
-
`container-info: "${String(file.name).slice(0, 40)}" is ${info.format}, ` +
|
|
401
|
-
`${info.durationSeconds === null ? "duration not declared" : `${info.durationSeconds.toFixed(3)}s`}, ` +
|
|
402
|
-
`${info.startTimeSeconds === null
|
|
403
|
-
? "start of its timeline not declared"
|
|
404
|
-
: `its timeline starts at ${info.startTimeSeconds.toFixed(6)}s`}`
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
return info;
|
|
408
|
-
} catch (error) {
|
|
409
|
-
logger.warn(
|
|
410
|
-
`container-info: "${String(file.name).slice(0, 40)}" could not be read: ${error?.message ?? error}`
|
|
411
|
-
);
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the viewers of one output say about their links, as one reading.
|
|
3
|
-
*
|
|
4
|
-
* The budget asks one question — is anybody failing to keep up — so both terms
|
|
5
|
-
* are the worst case: the slowest link and the emptiest buffer, which may
|
|
6
|
-
* belong to different people. Taking the last report instead meant a session
|
|
7
|
-
* with two viewers acted on whichever of them happened to report most recently.
|
|
8
|
-
*
|
|
9
|
-
* NOTHING HERE EXPIRES, and that is the correction of 2026-09-14. A reading
|
|
10
|
-
* used to be discarded once it was older than a chosen thirty seconds, which
|
|
11
|
-
* conflated two facts under one number: how fast the link is, and whether we
|
|
12
|
-
* are still hearing from the page. The first persists — a link does not stop
|
|
13
|
-
* being what it was measured to be because nobody measured it for a minute,
|
|
14
|
-
* which is why the page itself keeps its last figure for ever. The second is
|
|
15
|
-
* PRESENCE, and presence has its own owner: a viewer who has gone is removed
|
|
16
|
-
* from the output, so what is walked here is only people who are still here.
|
|
17
|
-
*
|
|
18
|
-
* The reading is corrected continuously while material crosses the link, which
|
|
19
|
-
* is the whole shape: measured once when the connection comes up, corrected by
|
|
20
|
-
* every transfer after it.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { viewersOf } from "./Viewer.js";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {object} output - The session whose viewers are asked.
|
|
27
|
-
* @returns {{ linkMbps: number, bufferedAheadSec: number, at: number, viewers: number } | null}
|
|
28
|
-
* Null when nobody present has ever measured their link, which every caller
|
|
29
|
-
* already treats as "no opinion either way".
|
|
30
|
-
*/
|
|
31
|
-
export function worstLinkReading(output) {
|
|
32
|
-
let worst = null;
|
|
33
|
-
for (const viewer of viewersOf(output).values()) {
|
|
34
|
-
if (!viewer.isPresent()) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const reading = viewer.linkReading();
|
|
38
|
-
if (reading === null) {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (worst === null) {
|
|
42
|
-
worst = {
|
|
43
|
-
linkMbps: reading.linkMbps,
|
|
44
|
-
bufferedAheadSec: reading.bufferedAheadSec,
|
|
45
|
-
at: reading.at,
|
|
46
|
-
viewers: 1
|
|
47
|
-
};
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
worst.linkMbps = Math.min(worst.linkMbps, reading.linkMbps);
|
|
51
|
-
worst.bufferedAheadSec = Math.min(worst.bufferedAheadSec, reading.bufferedAheadSec);
|
|
52
|
-
worst.at = Math.max(worst.at, reading.at);
|
|
53
|
-
worst.viewers += 1;
|
|
54
|
-
}
|
|
55
|
-
return worst;
|
|
56
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The session's one statement of what it has produced.
|
|
3
|
-
*
|
|
4
|
-
* Every check builds real directories and real files, because what is under
|
|
5
|
-
* test is exactly the reading of them: which run's copy wins, what an empty
|
|
6
|
-
* file counts as, and whether a directory that has not changed is read again.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import os from "node:os";
|
|
13
|
-
import { mkdtempSync, writeFileSync, rmSync, unlinkSync } from "node:fs";
|
|
14
|
-
|
|
15
|
-
import { ProducedIndex } from "../services/produced-index.js";
|
|
16
|
-
|
|
17
|
-
/** The naming the fMP4 format uses, reduced to what the index asks of it. */
|
|
18
|
-
const segmentFormat = {
|
|
19
|
-
isSegmentFileName: (name) => /^segment-\d{5}\.mp4$/.test(name),
|
|
20
|
-
segmentIndexFromName: (name) => {
|
|
21
|
-
const match = /^segment-(\d{5})\.mp4$/.exec(name);
|
|
22
|
-
return match ? Number(match[1]) : -1;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const nameOf = (index) => `segment-${String(index).padStart(5, "0")}.mp4`;
|
|
27
|
-
|
|
28
|
-
function output() {
|
|
29
|
-
const dirPath = mkdtempSync(path.join(os.tmpdir(), "produced-index-"));
|
|
30
|
-
return {
|
|
31
|
-
dirPath,
|
|
32
|
-
write(index, contents) {
|
|
33
|
-
writeFileSync(path.join(dirPath, nameOf(index)), contents);
|
|
34
|
-
return path.join(dirPath, nameOf(index));
|
|
35
|
-
},
|
|
36
|
-
writeNamed(name, contents) {
|
|
37
|
-
writeFileSync(path.join(dirPath, name), contents);
|
|
38
|
-
return path.join(dirPath, name);
|
|
39
|
-
},
|
|
40
|
-
index: () => new ProducedIndex({ dirPath, segmentFormat }),
|
|
41
|
-
remove: () => rmSync(dirPath, { recursive: true, force: true })
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
test("a file with bytes in it is produced; one that was only opened is not", () => {
|
|
46
|
-
const s = output();
|
|
47
|
-
try {
|
|
48
|
-
s.write(1, "a piece");
|
|
49
|
-
s.write(2, "");
|
|
50
|
-
const index = s.index();
|
|
51
|
-
|
|
52
|
-
assert.deepEqual([...index.segmentNumbers()], [1]);
|
|
53
|
-
assert.equal(index.pathOf(nameOf(1)), path.join(s.dirPath, nameOf(1)));
|
|
54
|
-
// The empty file exists and is not an answer — this is the pair of beliefs
|
|
55
|
-
// that stopped playback for ten minutes on 2026-09-03, now one belief.
|
|
56
|
-
assert.equal(index.pathOf(nameOf(2)), null);
|
|
57
|
-
} finally {
|
|
58
|
-
s.remove();
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
test("a file that is not a segment is found by name", () => {
|
|
65
|
-
const s = output();
|
|
66
|
-
try {
|
|
67
|
-
s.writeNamed("init.mp4", "header");
|
|
68
|
-
const index = s.index();
|
|
69
|
-
assert.equal(index.pathOf("init.mp4"), path.join(s.dirPath, "init.mp4"));
|
|
70
|
-
assert.ok(index.fileNames().includes("init.mp4"));
|
|
71
|
-
} finally {
|
|
72
|
-
s.remove();
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("a directory that is gone stops answering", () => {
|
|
77
|
-
const s = output();
|
|
78
|
-
try {
|
|
79
|
-
s.write(4, "piece");
|
|
80
|
-
const index = s.index();
|
|
81
|
-
assert.equal(index.segmentNumbers().size, 1);
|
|
82
|
-
|
|
83
|
-
rmSync(s.dirPath, { recursive: true, force: true });
|
|
84
|
-
assert.equal(index.segmentNumbers().size, 0);
|
|
85
|
-
assert.equal(index.pathOf(nameOf(4)), null);
|
|
86
|
-
} finally {
|
|
87
|
-
s.remove();
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("a file removed on purpose is not still an answer", () => {
|
|
92
|
-
const s = output();
|
|
93
|
-
try {
|
|
94
|
-
const written = s.write(9, "piece");
|
|
95
|
-
const index = s.index();
|
|
96
|
-
assert.equal(index.pathOf(nameOf(9)), written);
|
|
97
|
-
|
|
98
|
-
unlinkSync(written);
|
|
99
|
-
// The directory's own time has moved, but a caller that deletes and asks in
|
|
100
|
-
// the same tick must not be told the file is there.
|
|
101
|
-
index.invalidate();
|
|
102
|
-
assert.equal(index.pathOf(nameOf(9)), null);
|
|
103
|
-
} finally {
|
|
104
|
-
s.remove();
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("an unchanged directory is not listed again", () => {
|
|
109
|
-
const s = output();
|
|
110
|
-
try {
|
|
111
|
-
s.write(1, "piece");
|
|
112
|
-
const index = s.index();
|
|
113
|
-
|
|
114
|
-
index.segmentNumbers();
|
|
115
|
-
assert.equal(index.directoryReads, 1, "the first question reads what is there and no more");
|
|
116
|
-
|
|
117
|
-
for (let asked = 0; asked < 20; asked += 1) {
|
|
118
|
-
index.segmentNumbers();
|
|
119
|
-
index.pathOf(nameOf(1));
|
|
120
|
-
}
|
|
121
|
-
assert.equal(
|
|
122
|
-
index.directoryReads,
|
|
123
|
-
1,
|
|
124
|
-
"forty more questions about a directory that has not changed must read nothing"
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
s.write(2, "another piece");
|
|
128
|
-
index.segmentNumbers();
|
|
129
|
-
assert.equal(index.directoryReads, 2, "a directory that gained a file is read once more");
|
|
130
|
-
} finally {
|
|
131
|
-
s.remove();
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("an output directory that does not exist answers with nothing", () => {
|
|
136
|
-
const index = new ProducedIndex({
|
|
137
|
-
dirPath: path.join(os.tmpdir(), "produced-index-absent-0000"),
|
|
138
|
-
segmentFormat
|
|
139
|
-
});
|
|
140
|
-
assert.equal(index.pathOf(nameOf(1)), null);
|
|
141
|
-
assert.deepEqual([...index.segmentNumbers()], []);
|
|
142
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|