@torrent-tv/proxy 2.87.10 → 2.87.12
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/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2191 -2181
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +124 -101
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +136 -129
- package/routes/api/transcode-sessions/post.js +173 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +758 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1141 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +315 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +245 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +727 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +620 -0
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +992 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +365 -365
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +682 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +323 -0
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,160 +1,173 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create or return an existing HLS transcode session for a torrent file.
|
|
3
|
-
*
|
|
4
|
-
* POST /api/transcode-sessions
|
|
5
|
-
*
|
|
6
|
-
* @param {import("fastify").FastifyRequest} req
|
|
7
|
-
* @param {import("fastify").FastifyReply} reply
|
|
8
|
-
* @param {{ viewerRequests: object, renditions: object, quality: object, outputs: object, lookaheadSeconds: number, sourceRegistry: object, torrentPool: object }} deps
|
|
9
|
-
* @returns {Promise<void>}
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { logger } from "../../../utils/logger.js";
|
|
13
|
-
import { OUTPUT_NO_CAPACITY, OUTPUT_UNAVAILABLE } from "../../../services/encode/output/index.js";
|
|
14
|
-
import { replyNoCapacity, replyOutputUnavailable } from "../../../services/server/http-responses.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extract a plain object from the request body, guarding against
|
|
18
|
-
* non-object payloads (arrays, primitives, null).
|
|
19
|
-
*
|
|
20
|
-
* @param {unknown} body
|
|
21
|
-
* @returns {Record<string, unknown>}
|
|
22
|
-
*/
|
|
23
|
-
function getPayload(body) {
|
|
24
|
-
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
25
|
-
return body;
|
|
26
|
-
}
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export async function handleApiTranscodeSessionsPost(req, reply, { viewerRequests, renditions, quality, outputs, lookaheadSeconds, sourceRegistry, torrentPool }) {
|
|
32
|
-
const payload = getPayload(req.body);
|
|
33
|
-
const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
|
|
34
|
-
const fileIndex = Number(payload.fileIndex);
|
|
35
|
-
const transcodeVideo = payload.transcodeVideo === true;
|
|
36
|
-
const transcodeAudio = payload.transcodeAudio === true;
|
|
37
|
-
const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
|
|
38
|
-
const fileName = typeof payload.fileName === "string" ? payload.fileName.trim() : "";
|
|
39
|
-
const targetWidth = Number(payload.targetWidth);
|
|
40
|
-
const targetHeight = Number(payload.targetHeight);
|
|
41
|
-
// The picture as the viewer sees it, in physical pixels. For a re-encoded
|
|
42
|
-
// picture the height made is the smallest rung whose frame is not smaller
|
|
43
|
-
// than this (roadmap item 98); it takes the place of a target box.
|
|
44
|
-
const visibleWidth = Number(payload.visiblePicture?.width);
|
|
45
|
-
const visibleHeight = Number(payload.visiblePicture?.height);
|
|
46
|
-
const visiblePicture = visibleWidth > 0 && visibleHeight > 0
|
|
47
|
-
? { width: Math.round(visibleWidth), height: Math.round(visibleHeight) }
|
|
48
|
-
: null;
|
|
49
|
-
// The target box is produced exactly (capped to source), with the realtime
|
|
50
|
-
// budget's auto-downscale and runtime downswitch off for this session.
|
|
51
|
-
//
|
|
52
|
-
// It says nothing about WHO asked — a rung sets it whether the viewer picked
|
|
53
|
-
// that rung or the player moved itself onto one.
|
|
54
|
-
const exactSize = payload.exactSize === true;
|
|
55
|
-
// Whether this browser will take its audio from a separate rendition group in
|
|
56
|
-
// the master playlist rather than muxed into the picture. It has to say so:
|
|
57
|
-
// publishing renditions AND muxing the same audio would play it twice, while
|
|
58
|
-
// a browser that does not know about them would get no sound at all.
|
|
59
|
-
const audioRenditions = payload.audioRenditions === true;
|
|
60
|
-
const startPositionSeconds = Number(payload.startPositionSeconds);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Create or return an existing HLS transcode session for a torrent file.
|
|
3
|
+
*
|
|
4
|
+
* POST /api/transcode-sessions
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ viewerRequests: object, renditions: object, quality: object, outputs: object, lookaheadSeconds: number, sourceRegistry: object, torrentPool: object }} deps
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { logger } from "../../../utils/logger.js";
|
|
13
|
+
import { OUTPUT_NO_CAPACITY, OUTPUT_UNAVAILABLE } from "../../../services/encode/output/index.js";
|
|
14
|
+
import { replyNoCapacity, replyOutputUnavailable } from "../../../services/server/http-responses.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract a plain object from the request body, guarding against
|
|
18
|
+
* non-object payloads (arrays, primitives, null).
|
|
19
|
+
*
|
|
20
|
+
* @param {unknown} body
|
|
21
|
+
* @returns {Record<string, unknown>}
|
|
22
|
+
*/
|
|
23
|
+
function getPayload(body) {
|
|
24
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
25
|
+
return body;
|
|
26
|
+
}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export async function handleApiTranscodeSessionsPost(req, reply, { viewerRequests, renditions, quality, outputs, lookaheadSeconds, sourceRegistry, torrentPool }) {
|
|
32
|
+
const payload = getPayload(req.body);
|
|
33
|
+
const sourceKey = typeof payload.sourceKey === "string" ? payload.sourceKey.trim() : "";
|
|
34
|
+
const fileIndex = Number(payload.fileIndex);
|
|
35
|
+
const transcodeVideo = payload.transcodeVideo === true;
|
|
36
|
+
const transcodeAudio = payload.transcodeAudio === true;
|
|
37
|
+
const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
|
|
38
|
+
const fileName = typeof payload.fileName === "string" ? payload.fileName.trim() : "";
|
|
39
|
+
const targetWidth = Number(payload.targetWidth);
|
|
40
|
+
const targetHeight = Number(payload.targetHeight);
|
|
41
|
+
// The picture as the viewer sees it, in physical pixels. For a re-encoded
|
|
42
|
+
// picture the height made is the smallest rung whose frame is not smaller
|
|
43
|
+
// than this (roadmap item 98); it takes the place of a target box.
|
|
44
|
+
const visibleWidth = Number(payload.visiblePicture?.width);
|
|
45
|
+
const visibleHeight = Number(payload.visiblePicture?.height);
|
|
46
|
+
const visiblePicture = visibleWidth > 0 && visibleHeight > 0
|
|
47
|
+
? { width: Math.round(visibleWidth), height: Math.round(visibleHeight) }
|
|
48
|
+
: null;
|
|
49
|
+
// The target box is produced exactly (capped to source), with the realtime
|
|
50
|
+
// budget's auto-downscale and runtime downswitch off for this session.
|
|
51
|
+
//
|
|
52
|
+
// It says nothing about WHO asked — a rung sets it whether the viewer picked
|
|
53
|
+
// that rung or the player moved itself onto one.
|
|
54
|
+
const exactSize = payload.exactSize === true;
|
|
55
|
+
// Whether this browser will take its audio from a separate rendition group in
|
|
56
|
+
// the master playlist rather than muxed into the picture. It has to say so:
|
|
57
|
+
// publishing renditions AND muxing the same audio would play it twice, while
|
|
58
|
+
// a browser that does not know about them would get no sound at all.
|
|
59
|
+
const audioRenditions = payload.audioRenditions === true;
|
|
60
|
+
const startPositionSeconds = Number(payload.startPositionSeconds);
|
|
61
|
+
const viewerLinkMbps = Number(payload.viewerLinkMbps);
|
|
62
|
+
const linkSampleMbps = Number(payload.linkSampleMbps);
|
|
63
|
+
const linkSampleAt = Number(payload.linkSampleAt);
|
|
64
|
+
const linkSampleAgeMs = Number(payload.linkSampleAgeMs);
|
|
65
|
+
const bufferedAheadSec = Number(payload.bufferedAheadSec);
|
|
66
|
+
const bufferLimitSeconds = Number(payload.bufferLimitSeconds);
|
|
67
|
+
const audioTrackIndex = Number(payload.audioTrackIndex);
|
|
68
|
+
// Which container to produce. The browser knows what its media stack will
|
|
69
|
+
// accept for the tracks it asked to be copied; an absent or unknown value
|
|
70
|
+
// leaves the proxy's own `--segment-format` in charge.
|
|
71
|
+
const segmentFormatId =
|
|
72
|
+
typeof payload.segmentFormat === "string" ? payload.segmentFormat.trim() : "";
|
|
73
|
+
|
|
74
|
+
if (!sourceKey || !Number.isInteger(fileIndex) || fileIndex < 0) {
|
|
75
|
+
return reply.code(400).send({ error: "sourceKey and valid fileIndex are required." });
|
|
76
|
+
}
|
|
77
|
+
// The output is opened FOR somebody, and a viewer always has a name.
|
|
78
|
+
if (!consumerId) {
|
|
79
|
+
return reply.code(400).send({ error: "consumerId is required." });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const session = await viewerRequests.createOrGetSession({
|
|
84
|
+
sourceKey,
|
|
85
|
+
fileIndex,
|
|
86
|
+
transcodeVideo,
|
|
87
|
+
transcodeAudio,
|
|
88
|
+
consumerId,
|
|
89
|
+
fileName,
|
|
90
|
+
targetWidth: Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : 0,
|
|
91
|
+
targetHeight: Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : 0,
|
|
92
|
+
exactSize,
|
|
93
|
+
visiblePicture,
|
|
94
|
+
audioRenditions,
|
|
95
|
+
startPositionSeconds:
|
|
96
|
+
Number.isFinite(startPositionSeconds) && startPositionSeconds > 0
|
|
97
|
+
? startPositionSeconds
|
|
98
|
+
: 0,
|
|
99
|
+
viewerLinkMbps: Number.isFinite(viewerLinkMbps) && viewerLinkMbps > 0 ? viewerLinkMbps : null,
|
|
100
|
+
linkSampleMbps: Number.isFinite(linkSampleMbps) && linkSampleMbps > 0 ? linkSampleMbps : null,
|
|
101
|
+
linkSampleAt: Number.isFinite(linkSampleAt) && linkSampleAt > 0 ? linkSampleAt : null,
|
|
102
|
+
linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null,
|
|
103
|
+
bufferedAheadSec: Number.isFinite(bufferedAheadSec) && bufferedAheadSec >= 0 ? bufferedAheadSec : 0,
|
|
104
|
+
bufferLimitSeconds:
|
|
105
|
+
Number.isFinite(bufferLimitSeconds) && bufferLimitSeconds > 0 ? bufferLimitSeconds : null,
|
|
106
|
+
audioTrackIndex:
|
|
107
|
+
Number.isInteger(audioTrackIndex) && audioTrackIndex > 0 ? audioTrackIndex : 0,
|
|
108
|
+
segmentFormatId
|
|
109
|
+
});
|
|
110
|
+
// The index of quality variants, when this session has more than one to
|
|
111
|
+
// offer. Its presence is what tells the browser it can change quality
|
|
112
|
+
// without a new session: the player switches variants itself, appending the
|
|
113
|
+
// new one after what is already buffered. Absent for a copied video, whose
|
|
114
|
+
// segments are cut at the source's own keyframes and so cannot be spliced
|
|
115
|
+
// with a re-encoded rung.
|
|
116
|
+
const hasVariants = renditions.buildMasterPlaylist(session.id) !== null;
|
|
117
|
+
return reply.send({
|
|
118
|
+
sessionId: session.id,
|
|
119
|
+
playlistPath: `/transcode/${session.id}/index.m3u8`,
|
|
120
|
+
...(hasVariants
|
|
121
|
+
? {
|
|
122
|
+
masterPath: `/transcode/${session.id}/master.m3u8`,
|
|
123
|
+
// Which of the master's variants this session IS. The browser pins
|
|
124
|
+
// the player to it, so loading the master costs nothing: an encoder
|
|
125
|
+
// is already producing that height, and any other rung would be a
|
|
126
|
+
// second cold start before the first frame.
|
|
127
|
+
variantHeight: outputs.variantHeightOf(session)
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
130
|
+
// The heights this host will actually serve this file at, largest first
|
|
131
|
+
// — the ladder minus every rung it cannot produce faster than it is
|
|
132
|
+
// watched. The browser needs it whether or not a master exists: without
|
|
133
|
+
// it, it fell back to a ladder of its own invention and offered rungs the
|
|
134
|
+
// proxy had just refused, and picking one re-opened the session at a
|
|
135
|
+
// height measured at a third of realtime.
|
|
136
|
+
offeredHeights: quality.offeredHeights(session),
|
|
137
|
+
// What this session's output will carry, stated rather than left to be
|
|
138
|
+
// discovered. The browser checks what it actually got against this: a
|
|
139
|
+
// track that never arrives is otherwise noticed only by its absence,
|
|
140
|
+
// minutes later, as a black picture with working sound.
|
|
141
|
+
tracks: renditions.declaredTracks(session),
|
|
142
|
+
// How far ahead of the viewer this proxy lets the encoder run, in seconds
|
|
143
|
+
// of playback. The browser sizes its own forward buffer from it, so the
|
|
144
|
+
// two sides agree by construction instead of each carrying a constant of
|
|
145
|
+
// its own — which is how the browser came to hold thirty seconds while
|
|
146
|
+
// two minutes stood produced on disk (roadmap item 4).
|
|
147
|
+
lookaheadSeconds: lookaheadSeconds
|
|
148
|
+
});
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof Error && error.code === "TRANSCODE_DISABLED") {
|
|
151
|
+
return reply.code(409).send({ error: error.message });
|
|
152
|
+
}
|
|
153
|
+
if (error?.code === OUTPUT_UNAVAILABLE) {
|
|
154
|
+
return replyOutputUnavailable(reply, error.details);
|
|
155
|
+
}
|
|
156
|
+
if (error?.code === OUTPUT_NO_CAPACITY) {
|
|
157
|
+
return replyNoCapacity(reply, error.details);
|
|
158
|
+
}
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
160
|
+
// Say why on the proxy's own log, not only in the answer. This route
|
|
161
|
+
// answered 500 for every viewer of proxy 2.9.101-2.9.102 (an undeclared
|
|
162
|
+
// constant) and the addon log carried nothing but the data-channel layer's
|
|
163
|
+
// bare "→ 500": the cause had to be recovered by replaying the request
|
|
164
|
+
// against the live proxy. The stack is worth the two lines it costs — a
|
|
165
|
+
// programming error here is invisible to the viewer, who only sees that
|
|
166
|
+
// nothing plays.
|
|
167
|
+
logger.error(
|
|
168
|
+
`transcode-sessions: ${sourceKey}:${fileIndex} failed to prepare: ${message}\n` +
|
|
169
|
+
`${error instanceof Error ? (error.stack ?? "") : ""}`
|
|
170
|
+
);
|
|
171
|
+
return reply.code(500).send({ error: `Failed to prepare transcode session: ${message}` });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Return the current encoding progress for an active HLS transcode session.
|
|
3
|
-
*
|
|
4
|
-
* GET /api/transcode-sessions/:sessionId/progress
|
|
5
|
-
*
|
|
6
|
-
* @param {import("fastify").FastifyRequest} req
|
|
7
|
-
* @param {import("fastify").FastifyReply} reply
|
|
8
|
-
* @param {{ viewerRequests: object }} deps
|
|
9
|
-
* @returns {Promise<void>}
|
|
10
|
-
*/
|
|
11
|
-
export async function handleApiTranscodeSessionsProgressGet(req, reply, { viewerRequests }) {
|
|
12
|
-
const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
|
|
13
|
-
if (!sessionId) {
|
|
14
|
-
return reply.code(400).send({ error: "sessionId is required." });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Whose progress. One picture serves everyone watching it, and after a
|
|
18
|
-
// quality change the stream on screen is another session — a different one
|
|
19
|
-
// for each viewer who changed.
|
|
20
|
-
const consumerId = typeof req.query?.consumer === "string" ? req.query.consumer : "";
|
|
21
|
-
const progress = await viewerRequests.getSessionProgress(sessionId, consumerId);
|
|
22
|
-
if (!progress) {
|
|
23
|
-
return reply.code(404).send({ error: "Transcode session was not found." });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return reply.send(progress);
|
|
27
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Return the current encoding progress for an active HLS transcode session.
|
|
3
|
+
*
|
|
4
|
+
* GET /api/transcode-sessions/:sessionId/progress
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ viewerRequests: object }} deps
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
export async function handleApiTranscodeSessionsProgressGet(req, reply, { viewerRequests }) {
|
|
12
|
+
const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
|
|
13
|
+
if (!sessionId) {
|
|
14
|
+
return reply.code(400).send({ error: "sessionId is required." });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Whose progress. One picture serves everyone watching it, and after a
|
|
18
|
+
// quality change the stream on screen is another session — a different one
|
|
19
|
+
// for each viewer who changed.
|
|
20
|
+
const consumerId = typeof req.query?.consumer === "string" ? req.query.consumer : "";
|
|
21
|
+
const progress = await viewerRequests.getSessionProgress(sessionId, consumerId);
|
|
22
|
+
if (!progress) {
|
|
23
|
+
return reply.code(404).send({ error: "Transcode session was not found." });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return reply.send(progress);
|
|
27
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Release a consumer from a transcode session.
|
|
3
|
-
* When the last consumer is released the session is disposed automatically.
|
|
4
|
-
*
|
|
5
|
-
* POST /api/transcode-sessions/:sessionId/release
|
|
6
|
-
*
|
|
7
|
-
* @param {import("fastify").FastifyRequest} req
|
|
8
|
-
* @param {import("fastify").FastifyReply} reply
|
|
9
|
-
* @param {{ lifecycle: object }} deps
|
|
10
|
-
* @returns {Promise<void>}
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Extract a plain object from the request body, guarding against
|
|
15
|
-
* non-object payloads (arrays, primitives, null).
|
|
16
|
-
*
|
|
17
|
-
* @param {unknown} body
|
|
18
|
-
* @returns {Record<string, unknown>}
|
|
19
|
-
*/
|
|
20
|
-
function getPayload(body) {
|
|
21
|
-
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
22
|
-
return body;
|
|
23
|
-
}
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function handleApiTranscodeSessionReleasePost(req, reply, { lifecycle }) {
|
|
28
|
-
const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
|
|
29
|
-
const payload = getPayload(req.body);
|
|
30
|
-
const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
|
|
31
|
-
const reason = typeof payload.reason === "string" ? payload.reason.trim() : "";
|
|
32
|
-
if (!sessionId || !consumerId) {
|
|
33
|
-
return reply.code(400).send({ error: "sessionId and consumerId are required." });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const released = await lifecycle.releaseSessionConsumer(sessionId, consumerId, reason);
|
|
37
|
-
if (!released) {
|
|
38
|
-
return reply.code(404).send({ error: "Transcode session was not found." });
|
|
39
|
-
}
|
|
40
|
-
return reply.send({ ok: true });
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Release a consumer from a transcode session.
|
|
3
|
+
* When the last consumer is released the session is disposed automatically.
|
|
4
|
+
*
|
|
5
|
+
* POST /api/transcode-sessions/:sessionId/release
|
|
6
|
+
*
|
|
7
|
+
* @param {import("fastify").FastifyRequest} req
|
|
8
|
+
* @param {import("fastify").FastifyReply} reply
|
|
9
|
+
* @param {{ lifecycle: object }} deps
|
|
10
|
+
* @returns {Promise<void>}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract a plain object from the request body, guarding against
|
|
15
|
+
* non-object payloads (arrays, primitives, null).
|
|
16
|
+
*
|
|
17
|
+
* @param {unknown} body
|
|
18
|
+
* @returns {Record<string, unknown>}
|
|
19
|
+
*/
|
|
20
|
+
function getPayload(body) {
|
|
21
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
22
|
+
return body;
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleApiTranscodeSessionReleasePost(req, reply, { lifecycle }) {
|
|
28
|
+
const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
|
|
29
|
+
const payload = getPayload(req.body);
|
|
30
|
+
const consumerId = typeof payload.consumerId === "string" ? payload.consumerId.trim() : "";
|
|
31
|
+
const reason = typeof payload.reason === "string" ? payload.reason.trim() : "";
|
|
32
|
+
if (!sessionId || !consumerId) {
|
|
33
|
+
return reply.code(400).send({ error: "sessionId and consumerId are required." });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const released = await lifecycle.releaseSessionConsumer(sessionId, consumerId, reason);
|
|
37
|
+
if (!released) {
|
|
38
|
+
return reply.code(404).send({ error: "Transcode session was not found." });
|
|
39
|
+
}
|
|
40
|
+
return reply.send({ ok: true });
|
|
41
|
+
}
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file POST /api/transcode-sessions/:sessionId/seek — the viewer's seek target.
|
|
3
|
-
*
|
|
4
|
-
* The browser reports WHERE THE VIEWER ACTUALLY SEEKED, explicitly, the moment
|
|
5
|
-
* the scrub ends. This is the only authoritative source of that intent: the
|
|
6
|
-
* position lives in the browser (`video.currentTime`) and nowhere else.
|
|
7
|
-
*
|
|
8
|
-
* Why this route exists at all: our synthetic VOD playlist advertises every
|
|
9
|
-
* segment of the file, but segments only exist once ffmpeg has produced them.
|
|
10
|
-
* A player is entitled by the HLS contract to fetch any advertised segment and
|
|
11
|
-
* get it immediately, so when one 503s it legitimately probes elsewhere —
|
|
12
|
-
* field log 2026-08-02 shows a correct target burst (#973..#985, the real seek)
|
|
13
|
-
* followed by scattered probing across the whole file (#125, #173, #251, #326,
|
|
14
|
-
* #478...). Inferring the target from that request stream made the encoder
|
|
15
|
-
* restart at #519 — a probe, not the seek — and the viewer waited ten seconds
|
|
16
|
-
* for a segment nobody wanted. Segment requests are data fetches, not commands;
|
|
17
|
-
* the seek target now arrives here instead of being guessed from them.
|
|
18
|
-
*
|
|
19
|
-
* Mirrors how Jellyfin/Plex handle server-side seeking (an explicit start
|
|
20
|
-
* position from the client), rather than heuristics over request patterns.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {import("fastify").FastifyRequest} req
|
|
25
|
-
* @param {import("fastify").FastifyReply} reply
|
|
26
|
-
* @param {{ viewerRequests: object }} deps
|
|
27
|
-
* @returns {Promise<void>}
|
|
28
|
-
*/
|
|
29
|
-
export async function handleApiTranscodeSessionSeekPost(req, reply, { viewerRequests }) {
|
|
30
|
-
const sessionId = req.params?.sessionId;
|
|
31
|
-
const body = req.body && typeof req.body === "object" ? req.body : {};
|
|
32
|
-
const positionSeconds = Number(body.positionSeconds);
|
|
33
|
-
|
|
34
|
-
if (!Number.isFinite(positionSeconds) || positionSeconds < 0) {
|
|
35
|
-
reply.code(400);
|
|
36
|
-
return reply.send({ error: "positionSeconds must be a non-negative number." });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Who moved. A session can be shared, and the seeking viewer's own position
|
|
40
|
-
// has to move with them or their next request is judged against where they
|
|
41
|
-
// were before the jump.
|
|
42
|
-
const consumerId = typeof body.consumerId === "string" ? body.consumerId.trim() : "";
|
|
43
|
-
if (!consumerId) {
|
|
44
|
-
reply.code(400);
|
|
45
|
-
return reply.send({ error: "consumerId is required." });
|
|
46
|
-
}
|
|
47
|
-
// Which viewing their requests now belong to. The page raises this before it
|
|
48
|
-
// sends anything and stamps every request with it, so a request that crosses
|
|
49
|
-
// this message is still read against the viewing it was made in. A page that
|
|
50
|
-
// states nothing leaves the generation where it is.
|
|
51
|
-
const generation = Number(body.generation);
|
|
52
|
-
const applied = viewerRequests.requestSeek(sessionId, positionSeconds, consumerId, generation);
|
|
53
|
-
if (!applied) {
|
|
54
|
-
// Unknown or disposed session — nothing to steer. Not an error worth
|
|
55
|
-
// surfacing to the viewer: the seek will be handled by whatever session
|
|
56
|
-
// replaces it.
|
|
57
|
-
reply.code(404);
|
|
58
|
-
return reply.send({ error: "No such transcode session." });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
reply.code(204);
|
|
62
|
-
return reply.send();
|
|
63
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file POST /api/transcode-sessions/:sessionId/seek — the viewer's seek target.
|
|
3
|
+
*
|
|
4
|
+
* The browser reports WHERE THE VIEWER ACTUALLY SEEKED, explicitly, the moment
|
|
5
|
+
* the scrub ends. This is the only authoritative source of that intent: the
|
|
6
|
+
* position lives in the browser (`video.currentTime`) and nowhere else.
|
|
7
|
+
*
|
|
8
|
+
* Why this route exists at all: our synthetic VOD playlist advertises every
|
|
9
|
+
* segment of the file, but segments only exist once ffmpeg has produced them.
|
|
10
|
+
* A player is entitled by the HLS contract to fetch any advertised segment and
|
|
11
|
+
* get it immediately, so when one 503s it legitimately probes elsewhere —
|
|
12
|
+
* field log 2026-08-02 shows a correct target burst (#973..#985, the real seek)
|
|
13
|
+
* followed by scattered probing across the whole file (#125, #173, #251, #326,
|
|
14
|
+
* #478...). Inferring the target from that request stream made the encoder
|
|
15
|
+
* restart at #519 — a probe, not the seek — and the viewer waited ten seconds
|
|
16
|
+
* for a segment nobody wanted. Segment requests are data fetches, not commands;
|
|
17
|
+
* the seek target now arrives here instead of being guessed from them.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors how Jellyfin/Plex handle server-side seeking (an explicit start
|
|
20
|
+
* position from the client), rather than heuristics over request patterns.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {import("fastify").FastifyRequest} req
|
|
25
|
+
* @param {import("fastify").FastifyReply} reply
|
|
26
|
+
* @param {{ viewerRequests: object }} deps
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
export async function handleApiTranscodeSessionSeekPost(req, reply, { viewerRequests }) {
|
|
30
|
+
const sessionId = req.params?.sessionId;
|
|
31
|
+
const body = req.body && typeof req.body === "object" ? req.body : {};
|
|
32
|
+
const positionSeconds = Number(body.positionSeconds);
|
|
33
|
+
|
|
34
|
+
if (!Number.isFinite(positionSeconds) || positionSeconds < 0) {
|
|
35
|
+
reply.code(400);
|
|
36
|
+
return reply.send({ error: "positionSeconds must be a non-negative number." });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Who moved. A session can be shared, and the seeking viewer's own position
|
|
40
|
+
// has to move with them or their next request is judged against where they
|
|
41
|
+
// were before the jump.
|
|
42
|
+
const consumerId = typeof body.consumerId === "string" ? body.consumerId.trim() : "";
|
|
43
|
+
if (!consumerId) {
|
|
44
|
+
reply.code(400);
|
|
45
|
+
return reply.send({ error: "consumerId is required." });
|
|
46
|
+
}
|
|
47
|
+
// Which viewing their requests now belong to. The page raises this before it
|
|
48
|
+
// sends anything and stamps every request with it, so a request that crosses
|
|
49
|
+
// this message is still read against the viewing it was made in. A page that
|
|
50
|
+
// states nothing leaves the generation where it is.
|
|
51
|
+
const generation = Number(body.generation);
|
|
52
|
+
const applied = viewerRequests.requestSeek(sessionId, positionSeconds, consumerId, generation);
|
|
53
|
+
if (!applied) {
|
|
54
|
+
// Unknown or disposed session — nothing to steer. Not an error worth
|
|
55
|
+
// surfacing to the viewer: the seek will be handled by whatever session
|
|
56
|
+
// replaces it.
|
|
57
|
+
reply.code(404);
|
|
58
|
+
return reply.send({ error: "No such transcode session." });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
reply.code(204);
|
|
62
|
+
return reply.send();
|
|
63
|
+
}
|
package/routes/health/get.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Basic liveness check for load balancers and container orchestrators.
|
|
3
|
-
*
|
|
4
|
-
* GET /health
|
|
5
|
-
*
|
|
6
|
-
* @param {import("fastify").FastifyRequest} _req
|
|
7
|
-
* @param {import("fastify").FastifyReply} reply
|
|
8
|
-
* @param {{ version: string }} deps
|
|
9
|
-
* @returns {Promise<void>}
|
|
10
|
-
*/
|
|
11
|
-
export async function handleHealthGet(_req, reply, { version } = {}) {
|
|
12
|
-
return reply.send({ ok: true, version });
|
|
13
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Basic liveness check for load balancers and container orchestrators.
|
|
3
|
+
*
|
|
4
|
+
* GET /health
|
|
5
|
+
*
|
|
6
|
+
* @param {import("fastify").FastifyRequest} _req
|
|
7
|
+
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ version: string }} deps
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
export async function handleHealthGet(_req, reply, { version } = {}) {
|
|
12
|
+
return reply.send({ ok: true, version });
|
|
13
|
+
}
|