@torrent-tv/proxy 2.86.0 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2138 -1915
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +745 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +931 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/wire-outputs.js +958 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -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 +40 -8
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -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,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Subtitle orchestrator — application layer over subtitle domain.
|
|
3
|
-
*
|
|
4
|
-
* Wraps torrent-worker/subtitle-cues.js domain (planFor, cuesHeldFor,
|
|
5
|
-
* warmSubtitleCues, forgetSubtitles) behind Container/Track abstraction.
|
|
6
|
-
* Provides per-file track list and cue streaming, with the same "only already
|
|
7
|
-
* downloaded clusters" rule as before. Controllers (HTTP or data-channel)
|
|
8
|
-
* depend on this, not on the worker module directly.
|
|
9
|
-
*
|
|
10
|
-
* Delegates to ContainerOrchestrator for track enumeration so subtitle tracks
|
|
11
|
-
* and their flags come from the unified ContainerTrack hierarchy.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { containerOrchestrator } from "./ContainerOrchestrator.js";
|
|
15
|
-
import {
|
|
16
|
-
warmSubtitleCues as domainWarm,
|
|
17
|
-
subtitleTracksOf,
|
|
18
|
-
declaredSubtitleTracksOf,
|
|
19
|
-
forgetSubtitles as domainForget
|
|
20
|
-
} from "../torrent-worker/subtitle-cues.js";
|
|
21
|
-
import { logger } from "../../utils/logger.js";
|
|
22
|
-
|
|
23
|
-
export class SubtitleOrchestrator {
|
|
24
|
-
/**
|
|
25
|
-
* @param {import("./ContainerOrchestrator.js").ContainerOrchestrator} containerOrchestrator
|
|
26
|
-
*/
|
|
27
|
-
constructor(containerOrchestrator) {
|
|
28
|
-
this.containers = containerOrchestrator;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Tracks for menu — text tracks via domain, enriched with ContainerTrack flags.
|
|
33
|
-
* Falls back to container tracks when domain has no plan yet.
|
|
34
|
-
* @param {object} torrent
|
|
35
|
-
* @param {number} fileIndex
|
|
36
|
-
* @param {string} sourceKey
|
|
37
|
-
* @param {(start:number,end:number)=>Promise<Buffer|null>} [readRange]
|
|
38
|
-
* @param {number} [fileSize]
|
|
39
|
-
* @returns {Promise<import("../tracks/index.js").ContainerTrack[]>}
|
|
40
|
-
*/
|
|
41
|
-
async getTracks(torrent, fileIndex, sourceKey, readRange, fileSize) {
|
|
42
|
-
try {
|
|
43
|
-
const domain = await subtitleTracksOf(torrent, fileIndex, sourceKey);
|
|
44
|
-
if (Array.isArray(domain) && domain.length > 0) return domain;
|
|
45
|
-
} catch {}
|
|
46
|
-
if (readRange && Number.isFinite(fileSize)) {
|
|
47
|
-
try {
|
|
48
|
-
const tracks = await this.containers.getTracks({ sourceKey, fileIndex, readRange, fileSize, label: torrent?.files?.[fileIndex]?.name ?? "" });
|
|
49
|
-
return tracks.filter((t) => t.type === "subtitle");
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Declared subtitle tracks in container order (including image tracks) — for declaredIndex alignment.
|
|
57
|
-
*/
|
|
58
|
-
async getDeclaredTracks(torrent, fileIndex, sourceKey) {
|
|
59
|
-
try {
|
|
60
|
-
return await declaredSubtitleTracksOf(torrent, fileIndex, sourceKey);
|
|
61
|
-
} catch {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Cues already downloaded for one track — ASKED OF THE TORRENT WORKER, never
|
|
68
|
-
* walked here.
|
|
69
|
-
*
|
|
70
|
-
* The walk decides what it may read from `torrent.bitfield` and
|
|
71
|
-
* `torrent.pieceLength`, and a torrent stand-in on the main thread has
|
|
72
|
-
* neither: it carries `infoHash`, `name` and a `files` list whose reads go
|
|
73
|
-
* back across the boundary (`torrent-worker/client.js`). So the same code
|
|
74
|
-
* called here answers that nothing is downloaded, walks no clusters, and
|
|
75
|
-
* returns an empty document — which is not a failure anything reports,
|
|
76
|
-
* because "no cues held" is a legitimate answer.
|
|
77
|
-
*
|
|
78
|
-
* Field 2026-09-03, and it is the whole reason this method changed. An
|
|
79
|
-
* episode already downloaded from an earlier sitting had its cues found
|
|
80
|
-
* within a second and a half of the file being opened, and the first four
|
|
81
|
-
* pushes — everything before 81.7 s — went out before the browser had
|
|
82
|
-
* subscribed. The catch-up pull that exists for exactly that case answered
|
|
83
|
-
* `WEBVTT` and nothing else, with `x-subtitle-covered-clusters: 0` against
|
|
84
|
-
* 283 indexed, so the viewer watched the opening of the episode with no
|
|
85
|
-
* subtitles and the rest of it with them.
|
|
86
|
-
*
|
|
87
|
-
* There is a second reason, independent of the bitfield. The register of what
|
|
88
|
-
* has been walked, what has been found and in what ORDER lives in the module
|
|
89
|
-
* that does the walking, and the worker already keeps one — the push path
|
|
90
|
-
* fills it. Walking again on the main thread would build a SECOND register
|
|
91
|
-
* with its own `seq` counter, and the browser mixes the cursors from both
|
|
92
|
-
* paths (`#rememberCursor`): two counters would make a cursor from a pull and
|
|
93
|
-
* a cursor from a push incomparable. One register, one walk, one cursor.
|
|
94
|
-
*
|
|
95
|
-
* @param {{ getSubtitleCues?: Function }} pool - The torrent pool, which is
|
|
96
|
-
* what holds the channel to the worker.
|
|
97
|
-
* @param {object} torrent
|
|
98
|
-
* @param {number} fileIndex
|
|
99
|
-
* @param {string} sourceKey
|
|
100
|
-
* @param {number} trackNumber - Container trackNumber
|
|
101
|
-
* @returns {Promise<{ cues: object[], coveredClusters: number, indexedClusters: number, track: object | null }>}
|
|
102
|
-
*/
|
|
103
|
-
async getCues(pool, torrent, fileIndex, sourceKey, trackNumber) {
|
|
104
|
-
// Built fresh on each of the three paths that need it. One shared literal
|
|
105
|
-
// returned by reference would hand every caller the same array, and a
|
|
106
|
-
// single one of them appending to it would change what the next caller
|
|
107
|
-
// reads — which in a method about cue registers not leaking into each
|
|
108
|
-
// other would be a poor thing to introduce.
|
|
109
|
-
const empty = () => ({ cues: [], coveredClusters: 0, indexedClusters: 0, track: null });
|
|
110
|
-
if (typeof pool?.getSubtitleCues !== "function") {
|
|
111
|
-
// Nothing here can read pieces, and answering an empty document would be
|
|
112
|
-
// indistinguishable from a file that genuinely holds no cues.
|
|
113
|
-
logger.warn(
|
|
114
|
-
"subtitle-orchestrator: the torrent pool cannot be asked for cues, " +
|
|
115
|
-
"so none can be served — the walk needs the thread that owns the torrent"
|
|
116
|
-
);
|
|
117
|
-
return empty();
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
const answer = await pool.getSubtitleCues(torrent, fileIndex, trackNumber);
|
|
121
|
-
if (!answer) {
|
|
122
|
-
return empty();
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
cues: Array.isArray(answer.cues) ? answer.cues : [],
|
|
126
|
-
coveredClusters: answer.coveredClusters ?? 0,
|
|
127
|
-
indexedClusters: answer.indexedClusters ?? 0,
|
|
128
|
-
// The worker answers with the track's own fields flat, because a
|
|
129
|
-
// `ContainerTrack` is a class and only plain objects cross the boundary.
|
|
130
|
-
track: {
|
|
131
|
-
codecId: answer.codecId ?? "",
|
|
132
|
-
codecPrivate: answer.codecPrivate ?? "",
|
|
133
|
-
language: answer.language ?? ""
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
} catch (e) {
|
|
137
|
-
logger.warn(`subtitle-orchestrator: getCues failed: ${e?.message ?? e}`);
|
|
138
|
-
return empty();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Warm all subtitle tracks of a file — called periodically and on verified pieces.
|
|
144
|
-
* Returns per-track fresh cues for push.
|
|
145
|
-
*/
|
|
146
|
-
async warm(torrent, fileIndex, sourceKey) {
|
|
147
|
-
try {
|
|
148
|
-
return await domainWarm(torrent, fileIndex, sourceKey);
|
|
149
|
-
} catch {
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
forget(sourceKey, fileIndex) {
|
|
155
|
-
domainForget(sourceKey, fileIndex);
|
|
156
|
-
this.containers.forget(sourceKey, fileIndex);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export const subtitleOrchestrator = new SubtitleOrchestrator(containerOrchestrator);
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What one output is encoded AS, decided once.
|
|
3
|
-
*
|
|
4
|
-
* `OutputSpec` says what an output is — which tracks, in what form, cut how.
|
|
5
|
-
* This is the other half: the shape the encoder is actually given for it. The
|
|
6
|
-
* box in pixels, the frame rate, the speed setting, whether the picture is tone
|
|
7
|
-
* mapped down from HDR.
|
|
8
|
-
*
|
|
9
|
-
* **Why it is not a fact of a session.** It is decided by the realtime budget
|
|
10
|
-
* at the moment a session is created — what this machine could hold just then —
|
|
11
|
-
* so two sessions of one output, made minutes apart, could be given different
|
|
12
|
-
* shapes while claiming the same identity. Everything downstream assumes
|
|
13
|
-
* otherwise: a segment of one is supposed to be interchangeable with a segment
|
|
14
|
-
* of the other, and the master playlist names one `RESOLUTION` for both.
|
|
15
|
-
*
|
|
16
|
-
* Decided once per output and held here, that cannot happen. What the budget
|
|
17
|
-
* learns afterwards moves the RATE cap, which is deliberately not here: rate
|
|
18
|
-
* control appears in neither the SPS nor the PPS, so it can move under a player
|
|
19
|
-
* that has already cached the init. The size cannot, which is exactly why the
|
|
20
|
-
* size belongs to the output and the cap belongs to the run.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
export class Output {
|
|
24
|
-
/**
|
|
25
|
-
* @param {object} params
|
|
26
|
-
* @param {number} params.encodeWidth - 0 means the source's own width.
|
|
27
|
-
* @param {number} params.encodeHeight - 0 means the source's own height.
|
|
28
|
-
* @param {number} params.outputFps
|
|
29
|
-
* @param {string | null} params.softwarePreset - The speed setting, where the
|
|
30
|
-
* encoder has a ladder and one was chosen from it.
|
|
31
|
-
* @param {boolean} params.applyTonemap
|
|
32
|
-
*/
|
|
33
|
-
constructor({ encodeWidth, encodeHeight, outputFps, softwarePreset, applyTonemap }) {
|
|
34
|
-
this.encodeWidth = Number.isFinite(encodeWidth) ? encodeWidth : 0;
|
|
35
|
-
this.encodeHeight = Number.isFinite(encodeHeight) ? encodeHeight : 0;
|
|
36
|
-
this.outputFps = Number.isFinite(outputFps) && outputFps > 0 ? outputFps : 0;
|
|
37
|
-
this.softwarePreset = typeof softwarePreset === "string" ? softwarePreset : null;
|
|
38
|
-
this.applyTonemap = applyTonemap === true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* The shapes this proxy has decided, one per output.
|
|
44
|
-
*
|
|
45
|
-
* Keyed by `OutputSpec.toKey()` and by nothing else: the shape is a property of
|
|
46
|
-
* what is being produced, and two requests that produce the same thing must be
|
|
47
|
-
* given the same one however far apart they arrive.
|
|
48
|
-
*/
|
|
49
|
-
export class Outputs {
|
|
50
|
-
/** @type {Map<string, Output>} */
|
|
51
|
-
#byKey = new Map();
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The shape for this output, decided by `decide` the first time it is asked
|
|
55
|
-
* for and never again.
|
|
56
|
-
*
|
|
57
|
-
* @param {string} key
|
|
58
|
-
* @param {() => Output} decide
|
|
59
|
-
* @returns {Output}
|
|
60
|
-
*/
|
|
61
|
-
get(key, decide) {
|
|
62
|
-
let output = this.#byKey.get(key);
|
|
63
|
-
if (!output) {
|
|
64
|
-
output = decide();
|
|
65
|
-
this.#byKey.set(key, output);
|
|
66
|
-
}
|
|
67
|
-
return output;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Drop every shape nobody is holding.
|
|
72
|
-
*
|
|
73
|
-
* Same reason the timelines are swept: a map that only grows is the shape of
|
|
74
|
-
* half the memory faults recorded in this project.
|
|
75
|
-
*
|
|
76
|
-
* @param {Set<Output>} inUse
|
|
77
|
-
* @returns {number}
|
|
78
|
-
*/
|
|
79
|
-
forgetUnused(inUse) {
|
|
80
|
-
let dropped = 0;
|
|
81
|
-
for (const [key, output] of [...this.#byKey]) {
|
|
82
|
-
if (!inUse.has(output)) {
|
|
83
|
-
this.#byKey.delete(key);
|
|
84
|
-
dropped += 1;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return dropped;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** @returns {number} */
|
|
91
|
-
get size() {
|
|
92
|
-
return this.#byKey.size;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a session PRODUCES, stated once and used as its identity.
|
|
3
|
-
*
|
|
4
|
-
* Three domain axes already have a home in this package: what a file states
|
|
5
|
-
* about itself (`container/`), what a track states about itself (`tracks/`),
|
|
6
|
-
* and what anybody wants off the swarm (`demand/`). This is the fourth — what
|
|
7
|
-
* we make out of them — and it had no home at all: the identity of a session
|
|
8
|
-
* was a `[...].join(":")` inside a 300-line function, with no name, no test and
|
|
9
|
-
* nothing printing it.
|
|
10
|
-
*
|
|
11
|
-
* The rule it exists to express, stated by the user 2026-09-03:
|
|
12
|
-
*
|
|
13
|
-
* > A session belongs to the tracks its output actually carries. Its key holds
|
|
14
|
-
* > those tracks' parameters and nothing else. Two sessions whose parameters
|
|
15
|
-
* > agree ARE the same session, and the encoded result is reused by definition.
|
|
16
|
-
*
|
|
17
|
-
* So reuse between viewers is not a feature built on top of this — it is what a
|
|
18
|
-
* correctly built key already means. A field of the REQUEST that does not
|
|
19
|
-
* change one byte of the output must not appear here; measured 2026-09-03, two
|
|
20
|
-
* viewers of one copied picture got two sessions whose output was identical
|
|
21
|
-
* byte for byte (`research/two-viewers-one-file-2026-09-03.md`).
|
|
22
|
-
*
|
|
23
|
-
* What deliberately does NOT appear:
|
|
24
|
-
*
|
|
25
|
-
* 1. the viewer, in any form. Not the consumer id, not where they started, not
|
|
26
|
-
* their viewport. A viewer is not a property of the material;
|
|
27
|
-
* 2. the bitrate ceiling the viewer's measured link puts on a re-encode. It is
|
|
28
|
-
* a runtime parameter of a SHARED session — the worst link among the live
|
|
29
|
-
* consumers decides — so two viewers on one encode is the design, not a
|
|
30
|
-
* collision. Rate control appears in neither the SPS nor the PPS, which is
|
|
31
|
-
* why it can move under a player that has already cached the init;
|
|
32
|
-
* 3. the video track number. Only `0:v:0` is ever mapped, so the file names the
|
|
33
|
-
* picture. Add it here the day a second video track can be chosen.
|
|
34
|
-
*
|
|
35
|
-
* A limit worth stating: two specs that agree name interchangeable output
|
|
36
|
-
* within ONE proxy. A copied picture's bytes depend only on the source, but a
|
|
37
|
-
* re-encoded one's depend on this host's encoder, its preset and its rate cap —
|
|
38
|
-
* so this is not enough to reuse segments BETWEEN proxies (roadmap item 41).
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The picture an output carries.
|
|
43
|
-
*/
|
|
44
|
-
export class VideoOutput {
|
|
45
|
-
/**
|
|
46
|
-
* @param {object} params
|
|
47
|
-
* @param {number} params.fileIndex - The file the picture is read from.
|
|
48
|
-
* @param {{ width: number, height: number, exactSize: boolean } | null} params.encode
|
|
49
|
-
* Null when the picture is copied — then the output is the source's own
|
|
50
|
-
* size and nothing about a target box can change it. The box when it is
|
|
51
|
-
* re-encoded, with `exactSize` saying that box is produced exactly as asked
|
|
52
|
-
* and the realtime budget must not move it.
|
|
53
|
-
*
|
|
54
|
-
* `exactSize` says NOTHING ABOUT WHO ASKED. Every rung of a master playlist
|
|
55
|
-
* sets it, whether the viewer picked that rung or the player moved itself,
|
|
56
|
-
* because two rungs allowed to drift would land on one height and the
|
|
57
|
-
* choice between them would mean nothing. It was called `manual` until
|
|
58
|
-
* 2026-09-13, and that name was read as "the viewer did this" four times
|
|
59
|
-
* over a few weeks — in field logs where the viewer had touched nothing.
|
|
60
|
-
*/
|
|
61
|
-
constructor({ fileIndex, encode = null }) {
|
|
62
|
-
this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
|
|
63
|
-
this.encode = encode
|
|
64
|
-
? {
|
|
65
|
-
width: Number.isInteger(encode.width) && encode.width > 0 ? encode.width : 0,
|
|
66
|
-
height: Number.isInteger(encode.height) && encode.height > 0 ? encode.height : 0,
|
|
67
|
-
exactSize: encode.exactSize === true
|
|
68
|
-
}
|
|
69
|
-
: null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @returns {string}
|
|
74
|
-
*/
|
|
75
|
-
toKey() {
|
|
76
|
-
if (!this.encode) {
|
|
77
|
-
return `v=${this.fileIndex}/copy`;
|
|
78
|
-
}
|
|
79
|
-
const box = `${this.encode.width}x${this.encode.height}`;
|
|
80
|
-
return `v=${this.fileIndex}/enc:${box}:${this.encode.exactSize ? "exact" : "budget"}`;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The soundtrack an output carries.
|
|
86
|
-
*/
|
|
87
|
-
export class AudioOutput {
|
|
88
|
-
/**
|
|
89
|
-
* @param {object} params
|
|
90
|
-
* @param {number} params.fileIndex - The file the TRACK lives in, which for a
|
|
91
|
-
* dub shipped beside the picture is not the picture's file.
|
|
92
|
-
* @param {number} params.trackIndex - `0:a:N` inside that file. The flat
|
|
93
|
-
* number the browser sends spans the picture's own tracks and the files
|
|
94
|
-
* beside it, and two flat numbers of two different pictures can name one
|
|
95
|
-
* track; this is the number that cannot.
|
|
96
|
-
* @param {boolean} params.transcode - Re-encoded to AAC, or copied.
|
|
97
|
-
*/
|
|
98
|
-
constructor({ fileIndex, trackIndex, transcode }) {
|
|
99
|
-
this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
|
|
100
|
-
this.trackIndex = Number.isInteger(trackIndex) && trackIndex >= 0 ? trackIndex : 0;
|
|
101
|
-
this.transcode = transcode === true;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* @returns {string}
|
|
106
|
-
*/
|
|
107
|
-
toKey() {
|
|
108
|
-
return `a=${this.fileIndex}/${this.trackIndex}/${this.transcode ? "aac" : "copy"}`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Where an output is cut.
|
|
114
|
-
*
|
|
115
|
-
* Both forms belong to a FILE and not to the session: the keyframe grid is that
|
|
116
|
-
* file's own keyframe times, and the uniform grid is derived from that file's
|
|
117
|
-
* duration. A soundtrack takes the grid of the picture it accompanies, so its
|
|
118
|
-
* grid names the picture's file — which is what makes two soundtrack sessions
|
|
119
|
-
* cut for two different pictures tell themselves apart.
|
|
120
|
-
*/
|
|
121
|
-
export class CutGrid {
|
|
122
|
-
/**
|
|
123
|
-
* @param {object} params
|
|
124
|
-
* @param {"keyframe" | "uniform"} params.kind
|
|
125
|
-
* @param {number} params.fileIndex - Whose keyframes, or whose duration.
|
|
126
|
-
*/
|
|
127
|
-
constructor({ kind, fileIndex }) {
|
|
128
|
-
this.kind = kind === "keyframe" ? "keyframe" : "uniform";
|
|
129
|
-
this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @returns {string}
|
|
134
|
-
*/
|
|
135
|
-
toKey() {
|
|
136
|
-
return `grid=${this.kind === "keyframe" ? "kf" : "even"}@${this.fileIndex}`;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* One encode of one torrent's material: which tracks, in what form, cut how,
|
|
142
|
-
* packaged how.
|
|
143
|
-
*/
|
|
144
|
-
export class OutputSpec {
|
|
145
|
-
/**
|
|
146
|
-
* @param {object} params
|
|
147
|
-
* @param {string} params.sourceKey - `torrent:<infohash>`, the canonical
|
|
148
|
-
* identity of the torrent itself: a magnet and a `.torrent` file for the
|
|
149
|
-
* same content produce the same one (`torrent-source-key.js`). Nothing
|
|
150
|
-
* further is needed to say WHICH film this is.
|
|
151
|
-
* @param {string} params.segmentFormatId - fMP4 or MPEG-TS. Two viewers
|
|
152
|
-
* asking for different containers cannot share one ffmpeg.
|
|
153
|
-
* @param {CutGrid} params.grid
|
|
154
|
-
* @param {VideoOutput | null} params.video
|
|
155
|
-
* @param {AudioOutput | null} params.audio
|
|
156
|
-
*/
|
|
157
|
-
constructor({ sourceKey, segmentFormatId, grid, video = null, audio = null }) {
|
|
158
|
-
this.sourceKey = String(sourceKey ?? "");
|
|
159
|
-
this.segmentFormatId = String(segmentFormatId ?? "");
|
|
160
|
-
this.grid = grid;
|
|
161
|
-
this.video = video;
|
|
162
|
-
this.audio = audio;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* What this output carries, in the vocabulary the rest of the class uses.
|
|
167
|
-
*
|
|
168
|
-
* `muxed` is the one case where a session legitimately holds the parameters
|
|
169
|
-
* of two tracks: a browser that does not understand rendition groups must be
|
|
170
|
-
* sent its sound inside the picture's own stream.
|
|
171
|
-
*
|
|
172
|
-
* @returns {"video-only" | "audio-only" | "muxed" | "empty"}
|
|
173
|
-
*/
|
|
174
|
-
get carries() {
|
|
175
|
-
if (this.video && this.audio) {
|
|
176
|
-
return "muxed";
|
|
177
|
-
}
|
|
178
|
-
if (this.video) {
|
|
179
|
-
return "video-only";
|
|
180
|
-
}
|
|
181
|
-
if (this.audio) {
|
|
182
|
-
return "audio-only";
|
|
183
|
-
}
|
|
184
|
-
return "empty";
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* The identity. Two outputs with the same one are the same output.
|
|
189
|
-
*
|
|
190
|
-
* @returns {string}
|
|
191
|
-
*/
|
|
192
|
-
toKey() {
|
|
193
|
-
const parts = [this.sourceKey, `fmt=${this.segmentFormatId}`, this.grid.toKey(), this.carries];
|
|
194
|
-
if (this.video) {
|
|
195
|
-
parts.push(this.video.toKey());
|
|
196
|
-
}
|
|
197
|
-
if (this.audio) {
|
|
198
|
-
parts.push(this.audio.toKey());
|
|
199
|
-
}
|
|
200
|
-
return parts.join(":");
|
|
201
|
-
}
|
|
202
|
-
}
|
package/services/output/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { OutputSpec, VideoOutput, AudioOutput, CutGrid } from "./OutputSpec.js";
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The heights a picture may be offered at.
|
|
3
|
-
*
|
|
4
|
-
* A fact about an output and about nothing else: it takes a source height and
|
|
5
|
-
* answers with heights. What the machine can afford of them is the quality
|
|
6
|
-
* budget's question, and what a player may splice between is `LiveOutputs`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// The resolutions a viewer may choose between. Only rungs at or below the
|
|
10
|
-
// source are offered: upscaling invents detail and costs the encoder more than
|
|
11
|
-
// the source itself.
|
|
12
|
-
const VARIANT_LADDER = [2160, 1440, 1080, 720, 540, 480, 360, 240];
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The heights offered for a source of this height, largest first.
|
|
16
|
-
*
|
|
17
|
-
* @param {number} sourceHeight
|
|
18
|
-
* @returns {number[]}
|
|
19
|
-
*/
|
|
20
|
-
export function variantHeightsFor(sourceHeight) {
|
|
21
|
-
if (!Number.isFinite(sourceHeight) || sourceHeight <= 0) {
|
|
22
|
-
return [];
|
|
23
|
-
}
|
|
24
|
-
const rungs = VARIANT_LADDER.filter((height) => height < sourceHeight);
|
|
25
|
-
return [Math.round(sourceHeight), ...rungs];
|
|
26
|
-
}
|