@torrent-tv/proxy 2.89.16 → 2.89.18
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/README.md +14 -3
- package/package.json +10 -2
- package/routes/api/client-logs/post.js +9 -2
- package/server.js +0 -11
- package/.claude/commands/opsx/apply.md +0 -155
- package/.claude/commands/opsx/archive.md +0 -160
- package/.claude/commands/opsx/explore.md +0 -174
- package/.claude/commands/opsx/propose.md +0 -109
- package/.claude/commands/opsx/sync.md +0 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
- package/.claude/skills/openspec-explore/SKILL.md +0 -289
- package/.claude/skills/openspec-propose/SKILL.md +0 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.dockerignore +0 -5
- package/AGENTS.md +0 -363
- package/CHANGELOG.md +0 -2373
- package/Dockerfile +0 -30
- package/biome.json +0 -412
- package/corpus.tmp.json +0 -1
- package/docs/browser-proxy-contract.md +0 -155
- package/docs/container-architecture.md +0 -273
- package/docs/download-architecture.md +0 -261
- package/docs/encode-architecture.md +0 -765
- package/docs/encode-run-state.md +0 -96
- package/docs/logs.md +0 -138
- package/docs/playback-start-forecast.md +0 -113
- package/docs/socks5-egress-research.md +0 -114
- package/docs/storage-architecture.md +0 -273
- package/knip.json +0 -14
- package/main2.cpuprofile +0 -1
- package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
- package/openspec/changes/adaptive-bitrate/design.md +0 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
- package/openspec/changes/binary-distribution/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/design.md +0 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
- package/openspec/changes/cold-start/design.md +0 -98
- package/openspec/changes/cold-start/proposal.md +0 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
- package/openspec/changes/cold-start/tasks.md +0 -48
- package/openspec/changes/disk-cap/.openspec.yaml +0 -2
- package/openspec/changes/disk-cap/proposal.md +0 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
- package/openspec/changes/disk-cap/tasks.md +0 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
- package/openspec/changes/proxy-observability/design.md +0 -38
- package/openspec/changes/proxy-observability/proposal.md +0 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
- package/openspec/changes/proxy-observability/tasks.md +0 -21
- package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
- package/openspec/changes/seek-debounce/design.md +0 -118
- package/openspec/changes/seek-debounce/proposal.md +0 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
- package/openspec/changes/seek-debounce/tasks.md +0 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
- package/openspec/changes/subtitle-language/proposal.md +0 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
- package/openspec/changes/subtitle-language/tasks.md +0 -26
- package/openspec/changes/track-selection/.openspec.yaml +0 -2
- package/openspec/changes/track-selection/design.md +0 -47
- package/openspec/changes/track-selection/proposal.md +0 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
- package/openspec/changes/track-selection/tasks.md +0 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
- package/openspec/changes/transcode-quality/proposal.md +0 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
- package/openspec/changes/transcode-quality/tasks.md +0 -63
- package/openspec/config.yaml +0 -38
- package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
- package/research/double-spawn-2026-09-10.md +0 -171
- package/research/handover-2026-09-13.md +0 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
- package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
- package/scripts/build-audio-samples.mjs +0 -31
- package/scripts/render-run-graph.js +0 -173
- package/test/asset-paths.test.js +0 -28
- package/test/audio-buffer-continuity.test.js +0 -77
- package/test/audio-calibration.test.js +0 -23
- package/test/audio-inventory.test.js +0 -212
- package/test/audio-track-bitrate.test.js +0 -40
- package/test/audio-warm-recorded.test.js +0 -63
- package/test/auto-quality-step.test.js +0 -762
- package/test/available-share.test.js +0 -76
- package/test/background-fill.test.js +0 -190
- package/test/client-log-file.test.js +0 -126
- package/test/coalesce.test.js +0 -39
- package/test/completed-files.test.js +0 -115
- package/test/concurrent-cost.test.js +0 -148
- package/test/container-media-info.test.js +0 -228
- package/test/contention.test.js +0 -76
- package/test/core-dumps.test.js +0 -57
- package/test/coverage-follows-the-disk.test.js +0 -233
- package/test/coverage-map.test.js +0 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
- package/test/cut-times-timeline.test.js +0 -65
- package/test/cuts-follow-published-grid.test.js +0 -109
- package/test/data-channel-frame.test.js +0 -63
- package/test/data-channel-viewer-lifecycle.test.js +0 -193
- package/test/dead-torrent-handle.test.js +0 -52
- package/test/declared-rates.test.js +0 -116
- package/test/declared-tracks.test.js +0 -43
- package/test/decode-cost-fit.test.js +0 -202
- package/test/decode-cost.test.js +0 -475
- package/test/delivery-probe-field.test.js +0 -173
- package/test/delivery-probe.test.js +0 -382
- package/test/demand-register.test.js +0 -206
- package/test/dht-bootstrap.test.js +0 -72
- package/test/drain-threat.test.js +0 -65
- package/test/duplicate-add.test.js +0 -48
- package/test/encode-admission.test.js +0 -170
- package/test/encode-cost.test.js +0 -275
- package/test/encode-exit.test.js +0 -111
- package/test/encode-orchestrator.test.js +0 -426
- package/test/encode-plan-viewers.test.js +0 -725
- package/test/encode-plan.test.js +0 -545
- package/test/encode-run-state.test.js +0 -314
- package/test/encode-run.test.js +0 -248
- package/test/encode-slope.test.js +0 -46
- package/test/encoded-output.test.js +0 -39
- package/test/encoder-kinds.test.js +0 -122
- package/test/encoder-readings.test.js +0 -68
- package/test/episode-naming.test.js +0 -131
- package/test/every-claimant-is-told-its-share.test.js +0 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
- package/test/failed-start-breaker.test.js +0 -202
- package/test/fastest-wires.test.js +0 -143
- package/test/ffmpeg-stream-counts.test.js +0 -101
- package/test/file-edges.test.js +0 -191
- package/test/files-in-use.test.js +0 -34
- package/test/fixtures/audio-frame-boundary.json +0 -1
- package/test/fixtures/refused-audio-20261002.json +0 -10
- package/test/fmp4-presentation-clock.test.js +0 -148
- package/test/fmp4-self-contained.test.js +0 -80
- package/test/fmp4-timestamps.test.js +0 -140
- package/test/fmp4-track-completeness.test.js +0 -41
- package/test/format-keyed-outputs.test.js +0 -78
- package/test/health-metrics.test.js +0 -36
- package/test/held-read-ownership.test.js +0 -77
- package/test/held-request-width.test.js +0 -157
- package/test/helpers/encode-run.js +0 -248
- package/test/helpers/held-file.js +0 -77
- package/test/helpers/manager.js +0 -62
- package/test/helpers/matroska-file.js +0 -296
- package/test/helpers/output-spec.js +0 -63
- package/test/host-load.test.js +0 -68
- package/test/index-accuracy-is-about-the-file.test.js +0 -128
- package/test/init-compat.test.js +0 -219
- package/test/input-lost-quiets-the-plan.test.js +0 -261
- package/test/input-unavailable.test.js +0 -48
- package/test/installation-id.test.js +0 -39
- package/test/keeping-period.test.js +0 -83
- package/test/keyframe-index-accuracy.test.js +0 -188
- package/test/keyframe-table-budget.test.js +0 -123
- package/test/keyframes-belong-to-the-file.test.js +0 -249
- package/test/learned-median.test.js +0 -37
- package/test/letting-go-asks-who-is-watching.test.js +0 -191
- package/test/limit-row-by-area.test.js +0 -60
- package/test/link-budget.test.js +0 -111
- package/test/link-probe.test.js +0 -56
- package/test/local-encoding-data.test.js +0 -273
- package/test/logger-repeats.test.js +0 -134
- package/test/look-ahead-coverage.test.js +0 -56
- package/test/manager-surface.test.js +0 -291
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cluster-discovery.test.js +0 -323
- package/test/matroska-cues-track.test.js +0 -192
- package/test/media-time.test.js +0 -62
- package/test/memory-budget.test.js +0 -390
- package/test/move-cost.test.js +0 -216
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +0 -173
- package/test/one-authority.test.js +0 -285
- package/test/one-budget.test.js +0 -149
- package/test/one-output-one-session.test.js +0 -273
- package/test/one-resource-divided.test.js +0 -185
- package/test/one-serving-output-per-height.test.js +0 -458
- package/test/orchestrator-wired.test.js +0 -207
- package/test/output-catalog.test.js +0 -39
- package/test/output-shape.test.js +0 -21
- package/test/output-spec.test.js +0 -251
- package/test/output-speed.test.js +0 -92
- package/test/outputs-being-prepared.test.js +0 -40
- package/test/packet-witness-ring.test.js +0 -236
- package/test/packet-witness.test.js +0 -148
- package/test/piece-completeness.test.js +0 -78
- package/test/piece-disk-store.test.js +0 -401
- package/test/piece-from-whole-file.test.js +0 -129
- package/test/piece-lru.test.js +0 -319
- package/test/piece-reader.test.js +0 -162
- package/test/piece-selection-offset.test.js +0 -85
- package/test/piece-store-eviction.test.js +0 -595
- package/test/piece-store-never-refuses.test.js +0 -153
- package/test/piece-store-reservations.test.js +0 -403
- package/test/piece-store-slow-disk.test.js +0 -159
- package/test/piece-tail.test.js +0 -97
- package/test/plan-host-timings.test.js +0 -68
- package/test/plan-is-steady.test.js +0 -225
- package/test/playback-readiness.test.js +0 -518
- package/test/playback-start-regressions.test.js +0 -101
- package/test/priority-map-download.test.js +0 -174
- package/test/priority-map-emptied.test.js +0 -204
- package/test/priority-map-per-output.test.js +0 -215
- package/test/priority-map.test.js +0 -163
- package/test/probe-wedge-certainty.test.js +0 -60
- package/test/produced-copy-choice.test.js +0 -213
- package/test/published-timeline.test.js +0 -83
- package/test/quality-offer.test.js +0 -160
- package/test/quality-variants.test.js +0 -1274
- package/test/rate-control-in-the-output.test.js +0 -181
- package/test/read-survives-withdrawal.test.js +0 -164
- package/test/read-window.test.js +0 -365
- package/test/reads-not-yet-arrived.test.js +0 -218
- package/test/resume-warm.test.js +0 -39
- package/test/run-graph-drift.test.js +0 -37
- package/test/run-inputs.test.js +0 -80
- package/test/run-intervals.test.js +0 -100
- package/test/run-position-follows-published-grid.test.js +0 -89
- package/test/run-progress.test.js +0 -64
- package/test/same-height-switch.test.js +0 -663
- package/test/seek-frees-the-viewers-requests.test.js +0 -217
- package/test/seek-landing.test.js +0 -109
- package/test/seek-target-not-superseded.test.js +0 -195
- package/test/segment-closed-on-both-branches.test.js +0 -114
- package/test/segment-cut-times.test.js +0 -63
- package/test/segment-demand.test.js +0 -80
- package/test/segment-hold-supersede.test.js +0 -91
- package/test/segment-judged-by-its-own-header.test.js +0 -222
- package/test/segment-serve-wiring.test.js +0 -481
- package/test/segment-store-eviction.test.js +0 -247
- package/test/segment-store.test.js +0 -352
- package/test/segment-true-start.test.js +0 -86
- package/test/segments-are-shared.test.js +0 -176
- package/test/send-chunk-bytes.test.js +0 -86
- package/test/serving-existing-output.test.js +0 -111
- package/test/serving-output.test.js +0 -102
- package/test/shared-piece-store.test.js +0 -310
- package/test/sidecar-naming.test.js +0 -142
- package/test/soundtrack-rate-bound.test.js +0 -289
- package/test/source-file.test.js +0 -133
- package/test/source-files-route.test.js +0 -113
- package/test/source-registry.test.js +0 -18
- package/test/split-a-stretch.test.js +0 -139
- package/test/stale-request-after-seek.test.js +0 -158
- package/test/startup-readings.test.js +0 -124
- package/test/stream-route.test.js +0 -237
- package/test/subtitle-cue-framing.test.js +0 -200
- package/test/subtitle-cue-source.test.js +0 -102
- package/test/subtitle-cue-walk.test.js +0 -385
- package/test/subtitle-cursor.test.js +0 -63
- package/test/subtitle-defaults.test.js +0 -118
- package/test/subtitle-language.test.js +0 -252
- package/test/subtitle-metadata.test.js +0 -15
- package/test/subtitle-plan-waits.test.js +0 -125
- package/test/subtitle-track-numbering.test.js +0 -391
- package/test/supply-margin.test.js +0 -190
- package/test/swarm-follows-readers.test.js +0 -174
- package/test/swarm-reach.test.js +0 -130
- package/test/swarm-selection.test.js +0 -220
- package/test/tail-duplication.test.js +0 -167
- package/test/the-channel-has-both-ends.test.js +0 -91
- package/test/the-torrent-thread-serves-bytes.test.js +0 -107
- package/test/thread-pool.test.js +0 -53
- package/test/timeline.test.js +0 -84
- package/test/torrent-contents.test.js +0 -235
- package/test/torrent-cost.test.js +0 -71
- package/test/torrent-files.test.js +0 -221
- package/test/torrent-naming.test.js +0 -255
- package/test/tracks-begin-together.test.js +0 -209
- package/test/tunnel-renewal.test.js +0 -167
- package/test/two-viewers-one-picture.test.js +0 -508
- package/test/upload-hurry.test.js +0 -186
- package/test/usrsctp-state.test.js +0 -131
- package/test/video-facts.test.js +0 -102
- package/test/viewer-assignments.test.js +0 -143
- package/test/viewer-outputs.test.js +0 -285
- package/test/viewer-presence.test.js +0 -116
- package/test/viewer-requests-admission.test.js +0 -92
- package/test/viewer-starts-on-map.test.js +0 -27
- package/test/viewer-states.test.js +0 -192
- package/test/viewer-subtitle-subscription.test.js +0 -96
- package/test/viewer.test.js +0 -139
- package/test/viewing-generations.test.js +0 -390
- package/test/visible-rung.test.js +0 -37
- package/test/wait-ledger.test.js +0 -100
- package/test/webrtc-shim.test.js +0 -149
- package/test/wedge-certainty.test.js +0 -131
- package/test/whole-file-held-read.test.js +0 -30
- package/test/withdraw-piece-claim.test.js +0 -212
- package/test/worker-channel.test.js +0 -158
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# cold-start — delta spec (proxy)
|
|
2
|
-
|
|
3
|
-
## ADDED Requirements
|
|
4
|
-
|
|
5
|
-
### Requirement: Session create reuses the planner's probe
|
|
6
|
-
|
|
7
|
-
Creating an HLS transcode session SHALL NOT re-probe an input whose media
|
|
8
|
-
info the playback planner already probed and cached for the same source and
|
|
9
|
-
file. The cached info is used only when its critical fields (duration,
|
|
10
|
-
width, height) are present and valid; otherwise the session SHALL probe as
|
|
11
|
-
before. Cache lifetime and invalidation follow the plan cache exactly.
|
|
12
|
-
|
|
13
|
-
#### Scenario: Warm plan, immediate session
|
|
14
|
-
- **WHEN** the browser requests a transcode session right after receiving a
|
|
15
|
-
playback plan for the same file
|
|
16
|
-
- **THEN** the session starts without a second ffmpeg input scan and its
|
|
17
|
-
playlist/ladder decisions are identical to what the probe would have
|
|
18
|
-
produced
|
|
19
|
-
|
|
20
|
-
#### Scenario: Cache cannot serve
|
|
21
|
-
- **WHEN** no cached media info exists (e.g. proxy restarted between plan
|
|
22
|
-
and session) or a critical field is missing
|
|
23
|
-
- **THEN** the session probes the input itself, exactly as before this
|
|
24
|
-
change
|
|
25
|
-
|
|
26
|
-
### Requirement: The file-body start is warm before the encoder needs it
|
|
27
|
-
|
|
28
|
-
Once a playback plan probe succeeds, the proxy SHALL prefetch the beginning
|
|
29
|
-
of the file body (bounded, ~16 MB) in the background, without delaying the
|
|
30
|
-
plan response and without competing with a still-running header probe.
|
|
31
|
-
|
|
32
|
-
#### Scenario: First segment does not wait for pieces
|
|
33
|
-
- **WHEN** the viewer confirms playback within the normal flow (seconds
|
|
34
|
-
after the plan)
|
|
35
|
-
- **THEN** the encoder's initial reads are served from already-downloaded
|
|
36
|
-
data and the first segment's production is not blocked on piece download
|
|
37
|
-
|
|
38
|
-
### Requirement: Session startup is measurable per stage
|
|
39
|
-
|
|
40
|
-
Session creation SHALL log the media-info acquisition time (and whether it
|
|
41
|
-
was cached or probed), the keyframe-probe time when it runs, and — once per
|
|
42
|
-
session — the time from session-create entry to the first servable segment.
|
|
43
|
-
|
|
44
|
-
#### Scenario: Field regression triage
|
|
45
|
-
- **WHEN** a tester reports a slow start
|
|
46
|
-
- **THEN** the proxy log shows, for that session, where the time went:
|
|
47
|
-
media info, keyframes, or first-segment production
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# Tasks: Cold-start reduction (proxy side)
|
|
2
|
-
|
|
3
|
-
Execute in order; design.md is normative. Read the code regions listed at
|
|
4
|
-
the top of design.md first.
|
|
5
|
-
|
|
6
|
-
## 1. Media-info reuse
|
|
7
|
-
|
|
8
|
-
- [ ] 1.1 Make the banner-parse helpers of `probeInputMediaInfo` reusable by
|
|
9
|
-
the planner (export or shared module — no duplication). `node --check`
|
|
10
|
-
both files.
|
|
11
|
-
- [ ] 1.2 playback-planner: build + cache `mediaInfo` alongside the plan on
|
|
12
|
-
a codecs-detected probe; expose `getCachedMediaInfo({ sourceKey,
|
|
13
|
-
fileIndex })`. Pending probes cache nothing.
|
|
14
|
-
- [ ] 1.3 server.js: pass `getCachedMediaInfo` into HlsSessionManager;
|
|
15
|
-
hls-session-manager: consult it in `createSession` with the
|
|
16
|
-
critical-fields gate (duration/width/height), probe as fallback.
|
|
17
|
-
- [ ] 1.4 Verify equivalence: for one file, run plan → session with the
|
|
18
|
-
cache and (by temporarily disabling the wiring) without it — the
|
|
19
|
-
session log line (duration, segments, ladder/rung) must be identical.
|
|
20
|
-
|
|
21
|
-
## 2. Body-start prefetch
|
|
22
|
-
|
|
23
|
-
- [ ] 2.1 playback-planner: fire-and-forget 16 MB head prefetch
|
|
24
|
-
(`prefetchFileEdges` with `tailBytes: 0`) at the probe-success point;
|
|
25
|
-
never on pending polls; never awaited.
|
|
26
|
-
- [ ] 2.2 Verify: on a fresh well-seeded magnet, proxy log/order shows the
|
|
27
|
-
prefetch starting right after the plan while the session that follows
|
|
28
|
-
produces its first segment without a piece-wait stall (compare
|
|
29
|
-
first-segment ms with task 3 timings before/after).
|
|
30
|
-
|
|
31
|
-
## 3. Stage timings
|
|
32
|
-
|
|
33
|
-
- [ ] 3.1 `createSession`: one `cold-start …: media-info=<ms> (cached|probed)
|
|
34
|
-
keyframes=<ms|skipped> create-total=<ms>` line.
|
|
35
|
-
- [ ] 3.2 First-segment-ready line (`+<ms>` since create entry), once per
|
|
36
|
-
session (timestamp on the session object + boolean guard).
|
|
37
|
-
|
|
38
|
-
## 4. Verification and release
|
|
39
|
-
|
|
40
|
-
- [ ] 4.1 Regression: normal HLS transcode start, video-copy start (keyframe
|
|
41
|
-
probe still runs), seek-restart, quality switch — behaviour unchanged;
|
|
42
|
-
timings visible in the log.
|
|
43
|
-
- [ ] 4.2 Before/after numbers on the dev HA proxy for one cold magnet:
|
|
44
|
-
media-info ms (probed → cached) and first-segment ms. Record them in
|
|
45
|
-
this file.
|
|
46
|
-
- [ ] 4.3 CHANGELOG entry (current version + 1 patch); `npm run patch`; bump
|
|
47
|
-
ha-addon `config.yaml` + its CHANGELOG; push; update the addon in HA
|
|
48
|
-
(proxy FIRST, then addon — release order per root CLAUDE.md).
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# Proposal: Global disk cap with LRU eviction (Disk hygiene Level 1, final)
|
|
2
|
-
|
|
3
|
-
## Why
|
|
4
|
-
|
|
5
|
-
Downloaded torrent data is already removed on a 300 s idle TTL and at
|
|
6
|
-
shutdown, and orphans are swept at startup — but under pressure (several
|
|
7
|
-
large files opened within the TTL window, or a fast fill) the total can
|
|
8
|
-
still grow unbounded and fill a small Home Assistant host's disk
|
|
9
|
-
(SD/eMMC on a Yellow/Pi is often 16–32 GB). A full disk can take down Home
|
|
10
|
-
Assistant itself. This adds the last missing Level 1 piece: a global cap.
|
|
11
|
-
|
|
12
|
-
## What Changes
|
|
13
|
-
|
|
14
|
-
- The pool tracks total downloaded bytes and, when it exceeds a cap, evicts
|
|
15
|
-
whole torrents with NO active reader, least-recently-used first, until
|
|
16
|
-
back under the cap (checked every 30 s and reused via the existing
|
|
17
|
-
remove-with-store path). A torrent that is currently playing is never
|
|
18
|
-
evicted — we cannot delete what is in use.
|
|
19
|
-
- The cap defaults to `min(10 GB, half of free disk)` (measured via
|
|
20
|
-
`statfs` on the store filesystem), and is overridable with
|
|
21
|
-
`--max-disk-bytes` (0 disables).
|
|
22
|
-
|
|
23
|
-
## Capabilities
|
|
24
|
-
|
|
25
|
-
### New Capabilities
|
|
26
|
-
|
|
27
|
-
- `disk-cap`: bounded total on-disk footprint via LRU eviction.
|
|
28
|
-
|
|
29
|
-
### Modified Capabilities
|
|
30
|
-
|
|
31
|
-
<!-- none -->
|
|
32
|
-
|
|
33
|
-
## Impact
|
|
34
|
-
|
|
35
|
-
- `services/torrent/torrent-pool.js` (access tracking, cap enforcement),
|
|
36
|
-
`bin/cli.js` (`--max-disk-bytes`), `server.js` (option pass-through);
|
|
37
|
-
ha-addon bump. Part of the proxy transcode/hygiene batch (2.9.29).
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# disk-cap — delta spec
|
|
2
|
-
|
|
3
|
-
## ADDED Requirements
|
|
4
|
-
|
|
5
|
-
### Requirement: Total torrent data is bounded by a disk cap
|
|
6
|
-
The pool SHALL keep the total downloaded torrent footprint under a cap. When
|
|
7
|
-
the total exceeds the cap it SHALL evict whole torrents that have no active
|
|
8
|
-
file reader, least-recently-used first, removing each with its on-disk store,
|
|
9
|
-
until back under the cap or no evictable torrent remains. A torrent with an
|
|
10
|
-
active reader SHALL NEVER be evicted. The cap SHALL default to the smaller of
|
|
11
|
-
10 GB and half the free disk, and be overridable (0 disables).
|
|
12
|
-
|
|
13
|
-
#### Scenario: Idle torrents evicted under pressure
|
|
14
|
-
- **WHEN** the total downloaded data exceeds the cap and some torrents have no
|
|
15
|
-
active reader
|
|
16
|
-
- **THEN** the least-recently-used idle torrents are removed with their stores
|
|
17
|
-
until the total is back under the cap
|
|
18
|
-
|
|
19
|
-
#### Scenario: Active torrent protected
|
|
20
|
-
- **WHEN** the cap is exceeded but the only large torrent is currently playing
|
|
21
|
-
- **THEN** it is not evicted (the cap cannot delete in-use data)
|
|
22
|
-
|
|
23
|
-
#### Scenario: Cap disabled
|
|
24
|
-
- **WHEN** the cap is set to 0
|
|
25
|
-
- **THEN** no eviction occurs
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Tasks: Global disk cap with LRU eviction
|
|
2
|
-
|
|
3
|
-
## 1. Implementation
|
|
4
|
-
|
|
5
|
-
- [x] 1.1 Access tracking (`#lastAccess`) updated on getTorrent (incl. the
|
|
6
|
-
duplicate-infoHash path) and acquireFile
|
|
7
|
-
- [x] 1.2 Cap computed at construction (`min(10GB, half free)` via statfs) or
|
|
8
|
-
taken from the `maxDiskBytes` option; 0 disables
|
|
9
|
-
- [x] 1.3 Periodic (30 s) `#enforceDiskCap`: evict zero-reader torrents
|
|
10
|
-
LRU-first via the existing remove-with-store path; clear timer on
|
|
11
|
-
destroyAll; drop `#lastAccess` on removal
|
|
12
|
-
- [x] 1.4 `--max-disk-bytes` CLI flag → server.js → pool
|
|
13
|
-
- [x] 1.5 Syntax checks + unit-test the eviction ordering/active-skip
|
|
14
|
-
|
|
15
|
-
## 2. Release
|
|
16
|
-
|
|
17
|
-
- [ ] 2.1 Ship in the proxy batch (2.9.29) + ha-addon bump
|
|
18
|
-
- [ ] 2.2 Field-check on the host: open several large files, confirm idle
|
|
19
|
-
ones are evicted and playback is never interrupted
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Design: Proxy observability
|
|
2
|
-
|
|
3
|
-
## Context
|
|
4
|
-
|
|
5
|
-
Diagnosing the 2026-07-06 failures required docker exec into the addon
|
|
6
|
-
container (version) and offered no explanation for zero-peer torrents.
|
|
7
|
-
See proposal for the three gaps.
|
|
8
|
-
|
|
9
|
-
## Goals / Non-Goals
|
|
10
|
-
|
|
11
|
-
**Goals:** remote version visibility; explainable zero-peer torrents; clean
|
|
12
|
-
logs.
|
|
13
|
-
|
|
14
|
-
**Non-Goals:** peer-origin tags in `[stats]` (tracker/DHT/PEX attribution is
|
|
15
|
-
not cheaply available on WebTorrent's public API — dropped from the original
|
|
16
|
-
proposal scope); structured/queryable log output.
|
|
17
|
-
|
|
18
|
-
## Decisions
|
|
19
|
-
|
|
20
|
-
1. **Version via createRequire of package.json** (same pattern as the server
|
|
21
|
-
repo), passed as a route dep — no version literal to forget on release.
|
|
22
|
-
2. **Announce results from `torrent.discovery.tracker` ("update" event)** —
|
|
23
|
-
internal WebTorrent API, therefore guarded with optional chaining and a
|
|
24
|
-
fallback log line when absent; a breaking WebTorrent upgrade degrades to
|
|
25
|
-
"not logged", never to a crash. Torrent-level `warning` events carry
|
|
26
|
-
tracker rejections/errors and are a public API.
|
|
27
|
-
3. **SSDP fix = setMaxListeners(0) on the one emitter** right after the
|
|
28
|
-
first successful map() (the UPnP client is created lazily by it).
|
|
29
|
-
Alternatives rejected: raising EventEmitter.defaultMaxListeners (global,
|
|
30
|
-
hides real leaks elsewhere); one shared NatAPI for both mappers (couples
|
|
31
|
-
TCP/UDP lifecycles for no gain).
|
|
32
|
-
|
|
33
|
-
## Risks / Trade-offs
|
|
34
|
-
|
|
35
|
-
- [Announce logging depends on WebTorrent internals] → defensive access +
|
|
36
|
-
explicit fallback line; covered by the spec's best-effort clause.
|
|
37
|
-
- [Announce lines add log volume] → one line per announce interval
|
|
38
|
-
(minutes), negligible next to the existing [stats] cadence.
|
|
@@ -1,49 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
@@ -1,118 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
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,41 +0,0 @@
|
|
|
1
|
-
# seek-debounce — delta spec (proxy)
|
|
2
|
-
|
|
3
|
-
## ADDED Requirements
|
|
4
|
-
|
|
5
|
-
### Requirement: Scattered post-seek segment requests collapse to one restart
|
|
6
|
-
|
|
7
|
-
When segment requests land outside the running encode's look-ahead window
|
|
8
|
-
(server-side seeks), the proxy SHALL NOT restart ffmpeg on each one. It SHALL
|
|
9
|
-
wait a short settle period, treating further out-of-window requests as
|
|
10
|
-
re-arming the period and updating the target to the most recently requested
|
|
11
|
-
index, and then restart the encoder exactly once at the settled target. A
|
|
12
|
-
fixed cap SHALL bound the total settle wait so a genuine seek is not delayed
|
|
13
|
-
indefinitely while the scrubber is still moving. During the settle the
|
|
14
|
-
segment route behaves as before (long-poll / client retry).
|
|
15
|
-
|
|
16
|
-
#### Scenario: Scrub emits a burst of scattered requests
|
|
17
|
-
- **WHEN** a player, after a seek, requests several far-apart segments in
|
|
18
|
-
quick succession (e.g. 367, 732, 369, 368, 370)
|
|
19
|
-
- **THEN** ffmpeg is restarted only once, at the last requested index, and
|
|
20
|
-
produces a continuous run from there — no ping-pong between positions
|
|
21
|
-
|
|
22
|
-
#### Scenario: Settle resolves on the wrong position
|
|
23
|
-
- **WHEN** the settled restart target turns out not to be where the player
|
|
24
|
-
ultimately needs to play (e.g. it was a lone probe request)
|
|
25
|
-
- **THEN** the player's next out-of-window request arms exactly one more
|
|
26
|
-
settle and one more restart — never an unbounded restart loop
|
|
27
|
-
|
|
28
|
-
#### Scenario: Request falls back inside the window
|
|
29
|
-
- **WHEN** a requested segment is within the current run's look-ahead window
|
|
30
|
-
- **THEN** it is served by the running encode with no restart and without
|
|
31
|
-
affecting any pending settle
|
|
32
|
-
|
|
33
|
-
### Requirement: A pending settle never outlives its session
|
|
34
|
-
|
|
35
|
-
When a session is disposed, any pending settle timer SHALL be cleared so it
|
|
36
|
-
cannot fire and restart a disposed session.
|
|
37
|
-
|
|
38
|
-
#### Scenario: Session disposed mid-settle
|
|
39
|
-
- **WHEN** a session with a pending seek-settle timer is disposed (idle TTL,
|
|
40
|
-
shutdown, or teardown)
|
|
41
|
-
- **THEN** the timer is cleared and no encode restart occurs afterwards
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Tasks: Debounce server-side seek restarts
|
|
2
|
-
|
|
3
|
-
## 1. Implementation (proxy)
|
|
4
|
-
|
|
5
|
-
- [x] 1.1 Add `SEEK_SETTLE_MS = 1200` and `SEEK_SETTLE_MAX_MS = 2500` to the
|
|
6
|
-
constants block in `hls-session-manager.js`.
|
|
7
|
-
- [x] 1.2 Add session state `seekSettleTimer: null`, `seekTarget: null`,
|
|
8
|
-
`seekFirstFarAt: 0` where `lastRestartAt` is initialised.
|
|
9
|
-
- [x] 1.3 Rewrite the out-of-window branch of `#ensureEncodingFor` as the
|
|
10
|
-
settle/debounce (design.md): record target, arm/re-arm the timer with
|
|
11
|
-
the capped delay, restart once on fire (`#fireSettledSeek`), re-arm for
|
|
12
|
-
the cooldown remainder if still cooling down. `timer.unref?.()`.
|
|
13
|
-
- [x] 1.4 `disposeSession`: clear `seekSettleTimer` and null it.
|
|
14
|
-
- [x] 1.5 Restart log line: `transcode <id> seek settle → restart at segment #<target>`.
|
|
15
|
-
|
|
16
|
-
## 2. Verification
|
|
17
|
-
|
|
18
|
-
- [x] 2.1 Timing/target logic verified with a fake-clock replica: burst
|
|
19
|
-
`367,732,369,368,370` → one restart at 370; lone later far request →
|
|
20
|
-
exactly one more restart (900); disposal mid-settle → no restart; the
|
|
21
|
-
2.5 s cap forces a fire while the scrubber keeps moving. (Replica, not
|
|
22
|
-
the wired private method — the methods are private and `#startEncodeRun`
|
|
23
|
-
spawns ffmpeg; the wired code mirrors the replica.)
|
|
24
|
-
- [x] 2.2 `node --check services/hls-session-manager.js`.
|
|
25
|
-
|
|
26
|
-
## 3. Release
|
|
27
|
-
|
|
28
|
-
- [ ] 3.1 CHANGELOG (proxy, next patch) + `npm run patch` (user OTP).
|
|
29
|
-
- [ ] 3.2 Bump `ha-addon/torrent_tv_proxy/config.yaml` + CHANGELOG; push;
|
|
30
|
-
update the addon in HA.
|
|
31
|
-
- [ ] 3.3 Field: scrub Poirot on iOS → a single `seek settle → restart` per
|
|
32
|
-
scrub (not a train of `seek → restart`), playback resumes after the
|
|
33
|
-
settle + the unavoidable cold-segment wait.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Proposal: Proxy owns subtitle conversion + content-based language detection
|
|
2
|
-
|
|
3
|
-
## Why
|
|
4
|
-
|
|
5
|
-
Subtitle language was guessed only from the filename, so a file without a
|
|
6
|
-
language code (e.g. the Enola release's `.srt`) showed "Unknown", and the
|
|
7
|
-
owner specifically does not want Ukrainian and Russian confused. Reliable
|
|
8
|
-
detection needs an n-gram model on the actual text — which belongs on the
|
|
9
|
-
proxy (Node, node_modules, no per-browser payload, and the subtitle bytes are
|
|
10
|
-
right there) rather than in the browser.
|
|
11
|
-
|
|
12
|
-
## What Changes
|
|
13
|
-
|
|
14
|
-
- The proxy becomes the single owner of subtitle content: `GET /api/subtitles`
|
|
15
|
-
now also serves EXTERNAL subtitle files (no `trackIndex`) — it reads the
|
|
16
|
-
file, decodes its encoding (UTF-8 or Windows-1251, common for Russian
|
|
17
|
-
`.srt`), and converts `.srt`/`.ass`/`.ssa` → WebVTT here. The browser no
|
|
18
|
-
longer converts.
|
|
19
|
-
- The proxy detects the language from the full text with `franc` (n-gram /
|
|
20
|
-
trigram, MIT), restricted to a curated set of plausible subtitle languages
|
|
21
|
-
(distinguishes ru/uk/bg/sr and Latin languages; avoids short-text false
|
|
22
|
-
positives like English→Scots), and reports it in `X-Subtitle-Language`
|
|
23
|
-
(+ `X-Subtitle-Language-Name`). Embedded tracks detect from the first chunk
|
|
24
|
-
of extracted VTT.
|
|
25
|
-
- The browser sets each track's language by priority: explicit code in the
|
|
26
|
-
filename / container metadata (author intent) → proxy content detection →
|
|
27
|
-
the film's audio-track language (forced-signs subs usually match the dub)
|
|
28
|
-
→ Unknown.
|
|
29
|
-
|
|
30
|
-
## Capabilities
|
|
31
|
-
|
|
32
|
-
### New Capabilities
|
|
33
|
-
|
|
34
|
-
- `subtitle-language`: proxy-side subtitle conversion, encoding handling and
|
|
35
|
-
content-based language detection.
|
|
36
|
-
|
|
37
|
-
### Modified Capabilities
|
|
38
|
-
|
|
39
|
-
<!-- track-selection covered embedded extraction; this extends /api/subtitles
|
|
40
|
-
to external files and adds detection. Its change is unarchived, so this
|
|
41
|
-
lands as a new capability rather than a delta. -->
|
|
42
|
-
|
|
43
|
-
## Impact
|
|
44
|
-
|
|
45
|
-
- proxy: new `services/subtitle-convert.js`, `services/language-detect.js`
|
|
46
|
-
(franc dep), extended `routes/api/subtitles/get.js`.
|
|
47
|
-
- server: `components/loading/loading.js` fetches VTT from the proxy for
|
|
48
|
-
external subs (drops client-side conversion) and applies the language
|
|
49
|
-
priority; browser franc/alphabet detection removed.
|
|
50
|
-
- Pairs with a server release; requires the ha-addon bump.
|