@torrent-tv/proxy 2.88.4 → 2.88.5
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 +2266 -2259
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/calibration/audio/NOTICE.md +19 -0
- package/assets/calibration/audio/aac.m4a +0 -0
- package/assets/calibration/audio/ac3.ac3 +0 -0
- package/assets/calibration/audio/alac.m4a +0 -0
- package/assets/calibration/audio/dts.dts +0 -0
- package/assets/calibration/audio/eac3.eac3 +0 -0
- package/assets/calibration/audio/flac.flac +0 -0
- package/assets/calibration/audio/manifest.json +98 -0
- package/assets/calibration/audio/mp2.mp2 +0 -0
- package/assets/calibration/audio/mp3.mp3 +0 -0
- package/assets/calibration/audio/opus.opus +0 -0
- package/assets/calibration/audio/pcm.wav +0 -0
- package/assets/calibration/audio/truehd.thd +0 -0
- package/assets/calibration/audio/vorbis.ogg +0 -0
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -411
- package/docs/browser-proxy-contract.md +155 -152
- 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/playback-start-forecast.md +56 -0
- 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 +1 -1
- 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/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- 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 +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- 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 +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- 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/build-audio-samples.mjs +31 -0
- package/scripts/render-run-graph.js +173 -173
- package/server.js +730 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1237 -1235
- 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 +783 -783
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2282
- 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 +418 -418
- package/services/encode/audio-calibration.js +67 -0
- package/services/encode/audio-work-rate.js +15 -0
- 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 +1167 -1150
- 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 +349 -349
- package/services/encode/quality/output-format.js +319 -319
- 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 +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +458 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +12 -1
- 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 +299 -298
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -609
- 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 +1344 -1344
- 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 +143 -143
- 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/audio-bound.js +233 -233
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +785 -760
- 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 +516 -708
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1020 -995
- 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 +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1937 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1255 -1240
- package/services/storage/wire.js +175 -175
- 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 +2688 -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 +371 -371
- 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 +691 -691
- 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-calibration.test.js +23 -0
- 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 +162 -162
- 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-presentation-clock.test.js +162 -147
- 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 +112 -112
- 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 +273 -270
- 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 +401 -401
- 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 +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- 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 +483 -453
- package/test/playback-start-regressions.test.js +99 -99
- 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 +1257 -1257
- 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 +338 -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 +105 -105
- 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/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- 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 +209 -209
- 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 +504 -504
- 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,49 +1,49 @@
|
|
|
1
|
-
# Proposal: Proxy observability (queued for the next proxy release, 2.9.25)
|
|
2
|
-
|
|
3
|
-
## Why
|
|
4
|
-
|
|
5
|
-
Three diagnosis gaps surfaced while debugging the 2026-07-06 mobile playback
|
|
6
|
-
failures:
|
|
7
|
-
|
|
8
|
-
1. `/healthz` and `/health` return only `{"ok":true}` — there is no way to
|
|
9
|
-
see the running proxy version remotely (the addon shipped a stale proxy
|
|
10
|
-
for a whole release and nothing detected it). The server already returns
|
|
11
|
-
`version` in its healthz; the proxy should match.
|
|
12
|
-
2. Tracker announces are silent: a torrent with zero peers gives no clue
|
|
13
|
-
whether the tracker rejected the announce (e.g. a private tracker's
|
|
14
|
-
client whitelist), returned an empty peer list, or was unreachable.
|
|
15
|
-
WebTorrent surfaces these as `warning` events that nothing logs.
|
|
16
|
-
3. `MaxListenersExceededWarning` for `[Ssdp]` floods the log — the two
|
|
17
|
-
port-mapper instances (TCP + UDP) plus WebTorrent's own nat-api attach
|
|
18
|
-
listeners to a shared SSDP emitter.
|
|
19
|
-
|
|
20
|
-
## What Changes
|
|
21
|
-
|
|
22
|
-
- `/healthz` and `/health` include the proxy `version` (from package.json),
|
|
23
|
-
keeping the current `ok` semantics.
|
|
24
|
-
- Torrent-level `warning` events (tracker errors and rejections) are logged;
|
|
25
|
-
each tracker announce result is logged with the peer count returned.
|
|
26
|
-
Where cheaply available, peer origin (tracker / DHT / PEX) is tagged in
|
|
27
|
-
the existing `[stats]` line.
|
|
28
|
-
- SSDP listener leak fixed (raise the max-listeners on the shared emitter or
|
|
29
|
-
share one discovery instance between the two port mappers).
|
|
30
|
-
|
|
31
|
-
## Capabilities
|
|
32
|
-
|
|
33
|
-
### New Capabilities
|
|
34
|
-
|
|
35
|
-
- `observability`: health/version reporting and peer-discovery diagnostics.
|
|
36
|
-
|
|
37
|
-
### Modified Capabilities
|
|
38
|
-
|
|
39
|
-
<!-- none yet — openspec/specs is empty in this repo -->
|
|
40
|
-
|
|
41
|
-
## Impact
|
|
42
|
-
|
|
43
|
-
- `routes/healthz/get.js`, `routes/health/get.js` (or their handlers) —
|
|
44
|
-
version field.
|
|
45
|
-
- `services/torrent/torrent-pool.js` — warning/announce logging, stats origin tags.
|
|
46
|
-
- `services/transport/port-mapper.js` — SSDP listener fix.
|
|
47
|
-
- CHANGELOG entry at current package.json version + 1 patch (2.9.25);
|
|
48
|
-
requires the usual ha-addon version bump (0.2.47) after `npm run patch`
|
|
49
|
-
(npm publish needs the user's npm key).
|
|
1
|
+
# Proposal: Proxy observability (queued for the next proxy release, 2.9.25)
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Three diagnosis gaps surfaced while debugging the 2026-07-06 mobile playback
|
|
6
|
+
failures:
|
|
7
|
+
|
|
8
|
+
1. `/healthz` and `/health` return only `{"ok":true}` — there is no way to
|
|
9
|
+
see the running proxy version remotely (the addon shipped a stale proxy
|
|
10
|
+
for a whole release and nothing detected it). The server already returns
|
|
11
|
+
`version` in its healthz; the proxy should match.
|
|
12
|
+
2. Tracker announces are silent: a torrent with zero peers gives no clue
|
|
13
|
+
whether the tracker rejected the announce (e.g. a private tracker's
|
|
14
|
+
client whitelist), returned an empty peer list, or was unreachable.
|
|
15
|
+
WebTorrent surfaces these as `warning` events that nothing logs.
|
|
16
|
+
3. `MaxListenersExceededWarning` for `[Ssdp]` floods the log — the two
|
|
17
|
+
port-mapper instances (TCP + UDP) plus WebTorrent's own nat-api attach
|
|
18
|
+
listeners to a shared SSDP emitter.
|
|
19
|
+
|
|
20
|
+
## What Changes
|
|
21
|
+
|
|
22
|
+
- `/healthz` and `/health` include the proxy `version` (from package.json),
|
|
23
|
+
keeping the current `ok` semantics.
|
|
24
|
+
- Torrent-level `warning` events (tracker errors and rejections) are logged;
|
|
25
|
+
each tracker announce result is logged with the peer count returned.
|
|
26
|
+
Where cheaply available, peer origin (tracker / DHT / PEX) is tagged in
|
|
27
|
+
the existing `[stats]` line.
|
|
28
|
+
- SSDP listener leak fixed (raise the max-listeners on the shared emitter or
|
|
29
|
+
share one discovery instance between the two port mappers).
|
|
30
|
+
|
|
31
|
+
## Capabilities
|
|
32
|
+
|
|
33
|
+
### New Capabilities
|
|
34
|
+
|
|
35
|
+
- `observability`: health/version reporting and peer-discovery diagnostics.
|
|
36
|
+
|
|
37
|
+
### Modified Capabilities
|
|
38
|
+
|
|
39
|
+
<!-- none yet — openspec/specs is empty in this repo -->
|
|
40
|
+
|
|
41
|
+
## Impact
|
|
42
|
+
|
|
43
|
+
- `routes/healthz/get.js`, `routes/health/get.js` (or their handlers) —
|
|
44
|
+
version field.
|
|
45
|
+
- `services/torrent/torrent-pool.js` — warning/announce logging, stats origin tags.
|
|
46
|
+
- `services/transport/port-mapper.js` — SSDP listener fix.
|
|
47
|
+
- CHANGELOG entry at current package.json version + 1 patch (2.9.25);
|
|
48
|
+
requires the usual ha-addon version bump (0.2.47) after `npm run patch`
|
|
49
|
+
(npm publish needs the user's npm key).
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# observability — delta spec
|
|
2
|
-
|
|
3
|
-
## ADDED Requirements
|
|
4
|
-
|
|
5
|
-
### Requirement: Health endpoints report the version
|
|
6
|
-
`GET /healthz` and `GET /health` SHALL include the running proxy version
|
|
7
|
-
(from package.json) alongside the existing `ok` field.
|
|
8
|
-
|
|
9
|
-
#### Scenario: Version visible remotely
|
|
10
|
-
- **WHEN** a client requests `/healthz`
|
|
11
|
-
- **THEN** the response contains `ok: true` and the exact npm package version
|
|
12
|
-
of the running proxy
|
|
13
|
-
|
|
14
|
-
### Requirement: Peer-discovery diagnostics
|
|
15
|
-
The proxy SHALL log, per torrent: an added line with file count, `private`
|
|
16
|
-
flag and tracker count; every torrent-level warning (tracker rejections and
|
|
17
|
-
errors surface as warnings); and each tracker announce response with the
|
|
18
|
-
seeder/leecher counts returned. Logging failures MUST NOT affect playback
|
|
19
|
-
(best-effort, defensive against WebTorrent internals).
|
|
20
|
-
|
|
21
|
-
#### Scenario: Zero-peer torrent is explainable
|
|
22
|
-
- **WHEN** a torrent sits at zero peers
|
|
23
|
-
- **THEN** the log shows either the tracker's rejection/warning text or an
|
|
24
|
-
announce response with zero seeders — distinguishing "tracker refused us"
|
|
25
|
-
from "the swarm is empty"
|
|
26
|
-
|
|
27
|
-
### Requirement: No SSDP listener warnings
|
|
28
|
-
Port mapping SHALL NOT flood the log with `MaxListenersExceededWarning`
|
|
29
|
-
regardless of how many ports one mapper maps or renews.
|
|
30
|
-
|
|
31
|
-
#### Scenario: WebRTC UDP range mapping
|
|
32
|
-
- **WHEN** the UDP mapper maps its 10-port range and later auto-renews it
|
|
33
|
-
- **THEN** no MaxListenersExceededWarning lines appear in the log
|
|
1
|
+
# observability — delta spec
|
|
2
|
+
|
|
3
|
+
## ADDED Requirements
|
|
4
|
+
|
|
5
|
+
### Requirement: Health endpoints report the version
|
|
6
|
+
`GET /healthz` and `GET /health` SHALL include the running proxy version
|
|
7
|
+
(from package.json) alongside the existing `ok` field.
|
|
8
|
+
|
|
9
|
+
#### Scenario: Version visible remotely
|
|
10
|
+
- **WHEN** a client requests `/healthz`
|
|
11
|
+
- **THEN** the response contains `ok: true` and the exact npm package version
|
|
12
|
+
of the running proxy
|
|
13
|
+
|
|
14
|
+
### Requirement: Peer-discovery diagnostics
|
|
15
|
+
The proxy SHALL log, per torrent: an added line with file count, `private`
|
|
16
|
+
flag and tracker count; every torrent-level warning (tracker rejections and
|
|
17
|
+
errors surface as warnings); and each tracker announce response with the
|
|
18
|
+
seeder/leecher counts returned. Logging failures MUST NOT affect playback
|
|
19
|
+
(best-effort, defensive against WebTorrent internals).
|
|
20
|
+
|
|
21
|
+
#### Scenario: Zero-peer torrent is explainable
|
|
22
|
+
- **WHEN** a torrent sits at zero peers
|
|
23
|
+
- **THEN** the log shows either the tracker's rejection/warning text or an
|
|
24
|
+
announce response with zero seeders — distinguishing "tracker refused us"
|
|
25
|
+
from "the swarm is empty"
|
|
26
|
+
|
|
27
|
+
### Requirement: No SSDP listener warnings
|
|
28
|
+
Port mapping SHALL NOT flood the log with `MaxListenersExceededWarning`
|
|
29
|
+
regardless of how many ports one mapper maps or renews.
|
|
30
|
+
|
|
31
|
+
#### Scenario: WebRTC UDP range mapping
|
|
32
|
+
- **WHEN** the UDP mapper maps its 10-port range and later auto-renews it
|
|
33
|
+
- **THEN** no MaxListenersExceededWarning lines appear in the log
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
# Tasks: Proxy observability
|
|
2
|
-
|
|
3
|
-
## 1. Implementation
|
|
4
|
-
|
|
5
|
-
- [x] 1.1 Version in `/healthz` and `/health` (createRequire package.json in
|
|
6
|
-
server.js, passed as route dep)
|
|
7
|
-
- [x] 1.2 torrent-pool.js: added-torrent line (files/private/trackers),
|
|
8
|
-
torrent `warning` logging, tracker `update` (announce seeders/leechers)
|
|
9
|
-
logging with defensive access, client-level warning logging
|
|
10
|
-
- [x] 1.3 port-mapper.js: setMaxListeners(0) on the UPnP SSDP emitter after
|
|
11
|
-
the first successful map()
|
|
12
|
-
- [x] 1.4 Syntax checks + healthz handler smoke test (version present)
|
|
13
|
-
|
|
14
|
-
## 2. Release
|
|
15
|
-
|
|
16
|
-
- [x] 2.1 CHANGELOG.md entry at 2.9.25
|
|
17
|
-
- [x] 2.2 `npm run patch` (needs npm auth), then ha-addon bump 0.2.47 + push
|
|
18
|
-
(2.9.25 published by the owner — npm 2FA; addon 0.2.47 pushed after)
|
|
19
|
-
- [ ] 2.3 After the addon updates: verify version via `/healthz`, watch the
|
|
20
|
-
addon log for announce lines on a real torrent, confirm no SSDP
|
|
21
|
-
warnings
|
|
1
|
+
# Tasks: Proxy observability
|
|
2
|
+
|
|
3
|
+
## 1. Implementation
|
|
4
|
+
|
|
5
|
+
- [x] 1.1 Version in `/healthz` and `/health` (createRequire package.json in
|
|
6
|
+
server.js, passed as route dep)
|
|
7
|
+
- [x] 1.2 torrent-pool.js: added-torrent line (files/private/trackers),
|
|
8
|
+
torrent `warning` logging, tracker `update` (announce seeders/leechers)
|
|
9
|
+
logging with defensive access, client-level warning logging
|
|
10
|
+
- [x] 1.3 port-mapper.js: setMaxListeners(0) on the UPnP SSDP emitter after
|
|
11
|
+
the first successful map()
|
|
12
|
+
- [x] 1.4 Syntax checks + healthz handler smoke test (version present)
|
|
13
|
+
|
|
14
|
+
## 2. Release
|
|
15
|
+
|
|
16
|
+
- [x] 2.1 CHANGELOG.md entry at 2.9.25
|
|
17
|
+
- [x] 2.2 `npm run patch` (needs npm auth), then ha-addon bump 0.2.47 + push
|
|
18
|
+
(2.9.25 published by the owner — npm 2FA; addon 0.2.47 pushed after)
|
|
19
|
+
- [ ] 2.3 After the addon updates: verify version via `/healthz`, watch the
|
|
20
|
+
addon log for announce lines on a real torrent, confirm no SSDP
|
|
21
|
+
warnings
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
# Design: Debounce server-side seek restarts
|
|
2
|
-
|
|
3
|
-
Normative — member names, constants, and edge cases as written. Read before
|
|
4
|
-
coding:
|
|
5
|
-
|
|
6
|
-
- `services/hls-session-manager.js`: `#ensureEncodingFor(session, index)`
|
|
7
|
-
(line ~1478 — the restart decision), the session object shape where
|
|
8
|
-
`lastRestartAt` / `pendingRestartIndex` / `encodeStartIndex` live (~883),
|
|
9
|
-
`#startEncodeRun`, `disposeSession` (must clear timers), and the constants
|
|
10
|
-
block (~36–58).
|
|
11
|
-
|
|
12
|
-
## Current behaviour (what we are replacing)
|
|
13
|
-
|
|
14
|
-
`#ensureEncodingFor` runs per segment request:
|
|
15
|
-
|
|
16
|
-
1. Compute the look-ahead window `[head, currentSeg + MAX_LOOKAHEAD_SEGMENTS]`.
|
|
17
|
-
2. In window → return (the running encode will reach it).
|
|
18
|
-
3. Out of window (a seek) → if `now - lastRestartAt < RESTART_COOLDOWN_MS`,
|
|
19
|
-
skip; else `#startEncodeRun(session, index)` immediately.
|
|
20
|
-
|
|
21
|
-
The cooldown is a fixed 4 s gap. Requests spaced wider than 4 s each restart
|
|
22
|
-
→ ping-pong. That is the bug.
|
|
23
|
-
|
|
24
|
-
## New behaviour: settle window (debounce)
|
|
25
|
-
|
|
26
|
-
Keep steps 1–2. Replace step 3 with a debounce:
|
|
27
|
-
|
|
28
|
-
far request (index outside window):
|
|
29
|
-
session.seekTarget = index // last far index wins
|
|
30
|
-
if (!session.seekSettleTimer):
|
|
31
|
-
session.seekFirstFarAt = now()
|
|
32
|
-
else:
|
|
33
|
-
clearTimeout(session.seekSettleTimer)
|
|
34
|
-
const waited = now() - session.seekFirstFarAt
|
|
35
|
-
const delay = waited >= SEEK_SETTLE_MAX_MS ? 0
|
|
36
|
-
: Math.min(SEEK_SETTLE_MS, SEEK_SETTLE_MAX_MS - waited)
|
|
37
|
-
session.seekSettleTimer = setTimeout(() => fireSettledSeek(session), delay)
|
|
38
|
-
return // do NOT restart now
|
|
39
|
-
|
|
40
|
-
fireSettledSeek(session):
|
|
41
|
-
const target = session.seekTarget
|
|
42
|
-
session.seekSettleTimer = null
|
|
43
|
-
session.seekTarget = null
|
|
44
|
-
session.seekFirstFarAt = 0
|
|
45
|
-
if (session disposed or target == null) return
|
|
46
|
-
// Minimum gap between actual restarts (defensive; the settle already
|
|
47
|
-
// collapses bursts). If still cooling down, re-arm once for the
|
|
48
|
-
// remaining cooldown instead of restarting.
|
|
49
|
-
const sinceRestart = now() - (session.lastRestartAt ?? 0)
|
|
50
|
-
if (sinceRestart < RESTART_COOLDOWN_MS):
|
|
51
|
-
session.seekFirstFarAt = now()
|
|
52
|
-
session.seekTarget = target
|
|
53
|
-
session.seekSettleTimer = setTimeout(() => fireSettledSeek(session),
|
|
54
|
-
RESTART_COOLDOWN_MS - sinceRestart)
|
|
55
|
-
return
|
|
56
|
-
log(`transcode ${id} seek settle → restart at #${target}`)
|
|
57
|
-
this.#startEncodeRun(session, target) // sets lastRestartAt
|
|
58
|
-
|
|
59
|
-
Notes:
|
|
60
|
-
- `timer.unref?.()` on each `setTimeout` so a pending settle never keeps the
|
|
61
|
-
process alive (mirror the codebase's other timers).
|
|
62
|
-
- A far request whose `index` equals the current `seekTarget` still re-arms
|
|
63
|
-
the timer (the player is still asking for the same place — that is fine, it
|
|
64
|
-
just extends the quiet period up to the cap).
|
|
65
|
-
- If, while a settle is pending, the running encode advances so a later
|
|
66
|
-
request falls back INSIDE the window, that request returns at step 2 and
|
|
67
|
-
does not touch the settle. The pending settle still fires for the recorded
|
|
68
|
-
target; that is acceptable (it restarts at a position the player recently
|
|
69
|
-
wanted). Simplicity over cleverness.
|
|
70
|
-
|
|
71
|
-
## Constants (add to the constants block)
|
|
72
|
-
|
|
73
|
-
// Quiet period after a far (seek) segment request before ffmpeg is
|
|
74
|
-
// restarted at it. Further far requests within the period re-arm it, so a
|
|
75
|
-
// scrub that emits a burst of scattered requests collapses to ONE restart
|
|
76
|
-
// at the position the player ended on.
|
|
77
|
-
SEEK_SETTLE_MS = 1200
|
|
78
|
-
// Hard cap on the total settle wait measured from the first far request of
|
|
79
|
-
// a burst, so a still-moving scrubber cannot delay a real seek forever.
|
|
80
|
-
SEEK_SETTLE_MAX_MS = 2500
|
|
81
|
-
|
|
82
|
-
`MAX_LOOKAHEAD_SEGMENTS` and `RESTART_COOLDOWN_MS` keep their current values;
|
|
83
|
-
`RESTART_COOLDOWN_MS` is now only the floor between actual restarts.
|
|
84
|
-
|
|
85
|
-
## Session state (add where lastRestartAt is initialised, ~883)
|
|
86
|
-
|
|
87
|
-
seekSettleTimer: null, // pending settle timer handle or null
|
|
88
|
-
seekTarget: null, // pending far segment index to restart at
|
|
89
|
-
seekFirstFarAt: 0, // timestamp of the first far request in the burst
|
|
90
|
-
|
|
91
|
-
## Disposal
|
|
92
|
-
|
|
93
|
-
`disposeSession` (and any teardown that abandons a session) MUST
|
|
94
|
-
`clearTimeout(session.seekSettleTimer)` and null it, so a settle cannot fire
|
|
95
|
-
after disposal and restart a dead session.
|
|
96
|
-
|
|
97
|
-
## Why proxy-side, not client-side (the user's original framing)
|
|
98
|
-
|
|
99
|
-
The user asked for a scrubber-release debounce. On the in-page media-chrome
|
|
100
|
-
control that is possible but fragile, and — decisively — it cannot cover the
|
|
101
|
-
iOS **native fullscreen** player, whose scrubber the web app does not control;
|
|
102
|
-
the failing session was exactly iOS. The proxy sees the segment requests from
|
|
103
|
-
EVERY client (native iOS, hls.js, direct `<video>`), so debouncing the
|
|
104
|
-
restart here is the one place that fixes all of them. An in-page scrubber
|
|
105
|
-
debounce may still be added later as a responsiveness nicety; it is not a
|
|
106
|
-
substitute and is out of scope here.
|
|
107
|
-
|
|
108
|
-
## Verification
|
|
109
|
-
|
|
110
|
-
- Unit-test the timing/target logic in isolation (a fake session + a fake
|
|
111
|
-
clock/`setTimeout`): a burst of far requests `367,732,369,368,370` within
|
|
112
|
-
the window collapses to a single `#startEncodeRun` at the LAST index (370),
|
|
113
|
-
and a lone later far request triggers exactly one more restart.
|
|
114
|
-
- `node --check`.
|
|
115
|
-
- Field: the earlier hang scenario (scrub Poirot on iOS) should now produce a
|
|
116
|
-
single `seek settle → restart` log line per scrub instead of a train of
|
|
117
|
-
`seek → restart` lines, and playback should resume after one settle + the
|
|
118
|
-
(separate, unavoidable) cold-segment wait.
|
|
1
|
+
# Design: Debounce server-side seek restarts
|
|
2
|
+
|
|
3
|
+
Normative — member names, constants, and edge cases as written. Read before
|
|
4
|
+
coding:
|
|
5
|
+
|
|
6
|
+
- `services/hls-session-manager.js`: `#ensureEncodingFor(session, index)`
|
|
7
|
+
(line ~1478 — the restart decision), the session object shape where
|
|
8
|
+
`lastRestartAt` / `pendingRestartIndex` / `encodeStartIndex` live (~883),
|
|
9
|
+
`#startEncodeRun`, `disposeSession` (must clear timers), and the constants
|
|
10
|
+
block (~36–58).
|
|
11
|
+
|
|
12
|
+
## Current behaviour (what we are replacing)
|
|
13
|
+
|
|
14
|
+
`#ensureEncodingFor` runs per segment request:
|
|
15
|
+
|
|
16
|
+
1. Compute the look-ahead window `[head, currentSeg + MAX_LOOKAHEAD_SEGMENTS]`.
|
|
17
|
+
2. In window → return (the running encode will reach it).
|
|
18
|
+
3. Out of window (a seek) → if `now - lastRestartAt < RESTART_COOLDOWN_MS`,
|
|
19
|
+
skip; else `#startEncodeRun(session, index)` immediately.
|
|
20
|
+
|
|
21
|
+
The cooldown is a fixed 4 s gap. Requests spaced wider than 4 s each restart
|
|
22
|
+
→ ping-pong. That is the bug.
|
|
23
|
+
|
|
24
|
+
## New behaviour: settle window (debounce)
|
|
25
|
+
|
|
26
|
+
Keep steps 1–2. Replace step 3 with a debounce:
|
|
27
|
+
|
|
28
|
+
far request (index outside window):
|
|
29
|
+
session.seekTarget = index // last far index wins
|
|
30
|
+
if (!session.seekSettleTimer):
|
|
31
|
+
session.seekFirstFarAt = now()
|
|
32
|
+
else:
|
|
33
|
+
clearTimeout(session.seekSettleTimer)
|
|
34
|
+
const waited = now() - session.seekFirstFarAt
|
|
35
|
+
const delay = waited >= SEEK_SETTLE_MAX_MS ? 0
|
|
36
|
+
: Math.min(SEEK_SETTLE_MS, SEEK_SETTLE_MAX_MS - waited)
|
|
37
|
+
session.seekSettleTimer = setTimeout(() => fireSettledSeek(session), delay)
|
|
38
|
+
return // do NOT restart now
|
|
39
|
+
|
|
40
|
+
fireSettledSeek(session):
|
|
41
|
+
const target = session.seekTarget
|
|
42
|
+
session.seekSettleTimer = null
|
|
43
|
+
session.seekTarget = null
|
|
44
|
+
session.seekFirstFarAt = 0
|
|
45
|
+
if (session disposed or target == null) return
|
|
46
|
+
// Minimum gap between actual restarts (defensive; the settle already
|
|
47
|
+
// collapses bursts). If still cooling down, re-arm once for the
|
|
48
|
+
// remaining cooldown instead of restarting.
|
|
49
|
+
const sinceRestart = now() - (session.lastRestartAt ?? 0)
|
|
50
|
+
if (sinceRestart < RESTART_COOLDOWN_MS):
|
|
51
|
+
session.seekFirstFarAt = now()
|
|
52
|
+
session.seekTarget = target
|
|
53
|
+
session.seekSettleTimer = setTimeout(() => fireSettledSeek(session),
|
|
54
|
+
RESTART_COOLDOWN_MS - sinceRestart)
|
|
55
|
+
return
|
|
56
|
+
log(`transcode ${id} seek settle → restart at #${target}`)
|
|
57
|
+
this.#startEncodeRun(session, target) // sets lastRestartAt
|
|
58
|
+
|
|
59
|
+
Notes:
|
|
60
|
+
- `timer.unref?.()` on each `setTimeout` so a pending settle never keeps the
|
|
61
|
+
process alive (mirror the codebase's other timers).
|
|
62
|
+
- A far request whose `index` equals the current `seekTarget` still re-arms
|
|
63
|
+
the timer (the player is still asking for the same place — that is fine, it
|
|
64
|
+
just extends the quiet period up to the cap).
|
|
65
|
+
- If, while a settle is pending, the running encode advances so a later
|
|
66
|
+
request falls back INSIDE the window, that request returns at step 2 and
|
|
67
|
+
does not touch the settle. The pending settle still fires for the recorded
|
|
68
|
+
target; that is acceptable (it restarts at a position the player recently
|
|
69
|
+
wanted). Simplicity over cleverness.
|
|
70
|
+
|
|
71
|
+
## Constants (add to the constants block)
|
|
72
|
+
|
|
73
|
+
// Quiet period after a far (seek) segment request before ffmpeg is
|
|
74
|
+
// restarted at it. Further far requests within the period re-arm it, so a
|
|
75
|
+
// scrub that emits a burst of scattered requests collapses to ONE restart
|
|
76
|
+
// at the position the player ended on.
|
|
77
|
+
SEEK_SETTLE_MS = 1200
|
|
78
|
+
// Hard cap on the total settle wait measured from the first far request of
|
|
79
|
+
// a burst, so a still-moving scrubber cannot delay a real seek forever.
|
|
80
|
+
SEEK_SETTLE_MAX_MS = 2500
|
|
81
|
+
|
|
82
|
+
`MAX_LOOKAHEAD_SEGMENTS` and `RESTART_COOLDOWN_MS` keep their current values;
|
|
83
|
+
`RESTART_COOLDOWN_MS` is now only the floor between actual restarts.
|
|
84
|
+
|
|
85
|
+
## Session state (add where lastRestartAt is initialised, ~883)
|
|
86
|
+
|
|
87
|
+
seekSettleTimer: null, // pending settle timer handle or null
|
|
88
|
+
seekTarget: null, // pending far segment index to restart at
|
|
89
|
+
seekFirstFarAt: 0, // timestamp of the first far request in the burst
|
|
90
|
+
|
|
91
|
+
## Disposal
|
|
92
|
+
|
|
93
|
+
`disposeSession` (and any teardown that abandons a session) MUST
|
|
94
|
+
`clearTimeout(session.seekSettleTimer)` and null it, so a settle cannot fire
|
|
95
|
+
after disposal and restart a dead session.
|
|
96
|
+
|
|
97
|
+
## Why proxy-side, not client-side (the user's original framing)
|
|
98
|
+
|
|
99
|
+
The user asked for a scrubber-release debounce. On the in-page media-chrome
|
|
100
|
+
control that is possible but fragile, and — decisively — it cannot cover the
|
|
101
|
+
iOS **native fullscreen** player, whose scrubber the web app does not control;
|
|
102
|
+
the failing session was exactly iOS. The proxy sees the segment requests from
|
|
103
|
+
EVERY client (native iOS, hls.js, direct `<video>`), so debouncing the
|
|
104
|
+
restart here is the one place that fixes all of them. An in-page scrubber
|
|
105
|
+
debounce may still be added later as a responsiveness nicety; it is not a
|
|
106
|
+
substitute and is out of scope here.
|
|
107
|
+
|
|
108
|
+
## Verification
|
|
109
|
+
|
|
110
|
+
- Unit-test the timing/target logic in isolation (a fake session + a fake
|
|
111
|
+
clock/`setTimeout`): a burst of far requests `367,732,369,368,370` within
|
|
112
|
+
the window collapses to a single `#startEncodeRun` at the LAST index (370),
|
|
113
|
+
and a lone later far request triggers exactly one more restart.
|
|
114
|
+
- `node --check`.
|
|
115
|
+
- Field: the earlier hang scenario (scrub Poirot on iOS) should now produce a
|
|
116
|
+
single `seek settle → restart` log line per scrub instead of a train of
|
|
117
|
+
`seek → restart` lines, and playback should resume after one settle + the
|
|
118
|
+
(separate, unavoidable) cold-segment wait.
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
# Proposal: Debounce server-side seek restarts
|
|
2
|
-
|
|
3
|
-
## Why
|
|
4
|
-
|
|
5
|
-
Field evidence (iPhone/Safari, Poirot, 2026-07-09): after a scrub the player
|
|
6
|
-
hung indefinitely. The proxy restarts ffmpeg at a requested segment when the
|
|
7
|
-
request lands outside the look-ahead window (server-side seek). The native
|
|
8
|
-
iOS player issued scattered segment requests after the seek —
|
|
9
|
-
`367 → 732 → 369 → 368 → 370` — each 25–35 s apart. The existing guard
|
|
10
|
-
(`RESTART_COOLDOWN_MS = 4000`) only suppresses restarts within 4 s of the
|
|
11
|
-
last one, so with requests spaced far wider than that, EVERY scattered
|
|
12
|
-
request restarted ffmpeg at a new position. ffmpeg ping-ponged between
|
|
13
|
-
positions and never finished a single segment, so playback stalled at
|
|
14
|
-
`currentTime` with `bufferedAhead=0` — "заглохло наглухо".
|
|
15
|
-
|
|
16
|
-
The fix is the user's idea placed where every client is covered (including
|
|
17
|
-
the iOS native fullscreen player, whose scrubber the web app cannot control):
|
|
18
|
-
don't act on the first far request — wait for the burst to settle, then
|
|
19
|
-
restart ONCE at the position the player ended on.
|
|
20
|
-
|
|
21
|
-
## What Changes
|
|
22
|
-
|
|
23
|
-
- Replace the fixed post-restart cooldown as the anti-thrash mechanism with a
|
|
24
|
-
**settle window**. When a segment request lands outside the look-ahead
|
|
25
|
-
window (a seek), the proxy does NOT restart immediately: it records the
|
|
26
|
-
requested index as the pending seek target and arms a short quiet-period
|
|
27
|
-
timer. Each further far request updates the target to the latest index and
|
|
28
|
-
re-arms the timer (debounce). When the quiet period elapses with no new far
|
|
29
|
-
request, ffmpeg restarts ONCE at the pending target. A hard cap bounds the
|
|
30
|
-
total wait so a genuine seek is never delayed more than a fixed budget even
|
|
31
|
-
while the scrubber is still moving.
|
|
32
|
-
- Meanwhile the segment route behaves exactly as today — it long-polls and
|
|
33
|
-
the client retries — so the player simply waits out the (short) settle
|
|
34
|
-
instead of driving restarts.
|
|
35
|
-
- "Last far index wins" self-corrects: if the settle resolves on the wrong
|
|
36
|
-
position (e.g. a lone probe request), the player's next request re-arms one
|
|
37
|
-
more settle — at most one extra cycle, never the old infinite ping-pong.
|
|
38
|
-
|
|
39
|
-
Out of scope (documented in design.md): an optional client-side scrubber
|
|
40
|
-
debounce for the in-page media-chrome control (helps responsiveness but does
|
|
41
|
-
NOT cover iOS native fullscreen, so the proxy-side settle is the must-have);
|
|
42
|
-
the cold-torrent piece-download and weak-host encode latency that make the
|
|
43
|
-
FIRST post-seek segment slow regardless — the settle removes the infinite
|
|
44
|
-
thrash, not the one-time seek latency.
|
|
45
|
-
|
|
46
|
-
## Capabilities
|
|
47
|
-
|
|
48
|
-
### Modified Capabilities
|
|
49
|
-
|
|
50
|
-
- `seek-debounce` (server-side HLS seeking): far-segment requests are
|
|
51
|
-
debounced into a single ffmpeg restart at the settled position.
|
|
52
|
-
|
|
53
|
-
## Impact
|
|
54
|
-
|
|
55
|
-
- `services/hls-session-manager.js` — `#ensureEncodingFor` gains the settle
|
|
56
|
-
window; per-session settle state; `disposeSession` clears the timer;
|
|
57
|
-
constants. `RESTART_COOLDOWN_MS` is retained only as a minimum gap between
|
|
58
|
-
actual restarts.
|
|
59
|
-
- Proxy release + ha-addon version bump (per release rules).
|
|
1
|
+
# Proposal: Debounce server-side seek restarts
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Field evidence (iPhone/Safari, Poirot, 2026-07-09): after a scrub the player
|
|
6
|
+
hung indefinitely. The proxy restarts ffmpeg at a requested segment when the
|
|
7
|
+
request lands outside the look-ahead window (server-side seek). The native
|
|
8
|
+
iOS player issued scattered segment requests after the seek —
|
|
9
|
+
`367 → 732 → 369 → 368 → 370` — each 25–35 s apart. The existing guard
|
|
10
|
+
(`RESTART_COOLDOWN_MS = 4000`) only suppresses restarts within 4 s of the
|
|
11
|
+
last one, so with requests spaced far wider than that, EVERY scattered
|
|
12
|
+
request restarted ffmpeg at a new position. ffmpeg ping-ponged between
|
|
13
|
+
positions and never finished a single segment, so playback stalled at
|
|
14
|
+
`currentTime` with `bufferedAhead=0` — "заглохло наглухо".
|
|
15
|
+
|
|
16
|
+
The fix is the user's idea placed where every client is covered (including
|
|
17
|
+
the iOS native fullscreen player, whose scrubber the web app cannot control):
|
|
18
|
+
don't act on the first far request — wait for the burst to settle, then
|
|
19
|
+
restart ONCE at the position the player ended on.
|
|
20
|
+
|
|
21
|
+
## What Changes
|
|
22
|
+
|
|
23
|
+
- Replace the fixed post-restart cooldown as the anti-thrash mechanism with a
|
|
24
|
+
**settle window**. When a segment request lands outside the look-ahead
|
|
25
|
+
window (a seek), the proxy does NOT restart immediately: it records the
|
|
26
|
+
requested index as the pending seek target and arms a short quiet-period
|
|
27
|
+
timer. Each further far request updates the target to the latest index and
|
|
28
|
+
re-arms the timer (debounce). When the quiet period elapses with no new far
|
|
29
|
+
request, ffmpeg restarts ONCE at the pending target. A hard cap bounds the
|
|
30
|
+
total wait so a genuine seek is never delayed more than a fixed budget even
|
|
31
|
+
while the scrubber is still moving.
|
|
32
|
+
- Meanwhile the segment route behaves exactly as today — it long-polls and
|
|
33
|
+
the client retries — so the player simply waits out the (short) settle
|
|
34
|
+
instead of driving restarts.
|
|
35
|
+
- "Last far index wins" self-corrects: if the settle resolves on the wrong
|
|
36
|
+
position (e.g. a lone probe request), the player's next request re-arms one
|
|
37
|
+
more settle — at most one extra cycle, never the old infinite ping-pong.
|
|
38
|
+
|
|
39
|
+
Out of scope (documented in design.md): an optional client-side scrubber
|
|
40
|
+
debounce for the in-page media-chrome control (helps responsiveness but does
|
|
41
|
+
NOT cover iOS native fullscreen, so the proxy-side settle is the must-have);
|
|
42
|
+
the cold-torrent piece-download and weak-host encode latency that make the
|
|
43
|
+
FIRST post-seek segment slow regardless — the settle removes the infinite
|
|
44
|
+
thrash, not the one-time seek latency.
|
|
45
|
+
|
|
46
|
+
## Capabilities
|
|
47
|
+
|
|
48
|
+
### Modified Capabilities
|
|
49
|
+
|
|
50
|
+
- `seek-debounce` (server-side HLS seeking): far-segment requests are
|
|
51
|
+
debounced into a single ffmpeg restart at the settled position.
|
|
52
|
+
|
|
53
|
+
## Impact
|
|
54
|
+
|
|
55
|
+
- `services/hls-session-manager.js` — `#ensureEncodingFor` gains the settle
|
|
56
|
+
window; per-session settle state; `disposeSession` clears the timer;
|
|
57
|
+
constants. `RESTART_COOLDOWN_MS` is retained only as a minimum gap between
|
|
58
|
+
actual restarts.
|
|
59
|
+
- Proxy release + ha-addon version bump (per release rules).
|