@torrent-tv/proxy 2.89.7 → 2.89.9

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.
Files changed (498) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +363 -338
  13. package/CHANGELOG.md +2345 -2335
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/calibration/audio/NOTICE.md +19 -19
  19. package/assets/calibration/audio/manifest.json +98 -98
  20. package/assets/diagnostics/sctpstate.gdb +31 -31
  21. package/bin/cli.js +684 -684
  22. package/bin/thread-pool.js +30 -30
  23. package/biome.json +412 -412
  24. package/docs/browser-proxy-contract.md +155 -155
  25. package/docs/container-architecture.md +273 -273
  26. package/docs/download-architecture.md +261 -261
  27. package/docs/encode-architecture.md +765 -765
  28. package/docs/encode-run-state.md +96 -96
  29. package/docs/logs.md +97 -97
  30. package/docs/playback-start-forecast.md +113 -113
  31. package/docs/socks5-egress-research.md +114 -114
  32. package/docs/storage-architecture.md +273 -273
  33. package/knip.json +14 -14
  34. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  35. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  36. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  37. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  38. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  39. package/openspec/changes/binary-distribution/proposal.md +53 -53
  40. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  41. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  42. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  43. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  44. package/openspec/changes/cold-start/design.md +98 -98
  45. package/openspec/changes/cold-start/proposal.md +62 -62
  46. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  47. package/openspec/changes/cold-start/tasks.md +48 -48
  48. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  49. package/openspec/changes/disk-cap/proposal.md +37 -37
  50. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  51. package/openspec/changes/disk-cap/tasks.md +19 -19
  52. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  53. package/openspec/changes/proxy-observability/design.md +38 -38
  54. package/openspec/changes/proxy-observability/proposal.md +49 -49
  55. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  56. package/openspec/changes/proxy-observability/tasks.md +21 -21
  57. package/openspec/changes/seek-debounce/design.md +118 -118
  58. package/openspec/changes/seek-debounce/proposal.md +59 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  60. package/openspec/changes/seek-debounce/tasks.md +33 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  62. package/openspec/changes/subtitle-language/proposal.md +50 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  64. package/openspec/changes/subtitle-language/tasks.md +26 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  66. package/openspec/changes/track-selection/design.md +47 -47
  67. package/openspec/changes/track-selection/proposal.md +45 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  69. package/openspec/changes/track-selection/tasks.md +20 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  71. package/openspec/changes/transcode-quality/proposal.md +51 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  73. package/openspec/changes/transcode-quality/tasks.md +63 -63
  74. package/openspec/config.yaml +38 -38
  75. package/package.json +51 -50
  76. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  77. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  78. package/research/double-spawn-2026-09-10.md +171 -171
  79. package/research/handover-2026-09-13.md +209 -209
  80. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  81. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  82. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  83. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  84. package/routes/api/client-logs/post.js +86 -86
  85. package/routes/api/delivery-sink/get.js +72 -72
  86. package/routes/api/link-probe/get.js +51 -51
  87. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  88. package/routes/api/playback-plan/post.js +53 -53
  89. package/routes/api/sources/files/get.js +106 -106
  90. package/routes/api/sources/post.js +36 -36
  91. package/routes/api/sources/stats/get.js +132 -132
  92. package/routes/api/sources/warm/post.js +217 -217
  93. package/routes/api/subtitles/get.js +230 -230
  94. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  95. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  96. package/routes/api/transcode-sessions/post.js +178 -178
  97. package/routes/api/transcode-sessions/progress/get.js +27 -27
  98. package/routes/api/transcode-sessions/release/post.js +41 -41
  99. package/routes/api/transcode-sessions/seek/post.js +63 -63
  100. package/routes/health/get.js +13 -13
  101. package/routes/healthz/get.js +13 -13
  102. package/routes/stream/get.js +416 -416
  103. package/routes/transcode/audio-file/get.js +64 -64
  104. package/routes/transcode/audio-warm/get.js +95 -95
  105. package/routes/transcode/session-file/get.js +52 -52
  106. package/routes/transcode/variant-file/get.js +95 -95
  107. package/routes/transcode/variant-warm/get.js +84 -84
  108. package/scripts/build-audio-samples.mjs +31 -31
  109. package/scripts/render-run-graph.js +173 -173
  110. package/server.js +748 -748
  111. package/services/encode/CoverageMap.js +428 -428
  112. package/services/encode/CushionReport.js +345 -345
  113. package/services/encode/EncodeAdmission.js +376 -376
  114. package/services/encode/EncodeOrchestrator.js +1192 -1192
  115. package/services/encode/EncodePlan.js +1368 -1368
  116. package/services/encode/EncodeRun.js +36 -12
  117. package/services/encode/Encoder.js +142 -142
  118. package/services/encode/EncoderSelection.js +38 -38
  119. package/services/encode/LocalObservations.js +359 -359
  120. package/services/encode/NvencEncoder.js +50 -50
  121. package/services/encode/OutputOpening.js +784 -784
  122. package/services/encode/OutputTimes.js +579 -579
  123. package/services/encode/QsvEncoder.js +58 -58
  124. package/services/encode/Renditions.js +2283 -2283
  125. package/services/encode/RunClock.js +87 -87
  126. package/services/encode/RunProgress.js +79 -79
  127. package/services/encode/SegmentDemand.js +95 -95
  128. package/services/encode/SoftwareEncoder.js +121 -121
  129. package/services/encode/V4l2m2mEncoder.js +53 -53
  130. package/services/encode/VaapiEncoder.js +66 -66
  131. package/services/encode/args.js +418 -418
  132. package/services/encode/audio-calibration.js +67 -67
  133. package/services/encode/audio-work-rate.js +15 -15
  134. package/services/encode/available-share.js +89 -89
  135. package/services/encode/calibration.js +361 -361
  136. package/services/encode/contention.js +128 -128
  137. package/services/encode/decode-cost-fit.js +363 -363
  138. package/services/encode/encode-exit.js +116 -116
  139. package/services/encode/encode-run-state.js +551 -551
  140. package/services/encode/encoder-readings.js +43 -43
  141. package/services/encode/fingerprint.js +322 -322
  142. package/services/encode/host-load.js +174 -174
  143. package/services/encode/hwaccel.js +1894 -1894
  144. package/services/encode/index.js +9 -9
  145. package/services/encode/learned-median.js +90 -90
  146. package/services/encode/output/EncodedOutput.js +57 -57
  147. package/services/encode/output/Output.js +131 -131
  148. package/services/encode/output/OutputCatalog.js +414 -414
  149. package/services/encode/output/OutputSpec.js +390 -390
  150. package/services/encode/output/Timeline.js +308 -308
  151. package/services/encode/output/cut-grid.js +107 -107
  152. package/services/encode/output/index.js +24 -24
  153. package/services/encode/output/ladder.js +32 -32
  154. package/services/encode/output/playlists.js +284 -284
  155. package/services/encode/output/rates.js +125 -125
  156. package/services/encode/output-key-format.js +21 -21
  157. package/services/encode/quality/EncodeCost.js +1043 -1043
  158. package/services/encode/quality/HostLoad.js +525 -525
  159. package/services/encode/quality/HostTimings.js +249 -249
  160. package/services/encode/quality/OutputQualityState.js +24 -24
  161. package/services/encode/quality/QualityController.js +758 -758
  162. package/services/encode/quality/QualityOffer.js +416 -416
  163. package/services/encode/quality/drain-threat.js +47 -47
  164. package/services/encode/quality/link-budget.js +337 -337
  165. package/services/encode/quality/output-format.js +319 -319
  166. package/services/encode/quality/serving-output.js +216 -216
  167. package/services/encode/quality/visible-rung.js +57 -57
  168. package/services/encode/run-budget.js +97 -97
  169. package/services/encode/run-command.js +757 -757
  170. package/services/encode/run-costs.js +155 -155
  171. package/services/encode/run-inputs.js +53 -53
  172. package/services/encode/segment-formats/fmp4.js +476 -476
  173. package/services/encode/segment-formats/init-compat.js +324 -324
  174. package/services/encode/segment-formats/mpegts.js +134 -134
  175. package/services/encode/start-stop-cost.js +178 -178
  176. package/services/encode/throughput.js +97 -97
  177. package/services/encode/torrent-cost.js +88 -88
  178. package/services/identity/installation-id.js +38 -38
  179. package/services/media/ContainerOrchestrator.js +202 -202
  180. package/services/media/KeyframeTables.js +356 -356
  181. package/services/media/SourceFile.js +298 -298
  182. package/services/media/SubtitleCues.js +592 -592
  183. package/services/media/SubtitleOrchestrator.js +167 -167
  184. package/services/media/audio-inventory.js +299 -299
  185. package/services/media/container/AviContainer.js +268 -268
  186. package/services/media/container/Container.js +632 -632
  187. package/services/media/container/ContainerFactory.js +38 -38
  188. package/services/media/container/KeyframeTable.js +272 -272
  189. package/services/media/container/MatroskaContainer.js +1031 -1031
  190. package/services/media/container/Mp4Container.js +1354 -1354
  191. package/services/media/container/SubtitleFileContainer.js +339 -339
  192. package/services/media/container/ebml-reader.js +159 -159
  193. package/services/media/container/ebml-stream.js +174 -174
  194. package/services/media/container/matroska-clusters.js +1165 -1165
  195. package/services/media/container/unavailable.js +77 -77
  196. package/services/media/ffmpeg-banner.js +260 -260
  197. package/services/media/keyframe-probe.js +157 -157
  198. package/services/media/media-info-probe.js +93 -93
  199. package/services/media/playback-planner.js +931 -931
  200. package/services/media/tracks/AudioTrack.js +143 -143
  201. package/services/media/tracks/ContainerTrack.js +84 -84
  202. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  203. package/services/media/tracks/SubtitleTrack.js +38 -38
  204. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  205. package/services/media/tracks/VideoTrack.js +33 -33
  206. package/services/media/tracks/audio-bound.js +233 -233
  207. package/services/media/tracks/index.js +15 -15
  208. package/services/media/tracks/language-detect.js +174 -174
  209. package/services/server/OutputLifecycle.js +418 -418
  210. package/services/server/SegmentServing.js +1152 -1152
  211. package/services/server/ViewerRequests.js +785 -785
  212. package/services/server/WaitLedger.js +142 -142
  213. package/services/server/controllers/PlaybackController.js +40 -40
  214. package/services/server/controllers/SubtitleController.js +109 -109
  215. package/services/server/http-responses.js +60 -60
  216. package/services/server/transcode-session-files.js +325 -325
  217. package/services/server/wire-outputs.js +1029 -1029
  218. package/services/storage/Diagnostics.js +202 -202
  219. package/services/storage/MachineBudget.js +235 -235
  220. package/services/storage/allowance.js +107 -107
  221. package/services/storage/core-dumps.js +106 -106
  222. package/services/storage/files/CompletedFiles.js +391 -391
  223. package/services/storage/files/piece-from-whole-file.js +118 -118
  224. package/services/storage/free.js +71 -71
  225. package/services/storage/keep.js +48 -48
  226. package/services/storage/machine-memory.js +56 -56
  227. package/services/storage/memory-report.js +575 -575
  228. package/services/storage/piece-store/piece-disk-store.js +539 -539
  229. package/services/storage/piece-store/piece-lru.js +495 -495
  230. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  231. package/services/storage/returns.js +103 -103
  232. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  233. package/services/storage/wire.js +175 -175
  234. package/services/torrent/Contents.js +382 -382
  235. package/services/torrent/demand/DemandRegister.js +202 -202
  236. package/services/torrent/demand/Urgency.js +137 -137
  237. package/services/torrent/demand/Window.js +118 -118
  238. package/services/torrent/demand/index.js +8 -8
  239. package/services/torrent/demand/pieces.js +140 -140
  240. package/services/torrent/download/SwarmSelection.js +386 -386
  241. package/services/torrent/download/registry.js +102 -102
  242. package/services/torrent/download/withdraw-claim.js +80 -80
  243. package/services/torrent/episode-naming.js +182 -182
  244. package/services/torrent/files.js +455 -455
  245. package/services/torrent/naming.js +619 -619
  246. package/services/torrent/piece-store-of.js +29 -29
  247. package/services/torrent/supply-margin.js +264 -264
  248. package/services/torrent/torrent-pool.js +2688 -2688
  249. package/services/torrent/worker/background-fill.js +213 -213
  250. package/services/torrent/worker/channel.js +271 -271
  251. package/services/torrent/worker/client.js +836 -836
  252. package/services/torrent/worker/fastest-wires.js +342 -342
  253. package/services/torrent/worker/handle-state.js +23 -23
  254. package/services/torrent/worker/held-bytes.js +231 -231
  255. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  256. package/services/torrent/worker/piece-reader.js +1078 -1078
  257. package/services/torrent/worker/pool-adapter.js +371 -371
  258. package/services/torrent/worker/protocol.js +184 -184
  259. package/services/torrent/worker/resume-warm.js +107 -107
  260. package/services/torrent/worker/webrtc-shim.js +219 -219
  261. package/services/torrent/worker/worker.js +1137 -1137
  262. package/services/transport/data-channel-handler.js +1406 -1373
  263. package/services/transport/delivery-probe.js +798 -842
  264. package/services/transport/health-collector.js +53 -53
  265. package/services/transport/nat-classifier.js +231 -231
  266. package/services/transport/packet-witness.js +808 -808
  267. package/services/transport/port-mapper.js +235 -235
  268. package/services/transport/registry-api.js +81 -81
  269. package/services/transport/tunnel-client.js +447 -447
  270. package/services/transport/usrsctp-state.js +175 -175
  271. package/services/transport/webrtc-manager.js +484 -484
  272. package/services/viewer/Assignments.js +340 -340
  273. package/services/viewer/PriorityMap.js +421 -421
  274. package/services/viewer/PriorityOrchestrator.js +325 -325
  275. package/services/viewer/Viewer.js +691 -691
  276. package/services/viewer/Viewers.js +368 -368
  277. package/services/viewer/active-output.js +37 -37
  278. package/services/viewer/audio-start.js +46 -46
  279. package/services/viewer/buffer-trend.js +81 -81
  280. package/services/viewer/choices.js +647 -647
  281. package/services/viewer/media-time.js +355 -355
  282. package/services/viewer/playback-readiness.js +635 -635
  283. package/services/viewer/positions.js +126 -126
  284. package/services/viewer/report-intake.js +46 -46
  285. package/store/source-registry.js +71 -71
  286. package/test/asset-paths.test.js +28 -28
  287. package/test/audio-buffer-continuity.test.js +77 -77
  288. package/test/audio-calibration.test.js +23 -23
  289. package/test/audio-inventory.test.js +212 -212
  290. package/test/audio-track-bitrate.test.js +40 -40
  291. package/test/auto-quality-step.test.js +762 -762
  292. package/test/available-share.test.js +76 -76
  293. package/test/background-fill.test.js +190 -190
  294. package/test/client-log-file.test.js +110 -110
  295. package/test/coalesce.test.js +39 -39
  296. package/test/completed-files.test.js +115 -115
  297. package/test/concurrent-cost.test.js +148 -148
  298. package/test/container-media-info.test.js +228 -228
  299. package/test/contention.test.js +76 -76
  300. package/test/core-dumps.test.js +57 -57
  301. package/test/coverage-follows-the-disk.test.js +233 -228
  302. package/test/coverage-map.test.js +195 -195
  303. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  304. package/test/cut-times-timeline.test.js +65 -65
  305. package/test/cuts-follow-published-grid.test.js +109 -109
  306. package/test/data-channel-frame.test.js +63 -63
  307. package/test/data-channel-viewer-lifecycle.test.js +162 -92
  308. package/test/dead-torrent-handle.test.js +52 -52
  309. package/test/declared-rates.test.js +116 -116
  310. package/test/declared-tracks.test.js +43 -43
  311. package/test/decode-cost-fit.test.js +202 -202
  312. package/test/decode-cost.test.js +475 -475
  313. package/test/delivery-probe-field.test.js +173 -0
  314. package/test/delivery-probe.test.js +382 -393
  315. package/test/demand-register.test.js +206 -206
  316. package/test/dht-bootstrap.test.js +72 -72
  317. package/test/drain-threat.test.js +65 -65
  318. package/test/duplicate-add.test.js +48 -48
  319. package/test/encode-admission.test.js +146 -146
  320. package/test/encode-cost.test.js +275 -275
  321. package/test/encode-exit.test.js +111 -111
  322. package/test/encode-orchestrator.test.js +426 -426
  323. package/test/encode-plan-viewers.test.js +725 -725
  324. package/test/encode-plan.test.js +545 -545
  325. package/test/encode-run-state.test.js +314 -314
  326. package/test/encode-run.test.js +20 -0
  327. package/test/encode-slope.test.js +46 -46
  328. package/test/encoded-output.test.js +39 -39
  329. package/test/encoder-kinds.test.js +122 -122
  330. package/test/encoder-readings.test.js +68 -68
  331. package/test/episode-naming.test.js +131 -131
  332. package/test/every-claimant-is-told-its-share.test.js +152 -152
  333. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  334. package/test/failed-start-breaker.test.js +202 -202
  335. package/test/fastest-wires.test.js +143 -143
  336. package/test/ffmpeg-stream-counts.test.js +78 -78
  337. package/test/file-edges.test.js +191 -191
  338. package/test/fixtures/audio-frame-boundary.json +1 -1
  339. package/test/fmp4-self-contained.test.js +80 -80
  340. package/test/fmp4-timestamps.test.js +140 -140
  341. package/test/fmp4-track-completeness.test.js +41 -41
  342. package/test/format-keyed-outputs.test.js +78 -78
  343. package/test/health-metrics.test.js +36 -38
  344. package/test/held-read-ownership.test.js +77 -77
  345. package/test/held-request-width.test.js +157 -157
  346. package/test/helpers/encode-run.js +248 -240
  347. package/test/helpers/held-file.js +77 -77
  348. package/test/helpers/manager.js +62 -62
  349. package/test/helpers/matroska-file.js +296 -296
  350. package/test/helpers/output-spec.js +63 -63
  351. package/test/host-load.test.js +68 -68
  352. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  353. package/test/init-compat.test.js +219 -219
  354. package/test/input-lost-quiets-the-plan.test.js +261 -261
  355. package/test/input-unavailable.test.js +48 -48
  356. package/test/installation-id.test.js +39 -39
  357. package/test/keeping-period.test.js +83 -83
  358. package/test/keyframe-index-accuracy.test.js +188 -188
  359. package/test/keyframe-table-budget.test.js +123 -123
  360. package/test/keyframes-belong-to-the-file.test.js +249 -249
  361. package/test/learned-median.test.js +37 -37
  362. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  363. package/test/limit-row-by-area.test.js +60 -60
  364. package/test/link-budget.test.js +111 -111
  365. package/test/link-probe.test.js +56 -56
  366. package/test/local-encoding-data.test.js +273 -273
  367. package/test/logger-repeats.test.js +134 -134
  368. package/test/look-ahead-coverage.test.js +56 -56
  369. package/test/manager-surface.test.js +291 -291
  370. package/test/matroska-cluster-discovery.test.js +323 -323
  371. package/test/matroska-cues-track.test.js +192 -192
  372. package/test/media-time.test.js +62 -62
  373. package/test/memory-budget.test.js +390 -390
  374. package/test/move-cost.test.js +216 -216
  375. package/test/mp4-subtitles.test.js +173 -173
  376. package/test/one-authority.test.js +285 -285
  377. package/test/one-budget.test.js +149 -149
  378. package/test/one-output-one-session.test.js +273 -273
  379. package/test/one-resource-divided.test.js +185 -185
  380. package/test/one-serving-output-per-height.test.js +458 -458
  381. package/test/orchestrator-wired.test.js +207 -207
  382. package/test/output-catalog.test.js +39 -39
  383. package/test/output-shape.test.js +21 -21
  384. package/test/output-spec.test.js +251 -251
  385. package/test/output-speed.test.js +92 -87
  386. package/test/outputs-being-prepared.test.js +40 -40
  387. package/test/packet-witness-ring.test.js +236 -236
  388. package/test/packet-witness.test.js +148 -148
  389. package/test/piece-disk-store.test.js +401 -401
  390. package/test/piece-from-whole-file.test.js +129 -129
  391. package/test/piece-lru.test.js +319 -319
  392. package/test/piece-reader.test.js +162 -162
  393. package/test/piece-selection-offset.test.js +85 -85
  394. package/test/piece-store-eviction.test.js +595 -595
  395. package/test/piece-store-never-refuses.test.js +153 -153
  396. package/test/piece-store-reservations.test.js +403 -403
  397. package/test/piece-store-slow-disk.test.js +159 -159
  398. package/test/piece-tail.test.js +97 -97
  399. package/test/plan-host-timings.test.js +68 -68
  400. package/test/plan-is-steady.test.js +225 -225
  401. package/test/playback-readiness.test.js +518 -518
  402. package/test/playback-start-regressions.test.js +101 -101
  403. package/test/priority-map-download.test.js +174 -174
  404. package/test/priority-map-emptied.test.js +204 -204
  405. package/test/priority-map-per-output.test.js +215 -215
  406. package/test/priority-map.test.js +163 -163
  407. package/test/probe-wedge-certainty.test.js +60 -60
  408. package/test/produced-copy-choice.test.js +213 -213
  409. package/test/published-timeline.test.js +83 -83
  410. package/test/quality-offer.test.js +160 -160
  411. package/test/quality-variants.test.js +1274 -1270
  412. package/test/rate-control-in-the-output.test.js +181 -180
  413. package/test/read-survives-withdrawal.test.js +164 -164
  414. package/test/read-window.test.js +365 -365
  415. package/test/reads-not-yet-arrived.test.js +218 -218
  416. package/test/resume-warm.test.js +39 -39
  417. package/test/run-graph-drift.test.js +37 -37
  418. package/test/run-inputs.test.js +80 -80
  419. package/test/run-intervals.test.js +100 -100
  420. package/test/run-position-follows-published-grid.test.js +89 -89
  421. package/test/run-progress.test.js +64 -64
  422. package/test/same-height-switch.test.js +663 -663
  423. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  424. package/test/seek-landing.test.js +109 -109
  425. package/test/seek-target-not-superseded.test.js +195 -195
  426. package/test/segment-closed-on-both-branches.test.js +114 -114
  427. package/test/segment-cut-times.test.js +63 -63
  428. package/test/segment-demand.test.js +80 -80
  429. package/test/segment-hold-supersede.test.js +91 -91
  430. package/test/segment-judged-by-its-own-header.test.js +222 -222
  431. package/test/segment-serve-wiring.test.js +481 -481
  432. package/test/segment-store-eviction.test.js +247 -247
  433. package/test/segment-store.test.js +352 -352
  434. package/test/segment-true-start.test.js +86 -86
  435. package/test/segments-are-shared.test.js +176 -176
  436. package/test/send-chunk-bytes.test.js +86 -86
  437. package/test/serving-existing-output.test.js +111 -111
  438. package/test/serving-output.test.js +102 -102
  439. package/test/shared-piece-store.test.js +310 -310
  440. package/test/sidecar-naming.test.js +142 -142
  441. package/test/soundtrack-rate-bound.test.js +289 -289
  442. package/test/source-file.test.js +133 -133
  443. package/test/source-files-route.test.js +113 -113
  444. package/test/source-registry.test.js +18 -18
  445. package/test/split-a-stretch.test.js +139 -139
  446. package/test/stale-request-after-seek.test.js +158 -158
  447. package/test/startup-readings.test.js +124 -124
  448. package/test/stream-route.test.js +237 -237
  449. package/test/subtitle-cue-framing.test.js +200 -200
  450. package/test/subtitle-cue-source.test.js +102 -102
  451. package/test/subtitle-cue-walk.test.js +385 -385
  452. package/test/subtitle-cursor.test.js +63 -63
  453. package/test/subtitle-defaults.test.js +118 -118
  454. package/test/subtitle-language.test.js +252 -252
  455. package/test/subtitle-metadata.test.js +15 -15
  456. package/test/subtitle-plan-waits.test.js +125 -125
  457. package/test/subtitle-track-numbering.test.js +391 -391
  458. package/test/supply-margin.test.js +190 -190
  459. package/test/swarm-follows-readers.test.js +174 -174
  460. package/test/swarm-reach.test.js +130 -130
  461. package/test/swarm-selection.test.js +220 -220
  462. package/test/tail-duplication.test.js +167 -167
  463. package/test/the-channel-has-both-ends.test.js +91 -91
  464. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  465. package/test/thread-pool.test.js +53 -53
  466. package/test/timeline.test.js +84 -84
  467. package/test/torrent-contents.test.js +235 -235
  468. package/test/torrent-cost.test.js +71 -71
  469. package/test/torrent-files.test.js +221 -221
  470. package/test/torrent-naming.test.js +255 -255
  471. package/test/tracks-begin-together.test.js +209 -209
  472. package/test/tunnel-renewal.test.js +167 -167
  473. package/test/two-viewers-one-picture.test.js +508 -508
  474. package/test/upload-hurry.test.js +186 -186
  475. package/test/usrsctp-state.test.js +131 -131
  476. package/test/video-facts.test.js +102 -102
  477. package/test/viewer-assignments.test.js +143 -143
  478. package/test/viewer-outputs.test.js +285 -285
  479. package/test/viewer-presence.test.js +116 -116
  480. package/test/viewer-requests-admission.test.js +92 -92
  481. package/test/viewer-starts-on-map.test.js +27 -27
  482. package/test/viewer-states.test.js +192 -189
  483. package/test/viewer-subtitle-subscription.test.js +96 -96
  484. package/test/viewer.test.js +139 -139
  485. package/test/viewing-generations.test.js +390 -390
  486. package/test/visible-rung.test.js +37 -37
  487. package/test/wait-ledger.test.js +100 -100
  488. package/test/webrtc-shim.test.js +149 -149
  489. package/test/wedge-certainty.test.js +131 -131
  490. package/test/whole-file-held-read.test.js +30 -30
  491. package/test/withdraw-piece-claim.test.js +212 -212
  492. package/test/worker-channel.test.js +158 -158
  493. package/utils/client-log-file.js +151 -151
  494. package/utils/coalesce.js +41 -41
  495. package/utils/logger.js +312 -312
  496. package/utils/parse-range.js +25 -25
  497. package/utils/perf.js +59 -59
  498. package/utils/torrent-source-key.js +38 -38
