@torrent-tv/proxy 2.87.10 → 2.87.11
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 +2186 -2181
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +124 -101
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +50 -50
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +136 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -2162
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +416 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1110 -1083
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +254 -254
- package/services/encode/quality/output-format.js +266 -266
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +131 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1146 -1146
- package/services/server/ViewerRequests.js +703 -522
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +620 -0
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +989 -983
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- 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/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- 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 +675 -657
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-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/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +204 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +381 -381
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +323 -0
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1229 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +102 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +182 -182
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +407 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,41 +1,41 @@
|
|
|
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
|
+
# 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 +1,33 @@
|
|
|
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
|
+
# 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,2 +1,2 @@
|
|
|
1
|
-
schema: spec-driven
|
|
2
|
-
created: 2026-07-07
|
|
1
|
+
schema: spec-driven
|
|
2
|
+
created: 2026-07-07
|
|
@@ -1,50 +1,50 @@
|
|
|
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.
|
|
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.
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
# subtitle-language — delta spec
|
|
2
|
-
|
|
3
|
-
## ADDED Requirements
|
|
4
|
-
|
|
5
|
-
### Requirement: Proxy converts subtitles and reports the language
|
|
6
|
-
`GET /api/subtitles` SHALL serve every subtitle as WebVTT and report the
|
|
7
|
-
detected language in `X-Subtitle-Language` (ISO 639-1) and
|
|
8
|
-
`X-Subtitle-Language-Name`. It SHALL handle two modes: an embedded track
|
|
9
|
-
(`trackIndex` given — extracted via ffmpeg) and an external subtitle file (no
|
|
10
|
-
`trackIndex` — the file is read, its encoding decoded, and `.srt`/`.ass`/
|
|
11
|
-
`.ssa` converted to WebVTT on the proxy). A leading BOM SHALL be stripped and
|
|
12
|
-
Windows-1251 bytes decoded when the file is not valid UTF-8.
|
|
13
|
-
|
|
14
|
-
#### Scenario: External Russian .srt without a filename code
|
|
15
|
-
- **WHEN** an external `.srt` whose name has no language code is requested
|
|
16
|
-
- **THEN** the response is WebVTT and `X-Subtitle-Language` is `ru`
|
|
17
|
-
|
|
18
|
-
#### Scenario: Ukrainian is not reported as Russian
|
|
19
|
-
- **WHEN** the subtitle text is Ukrainian
|
|
20
|
-
- **THEN** the detected language is `uk`, not `ru`
|
|
21
|
-
|
|
22
|
-
#### Scenario: Unsupported format
|
|
23
|
-
- **WHEN** an image-based or unconvertible subtitle is requested
|
|
24
|
-
- **THEN** the proxy responds 422
|
|
25
|
-
|
|
26
|
-
### Requirement: Detection is confidence-gated
|
|
27
|
-
Language detection SHALL be restricted to a curated set of plausible subtitle
|
|
28
|
-
languages and SHALL return no language (omit the header) when the text is too
|
|
29
|
-
short or undetermined, rather than emitting a wrong guess.
|
|
30
|
-
|
|
31
|
-
#### Scenario: Too little text
|
|
32
|
-
- **WHEN** the subtitle has only a few characters
|
|
33
|
-
- **THEN** no language header is set (the browser falls back to filename or
|
|
34
|
-
audio language)
|
|
1
|
+
# subtitle-language — delta spec
|
|
2
|
+
|
|
3
|
+
## ADDED Requirements
|
|
4
|
+
|
|
5
|
+
### Requirement: Proxy converts subtitles and reports the language
|
|
6
|
+
`GET /api/subtitles` SHALL serve every subtitle as WebVTT and report the
|
|
7
|
+
detected language in `X-Subtitle-Language` (ISO 639-1) and
|
|
8
|
+
`X-Subtitle-Language-Name`. It SHALL handle two modes: an embedded track
|
|
9
|
+
(`trackIndex` given — extracted via ffmpeg) and an external subtitle file (no
|
|
10
|
+
`trackIndex` — the file is read, its encoding decoded, and `.srt`/`.ass`/
|
|
11
|
+
`.ssa` converted to WebVTT on the proxy). A leading BOM SHALL be stripped and
|
|
12
|
+
Windows-1251 bytes decoded when the file is not valid UTF-8.
|
|
13
|
+
|
|
14
|
+
#### Scenario: External Russian .srt without a filename code
|
|
15
|
+
- **WHEN** an external `.srt` whose name has no language code is requested
|
|
16
|
+
- **THEN** the response is WebVTT and `X-Subtitle-Language` is `ru`
|
|
17
|
+
|
|
18
|
+
#### Scenario: Ukrainian is not reported as Russian
|
|
19
|
+
- **WHEN** the subtitle text is Ukrainian
|
|
20
|
+
- **THEN** the detected language is `uk`, not `ru`
|
|
21
|
+
|
|
22
|
+
#### Scenario: Unsupported format
|
|
23
|
+
- **WHEN** an image-based or unconvertible subtitle is requested
|
|
24
|
+
- **THEN** the proxy responds 422
|
|
25
|
+
|
|
26
|
+
### Requirement: Detection is confidence-gated
|
|
27
|
+
Language detection SHALL be restricted to a curated set of plausible subtitle
|
|
28
|
+
languages and SHALL return no language (omit the header) when the text is too
|
|
29
|
+
short or undetermined, rather than emitting a wrong guess.
|
|
30
|
+
|
|
31
|
+
#### Scenario: Too little text
|
|
32
|
+
- **WHEN** the subtitle has only a few characters
|
|
33
|
+
- **THEN** no language header is set (the browser falls back to filename or
|
|
34
|
+
audio language)
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
# Tasks: Proxy subtitle conversion + language detection
|
|
2
|
-
|
|
3
|
-
## 1. Proxy
|
|
4
|
-
|
|
5
|
-
- [x] 1.1 `services/subtitle-convert.js`: encoding-aware decode (UTF-8/BOM/
|
|
6
|
-
Windows-1251) + srt/ass/ssa → WebVTT (ported, BOM-stripped)
|
|
7
|
-
- [x] 1.2 `services/language-detect.js`: franc restricted to a curated
|
|
8
|
-
ISO 639-3→639-1 allowlist; null when undetermined/too short
|
|
9
|
-
- [x] 1.3 `routes/api/subtitles/get.js`: external-file mode (read, decode,
|
|
10
|
-
convert, detect) + `X-Subtitle-Language(-Name)` on both modes
|
|
11
|
-
- [x] 1.4 franc dependency added to proxy
|
|
12
|
-
- [x] 1.5 Verified: unit-tested convert+detect (ru/uk/en/de, short→null) and
|
|
13
|
-
the route end-to-end on the real Enola .srt (→ ru, 19 cues)
|
|
14
|
-
|
|
15
|
-
## 2. Server (browser)
|
|
16
|
-
|
|
17
|
-
- [x] 2.1 External subs fetch VTT from `/api/subtitles` (no client convert)
|
|
18
|
-
- [x] 2.2 Language priority: filename code → X-Subtitle-Language → audio-track
|
|
19
|
-
language → und; embedded track uses metadata → header → audio
|
|
20
|
-
- [x] 2.3 Removed the client-side convertSubtitleToVtt call + dead import
|
|
21
|
-
|
|
22
|
-
## 3. Release
|
|
23
|
-
|
|
24
|
-
- [ ] 3.1 Proxy publish (OTP) + ha-addon bump + server patch
|
|
25
|
-
- [ ] 3.2 Field-check: Enola external .srt labels "Russian"; a Ukrainian sub
|
|
26
|
-
labels "Ukrainian"; embedded tracks keep their metadata language
|
|
1
|
+
# Tasks: Proxy subtitle conversion + language detection
|
|
2
|
+
|
|
3
|
+
## 1. Proxy
|
|
4
|
+
|
|
5
|
+
- [x] 1.1 `services/subtitle-convert.js`: encoding-aware decode (UTF-8/BOM/
|
|
6
|
+
Windows-1251) + srt/ass/ssa → WebVTT (ported, BOM-stripped)
|
|
7
|
+
- [x] 1.2 `services/language-detect.js`: franc restricted to a curated
|
|
8
|
+
ISO 639-3→639-1 allowlist; null when undetermined/too short
|
|
9
|
+
- [x] 1.3 `routes/api/subtitles/get.js`: external-file mode (read, decode,
|
|
10
|
+
convert, detect) + `X-Subtitle-Language(-Name)` on both modes
|
|
11
|
+
- [x] 1.4 franc dependency added to proxy
|
|
12
|
+
- [x] 1.5 Verified: unit-tested convert+detect (ru/uk/en/de, short→null) and
|
|
13
|
+
the route end-to-end on the real Enola .srt (→ ru, 19 cues)
|
|
14
|
+
|
|
15
|
+
## 2. Server (browser)
|
|
16
|
+
|
|
17
|
+
- [x] 2.1 External subs fetch VTT from `/api/subtitles` (no client convert)
|
|
18
|
+
- [x] 2.2 Language priority: filename code → X-Subtitle-Language → audio-track
|
|
19
|
+
language → und; embedded track uses metadata → header → audio
|
|
20
|
+
- [x] 2.3 Removed the client-side convertSubtitleToVtt call + dead import
|
|
21
|
+
|
|
22
|
+
## 3. Release
|
|
23
|
+
|
|
24
|
+
- [ ] 3.1 Proxy publish (OTP) + ha-addon bump + server patch
|
|
25
|
+
- [ ] 3.2 Field-check: Enola external .srt labels "Russian"; a Ukrainian sub
|
|
26
|
+
labels "Ukrainian"; embedded tracks keep their metadata language
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
schema: spec-driven
|
|
2
|
-
created: 2026-07-07
|
|
1
|
+
schema: spec-driven
|
|
2
|
+
created: 2026-07-07
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
# Design: Track inventory, audio selection and embedded-subtitle extraction
|
|
2
|
-
|
|
3
|
-
## Context
|
|
4
|
-
|
|
5
|
-
The probe already captures the full ffmpeg `-i` banner; the HLS session
|
|
6
|
-
manager already restarts ffmpeg per (source, file, settings) key; the
|
|
7
|
-
`/stream` route already drives prioritised sequential download. All three
|
|
8
|
-
features ride those mechanisms.
|
|
9
|
-
|
|
10
|
-
## Goals / Non-Goals
|
|
11
|
-
|
|
12
|
-
**Goals:** expose every track; select audio server-side; deliver embedded
|
|
13
|
-
text subtitles as WebVTT.
|
|
14
|
-
|
|
15
|
-
**Non-Goals:** seamless (no-restart) audio switching via HLS alternate
|
|
16
|
-
renditions; image-based subtitles (PGS/VobSub — needs OCR or burn-in);
|
|
17
|
-
subtitle extraction that avoids downloading the file (impossible: cues are
|
|
18
|
-
interleaved across the whole container).
|
|
19
|
-
|
|
20
|
-
## Decisions
|
|
21
|
-
|
|
22
|
-
1. **Parse tracks from the existing probe output** (zero extra probe cost).
|
|
23
|
-
The scanner reads only the Input section — ffmpeg prints Stream lines for
|
|
24
|
-
the null output too, which would duplicate every track (caught against
|
|
25
|
-
real output). Titles come from each stream's `title` metadata line.
|
|
26
|
-
2. **Audio switch = new session.** `audioTrackIndex` joins the session key;
|
|
27
|
-
the old session dies via the existing idle TTL. Reuses the proven
|
|
28
|
-
seek-restart machinery instead of building HLS alternate renditions;
|
|
29
|
-
the cost is a few seconds' gap on switch — acceptable v1.
|
|
30
|
-
3. **Extraction as a streaming route.** ffmpeg writes WebVTT to stdout piped
|
|
31
|
-
into the HTTP response; the first stdout chunk decides 200-vs-422 (a
|
|
32
|
-
non-text track dies before producing output). Client disconnect kills
|
|
33
|
-
ffmpeg; a 30-minute hard cap guards dead swarms. The transport layer's
|
|
34
|
-
60 s request timeout must be raised per-request by the browser (done in
|
|
35
|
-
the paired server change).
|
|
36
|
-
4. **Accepted v1 cost:** extraction reads to the last cue → cold torrents
|
|
37
|
-
download while extracting. For the transcode path the file downloads
|
|
38
|
-
anyway; for direct play this is extra traffic the viewer opted into by
|
|
39
|
-
picking a subtitle.
|
|
40
|
-
|
|
41
|
-
## Risks / Trade-offs
|
|
42
|
-
|
|
43
|
-
- [Extraction competes with playback for piece priority] → both readers move
|
|
44
|
-
the 8 MB critical window; sequential download serves both. Field-watch; if
|
|
45
|
-
playback stalls appear, throttle extraction reads later.
|
|
46
|
-
- [Stream-line format drift across ffmpeg versions] → regex kept permissive;
|
|
47
|
-
a parse miss degrades to an empty inventory (menus simply do not appear).
|
|
1
|
+
# Design: Track inventory, audio selection and embedded-subtitle extraction
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
The probe already captures the full ffmpeg `-i` banner; the HLS session
|
|
6
|
+
manager already restarts ffmpeg per (source, file, settings) key; the
|
|
7
|
+
`/stream` route already drives prioritised sequential download. All three
|
|
8
|
+
features ride those mechanisms.
|
|
9
|
+
|
|
10
|
+
## Goals / Non-Goals
|
|
11
|
+
|
|
12
|
+
**Goals:** expose every track; select audio server-side; deliver embedded
|
|
13
|
+
text subtitles as WebVTT.
|
|
14
|
+
|
|
15
|
+
**Non-Goals:** seamless (no-restart) audio switching via HLS alternate
|
|
16
|
+
renditions; image-based subtitles (PGS/VobSub — needs OCR or burn-in);
|
|
17
|
+
subtitle extraction that avoids downloading the file (impossible: cues are
|
|
18
|
+
interleaved across the whole container).
|
|
19
|
+
|
|
20
|
+
## Decisions
|
|
21
|
+
|
|
22
|
+
1. **Parse tracks from the existing probe output** (zero extra probe cost).
|
|
23
|
+
The scanner reads only the Input section — ffmpeg prints Stream lines for
|
|
24
|
+
the null output too, which would duplicate every track (caught against
|
|
25
|
+
real output). Titles come from each stream's `title` metadata line.
|
|
26
|
+
2. **Audio switch = new session.** `audioTrackIndex` joins the session key;
|
|
27
|
+
the old session dies via the existing idle TTL. Reuses the proven
|
|
28
|
+
seek-restart machinery instead of building HLS alternate renditions;
|
|
29
|
+
the cost is a few seconds' gap on switch — acceptable v1.
|
|
30
|
+
3. **Extraction as a streaming route.** ffmpeg writes WebVTT to stdout piped
|
|
31
|
+
into the HTTP response; the first stdout chunk decides 200-vs-422 (a
|
|
32
|
+
non-text track dies before producing output). Client disconnect kills
|
|
33
|
+
ffmpeg; a 30-minute hard cap guards dead swarms. The transport layer's
|
|
34
|
+
60 s request timeout must be raised per-request by the browser (done in
|
|
35
|
+
the paired server change).
|
|
36
|
+
4. **Accepted v1 cost:** extraction reads to the last cue → cold torrents
|
|
37
|
+
download while extracting. For the transcode path the file downloads
|
|
38
|
+
anyway; for direct play this is extra traffic the viewer opted into by
|
|
39
|
+
picking a subtitle.
|
|
40
|
+
|
|
41
|
+
## Risks / Trade-offs
|
|
42
|
+
|
|
43
|
+
- [Extraction competes with playback for piece priority] → both readers move
|
|
44
|
+
the 8 MB critical window; sequential download serves both. Field-watch; if
|
|
45
|
+
playback stalls appear, throttle extraction reads later.
|
|
46
|
+
- [Stream-line format drift across ffmpeg versions] → regex kept permissive;
|
|
47
|
+
a parse miss degrades to an empty inventory (menus simply do not appear).
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
# Proposal: Track inventory, audio selection and embedded-subtitle extraction
|
|
2
|
-
|
|
3
|
-
## Why
|
|
4
|
-
|
|
5
|
-
Torrents routinely carry several audio languages and embedded subtitles
|
|
6
|
-
(the owner's own test MKVs embed ASS subtitles), but the proxy exposed only
|
|
7
|
-
"the first audio track" and no subtitles at all: the probe reported a single
|
|
8
|
-
audio/video codec pair, the HLS session hard-mapped `0:a:0`, and embedded
|
|
9
|
-
subtitle streams were unreachable by the browser.
|
|
10
|
-
|
|
11
|
-
## What Changes
|
|
12
|
-
|
|
13
|
-
- **Probe returns the full track inventory**: `audioTracks` and
|
|
14
|
-
`subtitleTracks` in the playback plan (type-relative index, codec,
|
|
15
|
-
language, `title` metadata, default flag, `textBased` for subtitles) —
|
|
16
|
-
parsed from the same single ffmpeg banner, no extra probe cost.
|
|
17
|
-
- **Audio selection**: `POST /api/transcode-sessions` accepts
|
|
18
|
-
`audioTrackIndex`; the ffmpeg map becomes `0:a:N` and the index joins the
|
|
19
|
-
session key (switch = fresh session via the existing restart machinery).
|
|
20
|
-
- **Embedded subtitle extraction**: `GET /api/subtitles` streams a chosen
|
|
21
|
-
text subtitle track as WebVTT (ffmpeg `-map 0:s:N -f webvtt`). Image-based
|
|
22
|
-
tracks (PGS/VobSub) are refused with 422. Known cost, accepted for v1:
|
|
23
|
-
extraction reads the file to the last cue, so a cold torrent downloads
|
|
24
|
-
sequentially while extracting.
|
|
25
|
-
- Announce log masks the tracker query string (passkey).
|
|
26
|
-
|
|
27
|
-
## Capabilities
|
|
28
|
-
|
|
29
|
-
### New Capabilities
|
|
30
|
-
|
|
31
|
-
- `track-selection`: track inventory in the plan, audio mapping, subtitle
|
|
32
|
-
extraction.
|
|
33
|
-
|
|
34
|
-
### Modified Capabilities
|
|
35
|
-
|
|
36
|
-
- `observability`: announce log masks the passkey (delta note; the change is
|
|
37
|
-
still unarchived so the edit lands there).
|
|
38
|
-
|
|
39
|
-
## Impact
|
|
40
|
-
|
|
41
|
-
- `services/playback-planner.js`, `services/hls-session-manager.js`,
|
|
42
|
-
`routes/api/transcode-sessions/post.js`, new `routes/api/subtitles/get.js`,
|
|
43
|
-
`server.js` wiring, `services/torrent/torrent-pool.js` (log masking).
|
|
44
|
-
- Pairs with the server-side `track-selection-ui` change; requires the usual
|
|
45
|
-
ha-addon bump.
|
|
1
|
+
# Proposal: Track inventory, audio selection and embedded-subtitle extraction
|
|
2
|
+
|
|
3
|
+
## Why
|
|
4
|
+
|
|
5
|
+
Torrents routinely carry several audio languages and embedded subtitles
|
|
6
|
+
(the owner's own test MKVs embed ASS subtitles), but the proxy exposed only
|
|
7
|
+
"the first audio track" and no subtitles at all: the probe reported a single
|
|
8
|
+
audio/video codec pair, the HLS session hard-mapped `0:a:0`, and embedded
|
|
9
|
+
subtitle streams were unreachable by the browser.
|
|
10
|
+
|
|
11
|
+
## What Changes
|
|
12
|
+
|
|
13
|
+
- **Probe returns the full track inventory**: `audioTracks` and
|
|
14
|
+
`subtitleTracks` in the playback plan (type-relative index, codec,
|
|
15
|
+
language, `title` metadata, default flag, `textBased` for subtitles) —
|
|
16
|
+
parsed from the same single ffmpeg banner, no extra probe cost.
|
|
17
|
+
- **Audio selection**: `POST /api/transcode-sessions` accepts
|
|
18
|
+
`audioTrackIndex`; the ffmpeg map becomes `0:a:N` and the index joins the
|
|
19
|
+
session key (switch = fresh session via the existing restart machinery).
|
|
20
|
+
- **Embedded subtitle extraction**: `GET /api/subtitles` streams a chosen
|
|
21
|
+
text subtitle track as WebVTT (ffmpeg `-map 0:s:N -f webvtt`). Image-based
|
|
22
|
+
tracks (PGS/VobSub) are refused with 422. Known cost, accepted for v1:
|
|
23
|
+
extraction reads the file to the last cue, so a cold torrent downloads
|
|
24
|
+
sequentially while extracting.
|
|
25
|
+
- Announce log masks the tracker query string (passkey).
|
|
26
|
+
|
|
27
|
+
## Capabilities
|
|
28
|
+
|
|
29
|
+
### New Capabilities
|
|
30
|
+
|
|
31
|
+
- `track-selection`: track inventory in the plan, audio mapping, subtitle
|
|
32
|
+
extraction.
|
|
33
|
+
|
|
34
|
+
### Modified Capabilities
|
|
35
|
+
|
|
36
|
+
- `observability`: announce log masks the passkey (delta note; the change is
|
|
37
|
+
still unarchived so the edit lands there).
|
|
38
|
+
|
|
39
|
+
## Impact
|
|
40
|
+
|
|
41
|
+
- `services/playback-planner.js`, `services/hls-session-manager.js`,
|
|
42
|
+
`routes/api/transcode-sessions/post.js`, new `routes/api/subtitles/get.js`,
|
|
43
|
+
`server.js` wiring, `services/torrent/torrent-pool.js` (log masking).
|
|
44
|
+
- Pairs with the server-side `track-selection-ui` change; requires the usual
|
|
45
|
+
ha-addon bump.
|