@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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which output already on this proxy serves a viewer, instead of a new
|
|
3
|
+
* encode of the format they would otherwise be given.
|
|
4
|
+
*
|
|
5
|
+
* Decided with the user 2026-09-16, and made one rule for every path on
|
|
6
|
+
* 2026-09-23 (roadmap item 97, step 11). Quality is compared by the number of
|
|
7
|
+
* points in the picture. An output qualifies only if the piece at the viewer's
|
|
8
|
+
* position is already made — material somewhere else in the film spares them
|
|
9
|
+
* nothing — AND the viewer's own link admits its load (`link-budget.js`), in
|
|
10
|
+
* every mode and on every branch. A load whose figure is a bound (`known`) is
|
|
11
|
+
* taken before one whose figure is an average (`estimated`), whatever their
|
|
12
|
+
* sizes; an `unknown` load is admitted only while nothing has measured the
|
|
13
|
+
* link.
|
|
14
|
+
*
|
|
15
|
+
* AUTOMATIC:
|
|
16
|
+
* 1. at least the quality wanted — the nearest to it;
|
|
17
|
+
* 2. below the quality wanted but above the next rung down of this viewer's
|
|
18
|
+
* own ladder — the largest of those;
|
|
19
|
+
* 3. lower still only when producing the wanted format here risks the viewer
|
|
20
|
+
* waiting: this machine does not hold it at the speed the file needs — the
|
|
21
|
+
* largest of those.
|
|
22
|
+
*
|
|
23
|
+
* CHOSEN BY HAND: exactly the size chosen — of those, the one with the highest
|
|
24
|
+
* bitrate the link admits. Which encoder made it and at which speed setting do
|
|
25
|
+
* not matter to the viewer.
|
|
26
|
+
*
|
|
27
|
+
* No reading of disk or machine here: every figure arrives as a plain value.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { OutputSpec } from "../output/OutputSpec.js";
|
|
31
|
+
import { admissionRank } from "./link-budget.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} Candidate
|
|
35
|
+
* @property {string} key - The output's key.
|
|
36
|
+
* @property {OutputSpec} spec
|
|
37
|
+
* @property {number} width
|
|
38
|
+
* @property {number} height
|
|
39
|
+
* @property {boolean} readyHere - The piece at the viewer's position is made.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} params
|
|
44
|
+
* @param {"auto" | "manual"} params.mode
|
|
45
|
+
* @param {{ width: number, height: number }} params.wanted - The format this
|
|
46
|
+
* viewer would otherwise be given.
|
|
47
|
+
* @param {number} params.nextLowerArea - Points in the next rung below `wanted`
|
|
48
|
+
* on this viewer's own ladder; zero when there is none.
|
|
49
|
+
* @param {boolean} params.atRisk - Producing `wanted` here is measured not to
|
|
50
|
+
* keep up with what the file needs. False where nothing measured it.
|
|
51
|
+
* @param {(candidate: Candidate) => { admitted: boolean, load: object }} params.judge -
|
|
52
|
+
* What this viewer's link does with the candidate's whole load.
|
|
53
|
+
* @param {Candidate[]} params.candidates
|
|
54
|
+
* @returns {{ key: string, answer: object } | null} The output to serve, or
|
|
55
|
+
* null to produce `wanted`.
|
|
56
|
+
*/
|
|
57
|
+
export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, judge, candidates }) {
|
|
58
|
+
const admitted = (candidates ?? [])
|
|
59
|
+
.filter((candidate) => candidate.readyHere === true)
|
|
60
|
+
.map((candidate) => ({ candidate, answer: judge(candidate) }))
|
|
61
|
+
.filter((one) => one.answer.admitted);
|
|
62
|
+
const areaOf = (one) => one.candidate.width * one.candidate.height;
|
|
63
|
+
const rateOf = (one) => one.answer.load.totalMbps ?? 0;
|
|
64
|
+
const byRank = (left, right) => admissionRank(right.answer) - admissionRank(left.answer);
|
|
65
|
+
const pick = (list) => (list.length > 0 ? { key: list[0].candidate.key, answer: list[0].answer } : null);
|
|
66
|
+
if (mode === "manual") {
|
|
67
|
+
return pick(
|
|
68
|
+
admitted
|
|
69
|
+
.filter((one) => one.candidate.width === wanted.width && one.candidate.height === wanted.height)
|
|
70
|
+
.sort((left, right) => byRank(left, right) || rateOf(right) - rateOf(left))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const wantedArea = wanted.width * wanted.height;
|
|
74
|
+
const atLeast = admitted
|
|
75
|
+
.filter((one) => areaOf(one) >= wantedArea)
|
|
76
|
+
.sort((left, right) => byRank(left, right) || areaOf(left) - areaOf(right) || rateOf(right) - rateOf(left));
|
|
77
|
+
if (atLeast.length > 0) {
|
|
78
|
+
return pick(atLeast);
|
|
79
|
+
}
|
|
80
|
+
const below = admitted
|
|
81
|
+
.filter((one) => areaOf(one) < wantedArea)
|
|
82
|
+
.sort((left, right) => byRank(left, right) || areaOf(right) - areaOf(left) || rateOf(right) - rateOf(left));
|
|
83
|
+
const close = below.filter((one) => areaOf(one) > nextLowerArea);
|
|
84
|
+
if (close.length > 0) {
|
|
85
|
+
return pick(close);
|
|
86
|
+
}
|
|
87
|
+
return atRisk ? pick(below) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Points in the next rung below a height on a viewer's own ladder.
|
|
92
|
+
*
|
|
93
|
+
* @param {{ width: number, height: number }[]} ladder - High to low.
|
|
94
|
+
* @param {number} height
|
|
95
|
+
* @returns {number} Zero when nothing on the ladder is lower.
|
|
96
|
+
*/
|
|
97
|
+
export function nextRungAreaBelow(ladder, height) {
|
|
98
|
+
const next = (ladder ?? []).find((rung) => rung.height < height);
|
|
99
|
+
return next ? next.width * next.height : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The outputs that could stand in for `wanted`, read from their keys.
|
|
104
|
+
*
|
|
105
|
+
* One qualifies when it is the same picture of the same file in every respect
|
|
106
|
+
* but its quality: the same torrent, container, cut grid, soundtrack and what
|
|
107
|
+
* the output carries. A re-encode qualifies only if this machine's encoder
|
|
108
|
+
* made it, because serving it means going on producing it, and pieces of
|
|
109
|
+
* another encoder cannot share its header. A copy is the source's own picture.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} params
|
|
112
|
+
* @param {OutputSpec} params.wanted
|
|
113
|
+
* @param {Iterable<string>} params.keys - Every output this proxy holds, live
|
|
114
|
+
* or only on disk. Repeats are fine.
|
|
115
|
+
* @param {{ width: number, height: number, megabitsPerSecond: number | null }} params.source
|
|
116
|
+
* @param {string} params.encoderName
|
|
117
|
+
* @param {(key: string) => boolean} params.readyAt - The piece at the viewer's
|
|
118
|
+
* position is made.
|
|
119
|
+
* @returns {Candidate[]}
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* Whether two outputs are of the same MATERIAL — the same film, the same
|
|
123
|
+
* container, the same cuts, the same tracks — and therefore interchangeable as
|
|
124
|
+
* far as anything but picture size is concerned.
|
|
125
|
+
*
|
|
126
|
+
* Stated once and exported, because two places ask it: choosing an output by
|
|
127
|
+
* quality, and answering which output serves a height. A step of a picture is
|
|
128
|
+
* only a step of THAT picture if this holds; a 720p step in fMP4 handed to a
|
|
129
|
+
* picture cut for MPEG-TS is a segment name its player cannot even ask for.
|
|
130
|
+
*
|
|
131
|
+
* Picture SIZE is deliberately not in it: differing in size is what a step is.
|
|
132
|
+
*
|
|
133
|
+
* @param {OutputSpec} spec
|
|
134
|
+
* @param {OutputSpec} wanted
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
137
|
+
export function isSameMaterial(spec, wanted) {
|
|
138
|
+
if (!spec || !wanted || !spec.video || !wanted.video) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return (
|
|
142
|
+
spec.sourceKey === wanted.sourceKey &&
|
|
143
|
+
spec.segmentFormatId === wanted.segmentFormatId &&
|
|
144
|
+
spec.grid.toKey() === wanted.grid.toKey() &&
|
|
145
|
+
spec.carries === wanted.carries &&
|
|
146
|
+
spec.video.fileIndex === wanted.video.fileIndex &&
|
|
147
|
+
(spec.audio?.toKey() ?? "") === (wanted.audio?.toKey() ?? "")
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function servingCandidates({ wanted, keys, source, encoderName, readyAt }) {
|
|
152
|
+
const found = new Map();
|
|
153
|
+
for (const key of keys ?? []) {
|
|
154
|
+
if (found.has(key)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const spec = OutputSpec.fromKey(key);
|
|
158
|
+
if (!spec || !spec.video || !wanted.video) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!isSameMaterial(spec, wanted)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const encode = spec.video.encode;
|
|
165
|
+
if (encode && encode.encoder !== encoderName) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const width = encode ? encode.width : source.width;
|
|
169
|
+
const height = encode ? encode.height : source.height;
|
|
170
|
+
if (!(width > 0 && height > 0)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
found.set(key, {
|
|
174
|
+
key,
|
|
175
|
+
spec,
|
|
176
|
+
width,
|
|
177
|
+
height,
|
|
178
|
+
readyHere: readyAt(key)
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return [...found.values()];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether ONE output may serve this viewer: the same material as the picture
|
|
186
|
+
* it would stand in for, the size their mode requires, and a load their own
|
|
187
|
+
* link admits.
|
|
188
|
+
*
|
|
189
|
+
* THE ONE RULE EVERY PATH ASKS (roadmap item 97, step 11). The picture
|
|
190
|
+
* answering for its own height, an output found already producing a height,
|
|
191
|
+
* and the choice among outputs already here used to decide each for itself,
|
|
192
|
+
* and only the last asked about the viewer at all.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} params
|
|
195
|
+
* @param {OutputSpec} params.spec - The output that would serve.
|
|
196
|
+
* @param {OutputSpec} params.pictureSpec - The picture it would stand in for.
|
|
197
|
+
* @param {"auto" | "manual"} params.mode
|
|
198
|
+
* @param {{ width: number, height: number } | null} params.size - The size the
|
|
199
|
+
* output produces; for a copy, the source's.
|
|
200
|
+
* @param {{ width: number, height: number } | null} params.wanted - The size a
|
|
201
|
+
* viewer who picked by hand must get exactly; ignored in AUTO.
|
|
202
|
+
* @param {(spec: OutputSpec) => { admitted: boolean }} params.judge
|
|
203
|
+
* @returns {{ suits: boolean, answer: object | null, reason: string }}
|
|
204
|
+
*/
|
|
205
|
+
export function outputSuits({ spec, pictureSpec, mode, size, wanted, judge }) {
|
|
206
|
+
if (!isSameMaterial(spec, pictureSpec)) {
|
|
207
|
+
return { suits: false, answer: null, reason: "another material" };
|
|
208
|
+
}
|
|
209
|
+
if (mode === "manual" && wanted && size && (size.width !== wanted.width || size.height !== wanted.height)) {
|
|
210
|
+
return { suits: false, answer: null, reason: "not the size picked" };
|
|
211
|
+
}
|
|
212
|
+
const answer = judge(spec);
|
|
213
|
+
return answer.admitted
|
|
214
|
+
? { suits: true, answer, reason: answer.verdict }
|
|
215
|
+
: { suits: false, answer, reason: answer.verdict };
|
|
216
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which rung of the ladder the picture a viewer sees asks for (roadmap
|
|
3
|
+
* item 98).
|
|
4
|
+
*
|
|
5
|
+
* THE RULE, stated by the user 2026-09-27: the smallest height of the ladder
|
|
6
|
+
* whose frame is not smaller than the visible picture — both its width and its
|
|
7
|
+
* height. The visible picture is what the viewer's page measured: the frame
|
|
8
|
+
* that would be shown without being enlarged, in physical pixels
|
|
9
|
+
* (`server/public/domain/visible-picture.js`). A frame smaller than that is
|
|
10
|
+
* enlarged on screen and loses detail; a larger one costs encoder and link for
|
|
11
|
+
* detail the screen cannot show.
|
|
12
|
+
*
|
|
13
|
+
* When no rung is large enough — a picture shown bigger than the source — the
|
|
14
|
+
* answer is the top rung, the source's own size: nothing is ever made larger
|
|
15
|
+
* than the source.
|
|
16
|
+
*
|
|
17
|
+
* It bounds only a RE-ENCODED picture. A copy of the source is not re-encoded
|
|
18
|
+
* because it is taller than the picture on screen; it is re-encoded only when
|
|
19
|
+
* the browser cannot play it or the viewer's link cannot carry it (the rule is
|
|
20
|
+
* in "Key decisions" of the project notes).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { buildResolutionLadder } from "../hwaccel.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The rung of `ladder` the visible picture asks for.
|
|
27
|
+
*
|
|
28
|
+
* @param {Array<{ width: number, height: number }>} ladder - Highest first, as
|
|
29
|
+
* `buildResolutionLadder` gives it.
|
|
30
|
+
* @param {{ width: number, height: number } | null} visible
|
|
31
|
+
* @returns {{ width: number, height: number } | null} Null when nothing was
|
|
32
|
+
* measured or there is no ladder.
|
|
33
|
+
*/
|
|
34
|
+
export function rungForVisiblePicture(ladder, visible) {
|
|
35
|
+
if (!Array.isArray(ladder) || ladder.length === 0 || !(visible?.width > 0) || !(visible?.height > 0)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const large = ladder.filter((rung) => rung.width >= visible.width && rung.height >= visible.height);
|
|
39
|
+
return large.length > 0 ? large[large.length - 1] : ladder[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The height a re-encoded picture of this source is bounded by for a viewer
|
|
44
|
+
* who sees `visible`, or null when nothing bounds it.
|
|
45
|
+
*
|
|
46
|
+
* @param {number} sourceWidth
|
|
47
|
+
* @param {number} sourceHeight
|
|
48
|
+
* @param {{ width: number, height: number } | null} visible
|
|
49
|
+
* @returns {number | null}
|
|
50
|
+
*/
|
|
51
|
+
export function visibleHeightCap(sourceWidth, sourceHeight, visible) {
|
|
52
|
+
const rung = rungForVisiblePicture(
|
|
53
|
+
buildResolutionLadder(Math.round(Number(sourceWidth) || 0), Math.round(Number(sourceHeight) || 0)),
|
|
54
|
+
visible
|
|
55
|
+
);
|
|
56
|
+
return rung ? rung.height : null;
|
|
57
|
+
}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* killing and resuming belong to whoever owns the process.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import { PLAYLIST_FILE_NAME } from "./output/playlists.js";
|
|
19
|
+
import { AUDIO_TRANSCODE_KBPS } from "./args.js";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* What ffmpeg's own CLI subtracts from an input seek, and therefore what has to
|
|
@@ -223,7 +223,7 @@ export function nearestKeyframeAtOrBefore(keyframeTimes, target) {
|
|
|
223
223
|
* the bound picks the smaller error, which is then under one keyframe interval
|
|
224
224
|
* and therefore under what a player bridges.
|
|
225
225
|
*
|
|
226
|
-
* @param {{ transcodeVideo?: boolean,
|
|
226
|
+
* @param {{ transcodeVideo?: boolean, audioOnly?: boolean, keyframes?: { times?: number[] | null, tolerance?: number } }} material
|
|
227
227
|
* @param {number} keyframe - A real keyframe time the run is to begin at.
|
|
228
228
|
* @returns {number} Seconds to add to the request.
|
|
229
229
|
*/
|
|
@@ -246,11 +246,11 @@ export function seekLandingOffsetFor(material, keyframe) {
|
|
|
246
246
|
// A grid whose times are approximate needs that error added on top, or a name
|
|
247
247
|
// sitting just below its real keyframe seeks to before it and lands on the
|
|
248
248
|
// one before that. Only AVI declares one.
|
|
249
|
-
const tolerance = Number.isFinite(material?.
|
|
250
|
-
? Math.max(0, material.
|
|
249
|
+
const tolerance = Number.isFinite(material?.keyframes?.tolerance)
|
|
250
|
+
? Math.max(0, material.keyframes.tolerance)
|
|
251
251
|
: 0;
|
|
252
252
|
const wanted = SEEK_LANDING_OFFSET_SEC + tolerance;
|
|
253
|
-
const times = Array.isArray(material?.
|
|
253
|
+
const times = Array.isArray(material?.keyframes?.times) ? material.keyframes.times : [];
|
|
254
254
|
const next = times.find((time) => time > keyframe + 0.001);
|
|
255
255
|
if (next === undefined) {
|
|
256
256
|
return wanted;
|
|
@@ -262,8 +262,9 @@ export function seekLandingOffsetFor(material, keyframe) {
|
|
|
262
262
|
* Everything ffmpeg is told for one run.
|
|
263
263
|
*
|
|
264
264
|
* @param {object} params
|
|
265
|
-
* @param {{
|
|
266
|
-
* PICTURE's file
|
|
265
|
+
* @param {{ times?: number[] | null, tolerance?: number }} params.keyframes - Where
|
|
266
|
+
* the PICTURE's file has its keyframes: what a seek snaps to. Plain values,
|
|
267
|
+
* so this can be asked without a container, a torrent or a session.
|
|
267
268
|
* @param {{ startTime: number }} params.inputFile - The file this run reads.
|
|
268
269
|
* @param {{ startTime: number }} params.audioFile - The file the chosen
|
|
269
270
|
* soundtrack lives in, which for a dub shipped beside the picture is not the
|
|
@@ -280,7 +281,8 @@ export function seekLandingOffsetFor(material, keyframe) {
|
|
|
280
281
|
* @param {boolean} params.audioSeparate - The picture's sound is published as a
|
|
281
282
|
* rendition, so this output carries none.
|
|
282
283
|
* @param {number} params.audioSourceTrackIndex - `0:a:N` within its own file.
|
|
283
|
-
* @param {number | null} params.
|
|
284
|
+
* @param {{ maxrateKbps: number, bufsizeKbps: number, level: string | null } | null} params.rateControl
|
|
285
|
+
* The output's own, as its key names it; null where nothing bounds the rate.
|
|
284
286
|
* @param {number} params.startIndex - First segment number this run makes.
|
|
285
287
|
* @param {number} params.endIndex - Last it makes, inclusive; below the start
|
|
286
288
|
* means it has no end.
|
|
@@ -291,7 +293,7 @@ export function seekLandingOffsetFor(material, keyframe) {
|
|
|
291
293
|
* @returns {{ args: string[], safeIndex: number, startSeconds: number, cutTimes: number[] | null }}
|
|
292
294
|
*/
|
|
293
295
|
export function buildRunCommand({
|
|
294
|
-
|
|
296
|
+
keyframes,
|
|
295
297
|
inputFile,
|
|
296
298
|
audioFile,
|
|
297
299
|
inputUrl,
|
|
@@ -304,7 +306,7 @@ export function buildRunCommand({
|
|
|
304
306
|
audioOnly,
|
|
305
307
|
audioSeparate,
|
|
306
308
|
audioSourceTrackIndex,
|
|
307
|
-
|
|
309
|
+
rateControl,
|
|
308
310
|
startIndex,
|
|
309
311
|
endIndex,
|
|
310
312
|
positionSecondsOverride,
|
|
@@ -340,8 +342,8 @@ export function buildRunCommand({
|
|
|
340
342
|
// Fresh by construction: the session may have been created before the
|
|
341
343
|
// soundtrack file's header could be read, and the reading lands on the file
|
|
342
344
|
// object this session holds — so there is nothing to re-read and nothing
|
|
343
|
-
// that can be stale. Same property as
|
|
344
|
-
//
|
|
345
|
+
// that can be stale. Same property as the keyframe table, which is one
|
|
346
|
+
// object shared by every session of the file.
|
|
345
347
|
// Which timeline this run works on. Asked once, because the closure that
|
|
346
348
|
// adds the second input reads it too, and two readings of one predicate is
|
|
347
349
|
// how the picture and the sound came apart before.
|
|
@@ -420,13 +422,14 @@ export function buildRunCommand({
|
|
|
420
422
|
// count can describe them: the encoder is told the times outright,
|
|
421
423
|
// the same ones the muxer will cut at.
|
|
422
424
|
forcedKeyframeTimes: cutTimes,
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
425
|
+
// The output's own rate control and level. Read off the output, whose
|
|
426
|
+
// key names them: two limits are two outputs, and nothing moves a
|
|
427
|
+
// limit under an output that already exists.
|
|
428
|
+
rateControl: rateControl ?? null
|
|
426
429
|
})
|
|
427
430
|
: ["-c:v", "copy"];
|
|
428
431
|
const audioCodecArgs = transcodeAudio
|
|
429
|
-
? ["-c:a", "aac", "-ac", "2", "-b:a",
|
|
432
|
+
? ["-c:a", "aac", "-ac", "2", "-b:a", `${AUDIO_TRANSCODE_KBPS}k`]
|
|
430
433
|
: ["-c:a", "copy"];
|
|
431
434
|
|
|
432
435
|
const args = ["-hide_banner", "-nostats", "-loglevel", "error", "-progress", "pipe:1"];
|
|
@@ -478,8 +481,8 @@ export function buildRunCommand({
|
|
|
478
481
|
: null;
|
|
479
482
|
const snappedKeyframe = Number.isFinite(carriedKeyframe)
|
|
480
483
|
? carriedKeyframe
|
|
481
|
-
: (Array.isArray(
|
|
482
|
-
? nearestKeyframeAtOrBefore(
|
|
484
|
+
: (Array.isArray(keyframes?.times) && keyframes.times.length > 0
|
|
485
|
+
? nearestKeyframeAtOrBefore(keyframes.times, seekSeconds)
|
|
483
486
|
: null);
|
|
484
487
|
// A second input, and it exists for exactly one case: a browser that takes
|
|
485
488
|
// its audio muxed into the picture, watching a release whose soundtrack is a
|
|
@@ -536,7 +539,7 @@ export function buildRunCommand({
|
|
|
536
539
|
if (snappedKeyframe !== null) {
|
|
537
540
|
const residualSeconds = Math.max(0, seekSeconds - snappedKeyframe);
|
|
538
541
|
if (snappedKeyframe > 0) {
|
|
539
|
-
args.push("-ss", ffmpegSeconds(snappedKeyframe + seekLandingOffsetFor({ audioOnly, transcodeVideo,
|
|
542
|
+
args.push("-ss", ffmpegSeconds(snappedKeyframe + seekLandingOffsetFor({ audioOnly, transcodeVideo, keyframes }, snappedKeyframe)));
|
|
540
543
|
}
|
|
541
544
|
args.push("-i", inputUrl);
|
|
542
545
|
// The coarse landing, not the exact target: the residual below is discarded
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The addresses an encoder reads its source from.
|
|
3
|
+
*
|
|
4
|
+
* Built per run from facts that belong elsewhere — which files the output
|
|
5
|
+
* reads (`OutputSpec`), where the stream route listens, which output the read
|
|
6
|
+
* feeds, and how wide its read window is — and kept nowhere.
|
|
7
|
+
*
|
|
8
|
+
* Two query parameters are what make the encoder's read what it is to the
|
|
9
|
+
* stream route: `reader=playback`, the one read that says where the viewer is,
|
|
10
|
+
* and `windowBytes`, how far ahead of itself that read asks the swarm for,
|
|
11
|
+
* sized in seconds of playback by whoever knows the file's byte rate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} params
|
|
16
|
+
* @param {{ fileIndex: number, streamUrl: (baseUrl: string, options?: { sessionId?: string }) => URL }} params.picture
|
|
17
|
+
* @param {{ fileIndex: number, streamUrl: (baseUrl: string, options?: { sessionId?: string }) => URL }} params.soundtrack
|
|
18
|
+
* The file the carried soundtrack lives in; the picture's own file for an
|
|
19
|
+
* embedded track.
|
|
20
|
+
* @param {"video-only" | "audio-only" | "muxed" | "empty"} params.carries
|
|
21
|
+
* @param {boolean} params.audioSeparate - The picture's sound travels as its own output.
|
|
22
|
+
* @param {string} params.sessionId - Which output this read feeds.
|
|
23
|
+
* @param {number} params.readWindowBytes - Zero when no window was measured.
|
|
24
|
+
* @param {string} params.baseUrl
|
|
25
|
+
* @returns {{ inputFile: object, audioFile: object, inputUrl: string, audioInputUrl: string }}
|
|
26
|
+
*/
|
|
27
|
+
export function encoderInputs({ picture, soundtrack, carries, audioSeparate, sessionId, readWindowBytes, baseUrl }) {
|
|
28
|
+
// A soundtrack shipped as its own file is encoded FROM that file, so a
|
|
29
|
+
// rendition of it reads nothing else.
|
|
30
|
+
const inputFile = carries === "audio-only" && soundtrack !== picture ? soundtrack : picture;
|
|
31
|
+
const input = inputFile.streamUrl(baseUrl, { sessionId });
|
|
32
|
+
if (Number.isFinite(readWindowBytes) && readWindowBytes > 0) {
|
|
33
|
+
input.searchParams.set("windowBytes", String(readWindowBytes));
|
|
34
|
+
input.searchParams.set("reader", "playback");
|
|
35
|
+
}
|
|
36
|
+
// The second input exists only for a picture that carries its sound inside
|
|
37
|
+
// itself when that sound is in another file.
|
|
38
|
+
const audioInputUrl = carries !== "audio-only" && !audioSeparate && soundtrack !== inputFile
|
|
39
|
+
? soundtrack.streamUrl(baseUrl, { sessionId }).toString()
|
|
40
|
+
: "";
|
|
41
|
+
return { inputFile, audioFile: soundtrack, inputUrl: input.toString(), audioInputUrl };
|
|
42
|
+
}
|
|
@@ -305,6 +305,69 @@ export const fmp4Format = {
|
|
|
305
305
|
return countFragmentTracks(bytes) >= expected.size;
|
|
306
306
|
},
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Whether this piece is whole, judged against ITS OWN header.
|
|
310
|
+
*
|
|
311
|
+
* A piece that disagrees with the session's cached header is one of two very
|
|
312
|
+
* different things, and they call for opposite actions: the piece may be
|
|
313
|
+
* short, or the HEADER may be the odd one out. A self-contained piece carries
|
|
314
|
+
* a `moov` of its own, so it can be compared with itself, and that comparison
|
|
315
|
+
* tells them apart without knowing anything about encoders.
|
|
316
|
+
*
|
|
317
|
+
* WHAT THIS REPLACED, because the difference is five minutes of a frozen
|
|
318
|
+
* picture. The action used to be chosen by "is any run of this session
|
|
319
|
+
* alive?" — a question about the SESSION, asked about a fact of the BYTES. A
|
|
320
|
+
* run anywhere in the film meant "still being written; waiting for it", and
|
|
321
|
+
* nothing re-judged or re-made the piece afterwards, so one misjudgement
|
|
322
|
+
* became permanent. Field 2026-09-14: `segment-00033.mp4` of a copied
|
|
323
|
+
* picture, already served whole to one viewer at 5 099 421 bytes, was refused
|
|
324
|
+
* to a second viewer for five minutes — four holds of sixty seconds each,
|
|
325
|
+
* then 503 — while the run in force was at #192 and beyond. That viewer's
|
|
326
|
+
* picture stood still 196.2 s and then a further 56.2 s.
|
|
327
|
+
*
|
|
328
|
+
* The run question is also unanswerable and does not need asking: a piece
|
|
329
|
+
* being written is called `making-<tag>-NNNNN.mp4` and takes its served name
|
|
330
|
+
* by a rename, which is atomic. A file under its served name is never being
|
|
331
|
+
* written and never half-read.
|
|
332
|
+
*
|
|
333
|
+
* The same file read back off the disk declares one track in its header and
|
|
334
|
+
* carries one in its fragments, so what fired in the field was not the piece.
|
|
335
|
+
* Until a reading names what it was, the verdict carries every count it made.
|
|
336
|
+
*
|
|
337
|
+
* @param {Buffer} raw - The piece as it is on disk, header and all.
|
|
338
|
+
* @param {Buffer} bytes - Its media part, the header already removed.
|
|
339
|
+
* @param {Buffer | null} sessionHeader - What the session serves as its init.
|
|
340
|
+
* @returns {{ whole: boolean, serve: boolean, fragmentTracks: number,
|
|
341
|
+
* ownTracks: number, sessionTracks: number, because: string } | null} Null
|
|
342
|
+
* where there is nothing to compare against, which is not a verdict.
|
|
343
|
+
*/
|
|
344
|
+
judgeTracks(raw, bytes, sessionHeader) {
|
|
345
|
+
if (!sessionHeader || sessionHeader.length === 0) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const sessionTracks = readTrackTimescales(sessionHeader).size;
|
|
349
|
+
if (sessionTracks === 0) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const fragmentTracks = countFragmentTracks(bytes);
|
|
353
|
+
if (fragmentTracks >= sessionTracks) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const ownHeader = this.extractInit(raw);
|
|
357
|
+
const ownTracks = ownHeader ? readTrackTimescales(ownHeader).size : 0;
|
|
358
|
+
const agreesWithItself = ownTracks > 0 && fragmentTracks >= ownTracks;
|
|
359
|
+
return {
|
|
360
|
+
whole: agreesWithItself,
|
|
361
|
+
serve: agreesWithItself,
|
|
362
|
+
fragmentTracks,
|
|
363
|
+
ownTracks,
|
|
364
|
+
sessionTracks,
|
|
365
|
+
because: agreesWithItself
|
|
366
|
+
? "it agrees with its own header, so the session's is the odd one out; serving it"
|
|
367
|
+
: "it disagrees with its own header, so it was closed short; removing it to be made again"
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
|
|
308
371
|
/**
|
|
309
372
|
* How many tracks an init segment declares.
|
|
310
373
|
*
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* @file Selectable HLS segment container formats.
|
|
3
3
|
*
|
|
4
4
|
* Everything that differs between fMP4/CMAF and MPEG-TS output lives behind one
|
|
5
|
-
* interface, so
|
|
6
|
-
* a format object and
|
|
7
|
-
* not editing
|
|
5
|
+
* interface, so the encoding and the serving never branch on the container: they
|
|
6
|
+
* hold a format object and ask it. Adding a container means adding a module
|
|
7
|
+
* here, not editing either of them.
|
|
8
8
|
*
|
|
9
9
|
* The choice is per-proxy (CLI `--segment-format`), mirroring how Jellyfin lets
|
|
10
10
|
* the operator pick the transcoding container. fMP4 is the default; MPEG-TS is
|