@@ -1,64 +1,64 @@
1
- /**
2
- * @file GET /transcode/:sessionId/a/:trackIndex/:fileName — one file of an audio
3
- * rendition.
4
- *
5
- * A rendition is one audio track of the file, encoded once and shared by every
6
- * quality rung, published in the master playlist as `#EXT-X-MEDIA`. It is an
7
- * ordinary session underneath, living one directory level below the base
8
- * session so every relative name inside its playlist resolves to it unchanged —
9
- * the same arrangement quality variants use.
10
- *
11
- * Unlike a variant, a rendition is fetched ALONGSIDE the picture rather than
12
- * instead of it: both encoders run, one for the rung and one for the audio.
13
- */
14
-
15
- import { consumerOf, refusedAsStale, serveSessionFile } from "../../../services/server/transcode-session-files.js";
16
-
17
- /**
18
- * @param {import("fastify").FastifyRequest} req
19
- * @param {import("fastify").FastifyReply} reply
20
- * @param {{ renditions: object, serving: object, viewerRequests: object }} deps
21
- * @returns {Promise<void>}
22
- */
23
- export async function handleTranscodeAudioFileGet(req, reply, { renditions, serving, viewerRequests }) {
24
- const baseSessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
25
- const trackIndex = Number(req.params.trackIndex);
26
- const fileName = typeof req.params.fileName === "string" ? req.params.fileName : "";
27
-
28
- // Which viewer is asking. One picture serves everyone watching it, and the
29
- // soundtrack each of them chose is their own: without this, a segment request
30
- // from one viewer would be read as everybody moving to that track, and the
31
- // other viewer's encoder would be stopped once per segment.
32
- const consumerId = consumerOf(req);
33
- // Before resolving, for the same reason as a step's: resolving can make the
34
- // soundtrack and registers this viewer on it. A soundtrack keeps no record of
35
- // what answered a repeat — it is not a height — but a request for a viewing
36
- // that has been left is refused here like any other, and the response that
37
- // does go out holds its output while it is being sent (`serveSessionFile`).
38
- if (refusedAsStale(req, reply, viewerRequests, fileName)) {
39
- return reply;
40
- }
41
- const resolved = await renditions.resolveAudioRenditionFile(
42
- baseSessionId,
43
- trackIndex,
44
- fileName,
45
- consumerId
46
- );
47
- if (resolved.error) {
48
- // Retryable, like every other not-ready answer on this path: a 500 for a
49
- // rendition playlist would end the stream over something the next attempt
50
- // may well get past.
51
- reply.header("Retry-After", "1");
52
- return reply.code(503).send({ error: `Could not prepare the audio rendition: ${resolved.error}` });
53
- }
54
- if (!resolved.sessionId) {
55
- return reply.code(404).send({ error: "No such audio rendition for this transcode session." });
56
- }
57
-
58
- return serveSessionFile(req, reply, {
59
- serving,
60
- viewerRequests,
61
- sessionId: resolved.sessionId,
62
- fileName
63
- });
64
- }
1
+ /**
2
+ * @file GET /transcode/:sessionId/a/:trackIndex/:fileName — one file of an audio
3
+ * rendition.
4
+ *
5
+ * A rendition is one audio track of the file, encoded once and shared by every
6
+ * quality rung, published in the master playlist as `#EXT-X-MEDIA`. It is an
7
+ * ordinary session underneath, living one directory level below the base
8
+ * session so every relative name inside its playlist resolves to it unchanged —
9
+ * the same arrangement quality variants use.
10
+ *
11
+ * Unlike a variant, a rendition is fetched ALONGSIDE the picture rather than
12
+ * instead of it: both encoders run, one for the rung and one for the audio.
13
+ */
14
+
15
+ import { consumerOf, refusedAsStale, serveSessionFile } from "../../../services/server/transcode-session-files.js";
16
+
17
+ /**
18
+ * @param {import("fastify").FastifyRequest} req
19
+ * @param {import("fastify").FastifyReply} reply
20
+ * @param {{ renditions: object, serving: object, viewerRequests: object }} deps
21
+ * @returns {Promise<void>}
22
+ */
23
+ export async function handleTranscodeAudioFileGet(req, reply, { renditions, serving, viewerRequests }) {
24
+ const baseSessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
25
+ const trackIndex = Number(req.params.trackIndex);
26
+ const fileName = typeof req.params.fileName === "string" ? req.params.fileName : "";
27
+
28
+ // Which viewer is asking. One picture serves everyone watching it, and the
29
+ // soundtrack each of them chose is their own: without this, a segment request
30
+ // from one viewer would be read as everybody moving to that track, and the
31
+ // other viewer's encoder would be stopped once per segment.
32
+ const consumerId = consumerOf(req);
33
+ // Before resolving, for the same reason as a step's: resolving can make the
34
+ // soundtrack and registers this viewer on it. A soundtrack keeps no record of
35
+ // what answered a repeat — it is not a height — but a request for a viewing
36
+ // that has been left is refused here like any other, and the response that
37
+ // does go out holds its output while it is being sent (`serveSessionFile`).
38
+ if (refusedAsStale(req, reply, viewerRequests, fileName)) {
39
+ return reply;
40
+ }
41
+ const resolved = await renditions.resolveAudioRenditionFile(
42
+ baseSessionId,
43
+ trackIndex,
44
+ fileName,
45
+ consumerId
46
+ );
47
+ if (resolved.error) {
48
+ // Retryable, like every other not-ready answer on this path: a 500 for a
49
+ // rendition playlist would end the stream over something the next attempt
50
+ // may well get past.
51
+ reply.header("Retry-After", "1");
52
+ return reply.code(503).send({ error: `Could not prepare the audio rendition: ${resolved.error}` });
53
+ }
54
+ if (!resolved.sessionId) {
55
+ return reply.code(404).send({ error: "No such audio rendition for this transcode session." });
56
+ }
57
+
58
+ return serveSessionFile(req, reply, {
59
+ serving,
60
+ viewerRequests,
61
+ sessionId: resolved.sessionId,
62
+ fileName
63
+ });
64
+ }
@@ -1,95 +1,95 @@
1
- /**
2
- * @file GET /transcode/:sessionId/a/:track/warm?position=<seconds>[&transcode=0|1]
3
- * — prepare an audio track before the player is told to change to it.
4
- *
5
- * `transcode` is the page's statement about THIS track: `1` when its browser
6
- * cannot play the track's codec as it stands, `0` when it can. The proxy then
7
- * decides how the track is produced by the same rule as when a file is opened
8
- * — a track nothing states a rate for is re-encoded even for a browser that
9
- * would play it. Left out, the page is taken to need what it needs for the
10
- * track it is on.
11
- *
12
- * The picture keeps playing while a quality rung is warmed (`variant-warm`),
13
- * and a track change deserves the same: the player discards the audio it holds
14
- * the moment it switches and cannot show a frame until the new track covers the
15
- * playhead, so switching first and producing second shows the track's cold
16
- * start as a spinner over a stopped picture (measured 2026-08-15).
17
- *
18
- * Answers 204 when the segment at that position is ready, so the caller can
19
- * switch into bytes that already exist; 503 while it is still being made.
20
- */
21
-
22
- import { waitForSessionFile } from "../../../services/server/transcode-session-files.js";
23
-
24
- /** How long to hold the request before telling the caller to retry. */
25
- const WARM_WAIT_MS = 12_000;
26
-
27
- /**
28
- * @param {import("fastify").FastifyRequest} req
29
- * @param {import("fastify").FastifyReply} reply
30
- * @param {{ renditions: object, serving: object, viewerRequests: object }} deps
31
- */
32
- export async function handleTranscodeAudioWarmGet(req, reply, { renditions, serving, viewerRequests }) {
33
- const baseSessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
34
- const trackIndex = Number(req.params.track);
35
- const positionSeconds = Number(req.query?.position);
36
-
37
- if (
38
- !Number.isInteger(trackIndex) ||
39
- trackIndex < 0 ||
40
- !Number.isFinite(positionSeconds) ||
41
- positionSeconds < 0
42
- ) {
43
- return reply.code(400).send({ error: "A track index and a non-negative position are required." });
44
- }
45
-
46
- // Whose track change this is. Another viewer of the same picture may be
47
- // listening to something else, and preparing a track must not be read as
48
- // everybody moving to it.
49
- const consumerId = typeof req.query?.consumer === "string" ? req.query.consumer : "";
50
- if (!consumerId) {
51
- return reply.code(400).send({ error: "A consumer is required." });
52
- }
53
-
54
- const statedNeed = req.query?.transcode;
55
- const browserPlays = statedNeed === "0" ? true : statedNeed === "1" ? false : null;
56
-
57
- let prepared;
58
- try {
59
- prepared = await renditions.prepareAudioTrack(
60
- baseSessionId,
61
- trackIndex,
62
- positionSeconds,
63
- consumerId,
64
- browserPlays
65
- );
66
- } catch (error) {
67
- const message = error instanceof Error ? error.message : String(error);
68
- reply.header("Retry-After", "1");
69
- return reply.code(503).send({ error: `Could not prepare the audio track: ${message}` });
70
- }
71
- if (!prepared) {
72
- return reply.code(404).send({ error: "No such audio track for this transcode session." });
73
- }
74
-
75
- const result = await waitForSessionFile(
76
- serving,
77
- prepared.sessionId,
78
- prepared.fileName,
79
- { holdMs: WARM_WAIT_MS }
80
- );
81
- if (result.kind === "file") {
82
- // The bytes are the player's to fetch; the handle opened to reach them is
83
- // ours to close, or a long-running proxy walks to EMFILE one track change
84
- // at a time.
85
- result.stream?.destroy?.();
86
- return reply.code(204).send();
87
- }
88
- if (result.kind === "failed") {
89
- return reply.code(500).send({ error: result.message });
90
- }
91
- // Still being produced. The caller may switch anyway — it will wait where it
92
- // would have waited before — or ask again.
93
- reply.header("Retry-After", "1");
94
- return reply.code(503).send({ error: "The audio track is still warming up." });
95
- }
1
+ /**
2
+ * @file GET /transcode/:sessionId/a/:track/warm?position=<seconds>[&transcode=0|1]
3
+ * — prepare an audio track before the player is told to change to it.
4
+ *
5
+ * `transcode` is the page's statement about THIS track: `1` when its browser
6
+ * cannot play the track's codec as it stands, `0` when it can. The proxy then
7
+ * decides how the track is produced by the same rule as when a file is opened
8
+ * — a track nothing states a rate for is re-encoded even for a browser that
9
+ * would play it. Left out, the page is taken to need what it needs for the
10
+ * track it is on.
11
+ *
12
+ * The picture keeps playing while a quality rung is warmed (`variant-warm`),
13
+ * and a track change deserves the same: the player discards the audio it holds
14
+ * the moment it switches and cannot show a frame until the new track covers the
15
+ * playhead, so switching first and producing second shows the track's cold
16
+ * start as a spinner over a stopped picture (measured 2026-08-15).
17
+ *
18
+ * Answers 204 when the segment at that position is ready, so the caller can
19
+ * switch into bytes that already exist; 503 while it is still being made.
20
+ */
21
+
22
+ import { waitForSessionFile } from "../../../services/server/transcode-session-files.js";
23
+
24
+ /** How long to hold the request before telling the caller to retry. */
25
+ const WARM_WAIT_MS = 12_000;
26
+
27
+ /**
28
+ * @param {import("fastify").FastifyRequest} req
29
+ * @param {import("fastify").FastifyReply} reply
30
+ * @param {{ renditions: object, serving: object, viewerRequests: object }} deps
31
+ */
32
+ export async function handleTranscodeAudioWarmGet(req, reply, { renditions, serving, viewerRequests }) {
33
+ const baseSessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
34
+ const trackIndex = Number(req.params.track);
35
+ const positionSeconds = Number(req.query?.position);
36
+
37
+ if (
38
+ !Number.isInteger(trackIndex) ||
39
+ trackIndex < 0 ||
40
+ !Number.isFinite(positionSeconds) ||
41
+ positionSeconds < 0
42
+ ) {
43
+ return reply.code(400).send({ error: "A track index and a non-negative position are required." });
44
+ }
45
+
46
+ // Whose track change this is. Another viewer of the same picture may be
47
+ // listening to something else, and preparing a track must not be read as
48
+ // everybody moving to it.
49
+ const consumerId = typeof req.query?.consumer === "string" ? req.query.consumer : "";
50
+ if (!consumerId) {
51
+ return reply.code(400).send({ error: "A consumer is required." });
52
+ }
53
+
54
+ const statedNeed = req.query?.transcode;
55
+ const browserPlays = statedNeed === "0" ? true : statedNeed === "1" ? false : null;
56
+
57
+ let prepared;
58
+ try {
59
+ prepared = await renditions.prepareAudioTrack(
60
+ baseSessionId,
61
+ trackIndex,
62
+ positionSeconds,
63
+ consumerId,
64
+ browserPlays
65
+ );
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ reply.header("Retry-After", "1");
69
+ return reply.code(503).send({ error: `Could not prepare the audio track: ${message}` });
70
+ }
71
+ if (!prepared) {
72
+ return reply.code(404).send({ error: "No such audio track for this transcode session." });
73
+ }
74
+
75
+ const result = await waitForSessionFile(
76
+ serving,
77
+ prepared.sessionId,
78
+ prepared.fileName,
79
+ { holdMs: WARM_WAIT_MS }
80
+ );
81
+ if (result.kind === "file") {
82
+ // The bytes are the player's to fetch; the handle opened to reach them is
83
+ // ours to close, or a long-running proxy walks to EMFILE one track change
84
+ // at a time.
85
+ result.stream?.destroy?.();
86
+ return reply.code(204).send();
87
+ }
88
+ if (result.kind === "failed") {
89
+ return reply.code(500).send({ error: result.message });
90
+ }
91
+ // Still being produced. The caller may switch anyway — it will wait where it
92
+ // would have waited before — or ask again.
93
+ reply.header("Retry-After", "1");
94
+ return reply.code(503).send({ error: "The audio track is still warming up." });
95
+ }
@@ -1,52 +1,52 @@
1
- import {
2
- consumerOf,
3
- refusedAsStale,
4
- serveSessionFile,
5
- statedGenerationOf
6
- } from "../../../services/server/transcode-session-files.js";
7
-
8
- /**
9
- * How long a request for a file not yet produced is held: AS LONG AS THE
10
- * REQUESTER SAYS IT WILL WAIT, and no longer than it stays.
11
- *
12
- * The patience is the requester's. It used to be a figure of this route's own,
13
- * 60 s, beside the page's own 60 s — two chosen numbers that happened to agree,
14
- * so the "retry" this route sent at its deadline reached a page that had given
15
- * up at the same instant. The page now states how long it will hold the answer
16
- * open (`X-Hold-Ms`, its own deadline less the round trip it has measured), and
17
- * this route answers by then. A requester that states nothing is held until it
18
- * closes the connection, which every HTTP client does when it stops waiting.
19
- */
20
-
21
- /**
22
- * Serve HLS playlist and segment files from an active transcode session.
23
- *
24
- * Briefly waits for the requested file to appear, then answers with a
25
- * retryable 503 rather than holding the connection, so clients — in
26
- * particular iOS's native HLS player — never hit their own response
27
- * deadline while a segment is still being produced.
28
- *
29
- * GET /transcode/:sessionId/:fileName
30
- *
31
- * @param {import("fastify").FastifyRequest} req
32
- * @param {import("fastify").FastifyReply} reply
33
- * @param {{ serving: object, viewerRequests: object }} deps
34
- * @returns {Promise<void>}
35
- */
36
- export async function handleTranscodeSessionFileGet(req, reply, { serving, viewerRequests }) {
37
- const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
38
- const fileName = typeof req.params.fileName === "string" ? req.params.fileName : "";
39
- if (refusedAsStale(req, reply, viewerRequests, fileName)) {
40
- return reply;
41
- }
42
- // This route names its output outright, so the answer is recorded rather
43
- // than chosen — under the height the output is named after, which is where
44
- // the step route answers with the picture itself.
45
- viewerRequests.noteAnsweredDirectly(
46
- sessionId,
47
- consumerOf(req),
48
- statedGenerationOf(req),
49
- fileName
50
- );
51
- return serveSessionFile(req, reply, { serving, viewerRequests, sessionId, fileName });
52
- }
1
+ import {
2
+ consumerOf,
3
+ refusedAsStale,
4
+ serveSessionFile,
5
+ statedGenerationOf
6
+ } from "../../../services/server/transcode-session-files.js";
7
+
8
+ /**
9
+ * How long a request for a file not yet produced is held: AS LONG AS THE
10
+ * REQUESTER SAYS IT WILL WAIT, and no longer than it stays.
11
+ *
12
+ * The patience is the requester's. It used to be a figure of this route's own,
13
+ * 60 s, beside the page's own 60 s — two chosen numbers that happened to agree,
14
+ * so the "retry" this route sent at its deadline reached a page that had given
15
+ * up at the same instant. The page now states how long it will hold the answer
16
+ * open (`X-Hold-Ms`, its own deadline less the round trip it has measured), and
17
+ * this route answers by then. A requester that states nothing is held until it
18
+ * closes the connection, which every HTTP client does when it stops waiting.
19
+ */
20
+
21
+ /**
22
+ * Serve HLS playlist and segment files from an active transcode session.
23
+ *
24
+ * Briefly waits for the requested file to appear, then answers with a
25
+ * retryable 503 rather than holding the connection, so clients — in
26
+ * particular iOS's native HLS player — never hit their own response
27
+ * deadline while a segment is still being produced.
28
+ *
29
+ * GET /transcode/:sessionId/:fileName
30
+ *
31
+ * @param {import("fastify").FastifyRequest} req
32
+ * @param {import("fastify").FastifyReply} reply
33
+ * @param {{ serving: object, viewerRequests: object }} deps
34
+ * @returns {Promise<void>}
35
+ */
36
+ export async function handleTranscodeSessionFileGet(req, reply, { serving, viewerRequests }) {
37
+ const sessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
38
+ const fileName = typeof req.params.fileName === "string" ? req.params.fileName : "";
39
+ if (refusedAsStale(req, reply, viewerRequests, fileName)) {
40
+ return reply;
41
+ }
42
+ // This route names its output outright, so the answer is recorded rather
43
+ // than chosen — under the height the output is named after, which is where
44
+ // the step route answers with the picture itself.
45
+ viewerRequests.noteAnsweredDirectly(
46
+ sessionId,
47
+ consumerOf(req),
48
+ statedGenerationOf(req),
49
+ fileName
50
+ );
51
+ return serveSessionFile(req, reply, { serving, viewerRequests, sessionId, fileName });
52
+ }
@@ -1,95 +1,95 @@
1
- /**
2
- * @file GET /transcode/:sessionId/v/:height/:fileName — one file of a quality
3
- * variant.
4
- *
5
- * A variant is another encode of the same file at another height, and it is an
6
- * ordinary session underneath. It lives under the base session's path, one
7
- * directory level down, so every relative name inside its playlist — its
8
- * segments and its `#EXT-X-MAP` init — resolves to that variant with nothing in
9
- * the playlist itself having to change.
10
- *
11
- * The variant is created on the first request for it, which is what keeps a
12
- * weak host running one encoder: the player's own bitrate adaptation is off, so
13
- * no variant is ever asked for unless the viewer picked it.
14
- */
15
-
16
- import { replyAssignmentLost, replyOutputUnavailable } from "../../../services/server/http-responses.js";
17
- import {
18
- consumerOf,
19
- refusedAsStale,
20
- serveSessionFile,
21
- statedGenerationOf
22
- } from "../../../services/server/transcode-session-files.js";
23
-
24
- /**
25
- * @param {import("fastify").FastifyRequest} req
26
- * @param {import("fastify").FastifyReply} reply
27
- * @param {{ renditions: object, serving: object, viewerRequests: object }} deps
28
- * @returns {Promise<void>}
29
- */
30
- export async function handleTranscodeVariantFileGet(req, reply, { renditions, serving, viewerRequests }) {
31
- const baseSessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
32
- const height = Number(req.params.height);
33
- const fileName = typeof req.params.fileName === "string" ? req.params.fileName : "";
34
-
35
- // Which viewer is asking. Two viewers of one picture can be on two rungs, and
36
- // a segment request is what says which rung a viewer is watching — read as
37
- // the session's own, one of them would take the other off their step.
38
- const consumerId = consumerOf(req);
39
- // Before resolving: resolving can make a step and registers this viewer on
40
- // it, which a request for a viewing they have already left must not do.
41
- if (refusedAsStale(req, reply, viewerRequests, fileName)) {
42
- return reply;
43
- }
44
- const resolved = await renditions.resolveVariantFile(
45
- baseSessionId,
46
- height,
47
- fileName,
48
- consumerId,
49
- // Which viewing this request was made in: a repeat within it is answered
50
- // by whatever answered it the first time.
51
- statedGenerationOf(req)
52
- );
53
- if (resolved.unavailable) {
54
- return replyOutputUnavailable(reply, resolved.unavailable);
55
- }
56
- if (resolved.lost) {
57
- return replyAssignmentLost(reply, resolved.lost);
58
- }
59
- if (resolved.recover) {
60
- // The very piece that was given, from what its gone output left in the
61
- // store — held for as long as it is being sent, like any other answer.
62
- const stored = await serving.storedPieceOf(resolved.recover.key, resolved.recover.likeId, fileName);
63
- if (!stored) {
64
- return replyAssignmentLost(reply, { reason: "the stored piece went while it was being fetched" });
65
- }
66
- const release = viewerRequests.holdResponseForKey(resolved.recover.key, consumerId);
67
- reply.raw?.once?.("finish", release);
68
- reply.raw?.once?.("close", release);
69
- reply.raw?.once?.("error", release);
70
- if (!reply.raw || reply.raw.destroyed || reply.raw.writableEnded) {
71
- release();
72
- }
73
- reply.header("Cache-Control", "public, max-age=60");
74
- reply.header("Content-Type", stored.contentType);
75
- return reply.send(stored.stream);
76
- }
77
- if (resolved.error) {
78
- // Preparing the variant failed — a probe, a keyframe index, an input that
79
- // is not there yet. Retryable, like every other not-ready answer on this
80
- // path: a 500 for a level playlist is fatal to hls.js, which would end the
81
- // stream over something the next attempt may well get past.
82
- reply.header("Retry-After", "1");
83
- return reply.code(503).send({ error: `Could not prepare the quality variant: ${resolved.error}` });
84
- }
85
- if (!resolved.sessionId) {
86
- return reply.code(404).send({ error: "No such quality variant for this transcode session." });
87
- }
88
-
89
- return serveSessionFile(req, reply, {
90
- serving,
91
- viewerRequests,
92
- sessionId: resolved.sessionId,
93
- fileName
94
- });
95
- }
1
+ /**
2
+ * @file GET /transcode/:sessionId/v/:height/:fileName — one file of a quality
3
+ * variant.
4
+ *
5
+ * A variant is another encode of the same file at another height, and it is an
6
+ * ordinary session underneath. It lives under the base session's path, one
7
+ * directory level down, so every relative name inside its playlist — its
8
+ * segments and its `#EXT-X-MAP` init — resolves to that variant with nothing in
9
+ * the playlist itself having to change.
10
+ *
11
+ * The variant is created on the first request for it, which is what keeps a
12
+ * weak host running one encoder: the player's own bitrate adaptation is off, so
13
+ * no variant is ever asked for unless the viewer picked it.
14
+ */
15
+
16
+ import { replyAssignmentLost, replyOutputUnavailable } from "../../../services/server/http-responses.js";
17
+ import {
18
+ consumerOf,
19
+ refusedAsStale,
20
+ serveSessionFile,
21
+ statedGenerationOf
22
+ } from "../../../services/server/transcode-session-files.js";
23
+
24
+ /**
25
+ * @param {import("fastify").FastifyRequest} req
26
+ * @param {import("fastify").FastifyReply} reply
27
+ * @param {{ renditions: object, serving: object, viewerRequests: object }} deps
28
+ * @returns {Promise<void>}
29
+ */
30
+ export async function handleTranscodeVariantFileGet(req, reply, { renditions, serving, viewerRequests }) {
31
+ const baseSessionId = typeof req.params.sessionId === "string" ? req.params.sessionId : "";
32
+ const height = Number(req.params.height);
33
+ const fileName = typeof req.params.fileName === "string" ? req.params.fileName : "";
34
+
35
+ // Which viewer is asking. Two viewers of one picture can be on two rungs, and
36
+ // a segment request is what says which rung a viewer is watching — read as
37
+ // the session's own, one of them would take the other off their step.
38
+ const consumerId = consumerOf(req);
39
+ // Before resolving: resolving can make a step and registers this viewer on
40
+ // it, which a request for a viewing they have already left must not do.
41
+ if (refusedAsStale(req, reply, viewerRequests, fileName)) {
42
+ return reply;
43
+ }
44
+ const resolved = await renditions.resolveVariantFile(
45
+ baseSessionId,
46
+ height,
47
+ fileName,
48
+ consumerId,
49
+ // Which viewing this request was made in: a repeat within it is answered
50
+ // by whatever answered it the first time.
51
+ statedGenerationOf(req)
52
+ );
53
+ if (resolved.unavailable) {
54
+ return replyOutputUnavailable(reply, resolved.unavailable);
55
+ }
56
+ if (resolved.lost) {
57
+ return replyAssignmentLost(reply, resolved.lost);
58
+ }
59
+ if (resolved.recover) {
60
+ // The very piece that was given, from what its gone output left in the
61
+ // store — held for as long as it is being sent, like any other answer.
62
+ const stored = await serving.storedPieceOf(resolved.recover.key, resolved.recover.likeId, fileName);
63
+ if (!stored) {
64
+ return replyAssignmentLost(reply, { reason: "the stored piece went while it was being fetched" });
65
+ }
66
+ const release = viewerRequests.holdResponseForKey(resolved.recover.key, consumerId);
67
+ reply.raw?.once?.("finish", release);
68
+ reply.raw?.once?.("close", release);
69
+ reply.raw?.once?.("error", release);
70
+ if (!reply.raw || reply.raw.destroyed || reply.raw.writableEnded) {
71
+ release();
72
+ }
73
+ reply.header("Cache-Control", "public, max-age=60");
74
+ reply.header("Content-Type", stored.contentType);
75
+ return reply.send(stored.stream);
76
+ }
77
+ if (resolved.error) {
78
+ // Preparing the variant failed — a probe, a keyframe index, an input that
79
+ // is not there yet. Retryable, like every other not-ready answer on this
80
+ // path: a 500 for a level playlist is fatal to hls.js, which would end the
81
+ // stream over something the next attempt may well get past.
82
+ reply.header("Retry-After", "1");
83
+ return reply.code(503).send({ error: `Could not prepare the quality variant: ${resolved.error}` });
84
+ }
85
+ if (!resolved.sessionId) {
86
+ return reply.code(404).send({ error: "No such quality variant for this transcode session." });
87
+ }
88
+
89
+ return serveSessionFile(req, reply, {
90
+ serving,
91
+ viewerRequests,
92
+ sessionId: resolved.sessionId,
93
+ fileName
94
+ });
95
+ }