@torrent-tv/proxy 2.89.5 → 2.89.7
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 +338 -338
- package/CHANGELOG.md +16 -0
- 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/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- 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/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/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 +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- 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/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/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- 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/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/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/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- 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-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -753
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +475 -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 +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- 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 +68 -68
- 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/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- 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 +240 -240
- package/test/helpers/manager.js +62 -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/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 +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- 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-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- 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 +87 -87
- 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-completeness.test.js +37 -1
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- 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 +160 -160
- package/test/quality-variants.test.js +1270 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +64 -64
- package/test/same-height-switch.test.js +663 -663
- 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-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 +222 -221
- package/test/segment-serve-wiring.test.js +9 -3
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- 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 +111 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- 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/supply-margin.test.js +190 -190
- 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 +508 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +92 -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,195 +1,195 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The segment the viewer just seeked TO must not be refused as stale.
|
|
3
|
-
*
|
|
4
|
-
* A seek bumps the wait epoch so that requests made for the position being left
|
|
5
|
-
* stop being held. The request for the position being ARRIVED at races that
|
|
6
|
-
* bump: hls.js asks for the new segment within milliseconds, and if the epoch
|
|
7
|
-
* moves underneath it, the epoch check alone cannot tell the two apart.
|
|
8
|
-
*
|
|
9
|
-
* Measured 2026-08-18: the viewer seeked to 1061.0 s, `segment-00101` — the
|
|
10
|
-
* segment at that very position — was answered 503 twice within 80 ms, the
|
|
11
|
-
* player never asked for it again, and instead looped `a/0/segment-00103` and
|
|
12
|
-
* `a/0/segment-00104` 737 and 736 times over 149 seconds while the picture
|
|
13
|
-
* stood at `t=1061.0s readyState=1`. The session ended without another frame.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import test from "node:test";
|
|
17
|
-
import assert from "node:assert/strict";
|
|
18
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
19
|
-
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
20
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
21
|
-
import os from "node:os";
|
|
22
|
-
import path from "node:path";
|
|
23
|
-
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
24
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
25
|
-
|
|
26
|
-
const SESSION_ID = "aaaaaaaa11112222";
|
|
27
|
-
const SEGMENT_SECONDS = 10.4;
|
|
28
|
-
const SEEK_TO_SECONDS = 1061;
|
|
29
|
-
const SEGMENT_AT_SEEK = Math.floor(SEEK_TO_SECONDS / SEGMENT_SECONDS);
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A session whose viewer has just landed at {@link SEEK_TO_SECONDS}.
|
|
33
|
-
*
|
|
34
|
-
* @returns {Promise<{ manager: object, dirPath: string }>}
|
|
35
|
-
*/
|
|
36
|
-
async function managerAfterSeek() {
|
|
37
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "seek-target-"));
|
|
38
|
-
const manager = wireOutputs({
|
|
39
|
-
enabled: true,
|
|
40
|
-
ffmpegBin: "ffmpeg",
|
|
41
|
-
localBindHost: "127.0.0.1",
|
|
42
|
-
localPort: 9090
|
|
43
|
-
});
|
|
44
|
-
const session = {
|
|
45
|
-
id: SESSION_ID,
|
|
46
|
-
dirPath,
|
|
47
|
-
// Where this file is cut, held by the file. A fixture that stated it
|
|
48
|
-
// on the session was describing what production no longer does.
|
|
49
|
-
timeline: new Timeline({
|
|
50
|
-
boundaries: Array.from({ length: 400 }, (_, index) => index * SEGMENT_SECONDS),
|
|
51
|
-
cutGrid: "uniform"
|
|
52
|
-
}),
|
|
53
|
-
state: "ready",
|
|
54
|
-
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
|
|
55
|
-
// An ordinary session reads its own file, and its sound is inside it. The
|
|
56
|
-
// three differ only for a soundtrack shipped as a file of its own.
|
|
57
|
-
get inputFile() { return this.file; },
|
|
58
|
-
get audioFile() { return this.file; },
|
|
59
|
-
startedAt: Date.now(),
|
|
60
|
-
lastAccessedAt: Date.now(),
|
|
61
|
-
consumers: new Set(),
|
|
62
|
-
segmentFormat: fmp4Format,
|
|
63
|
-
transcodeVideo: false,
|
|
64
|
-
useSyntheticPlaylist: true,
|
|
65
|
-
segmentCount: 399,
|
|
66
|
-
encodeStartIndex: SEGMENT_AT_SEEK,
|
|
67
|
-
waitEpoch: 1,
|
|
68
|
-
runState: "PRODUCING"
|
|
69
|
-
};
|
|
70
|
-
manager.outputs.set(SESSION_ID, session);
|
|
71
|
-
manager.viewers.of(session, "viewer").moveTo(SEEK_TO_SECONDS);
|
|
72
|
-
manager.viewers.of(session, "viewer").playing = false;
|
|
73
|
-
return { manager, dirPath };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
test("the segment at the seek target is still wanted", async () => {
|
|
77
|
-
const { manager, dirPath } = await managerAfterSeek();
|
|
78
|
-
|
|
79
|
-
assert.equal(
|
|
80
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(SEGMENT_AT_SEEK).padStart(5, "0")}.mp4`),
|
|
81
|
-
true,
|
|
82
|
-
"this is the segment the viewer is waiting for; refusing it is what froze the field session"
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("a segment behind the viewer is not wanted any more", async () => {
|
|
89
|
-
const { manager, dirPath } = await managerAfterSeek();
|
|
90
|
-
|
|
91
|
-
assert.equal(
|
|
92
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(SEGMENT_AT_SEEK - 3).padStart(5, "0")}.mp4`),
|
|
93
|
-
false,
|
|
94
|
-
"a request for the position the viewer left is exactly what the epoch exists to release"
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("a segment far beyond what the run will reach is not wanted", async () => {
|
|
101
|
-
const { manager, dirPath } = await managerAfterSeek();
|
|
102
|
-
|
|
103
|
-
assert.equal(
|
|
104
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(SEGMENT_AT_SEEK + 200).padStart(5, "0")}.mp4`),
|
|
105
|
-
false,
|
|
106
|
-
"nothing will produce it before the viewer moves again"
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("a playlist belongs to no position and is never stale", async () => {
|
|
113
|
-
const { manager, dirPath } = await managerAfterSeek();
|
|
114
|
-
|
|
115
|
-
assert.equal(manager.serving.requestStillWanted(SESSION_ID, "index.m3u8"), true);
|
|
116
|
-
assert.equal(manager.serving.requestStillWanted(SESSION_ID, "init.mp4"), true);
|
|
117
|
-
|
|
118
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("a request as deep as the cushion the browser is told to hold is still wanted", async () => {
|
|
122
|
-
const { manager, dirPath } = await managerAfterSeek();
|
|
123
|
-
|
|
124
|
-
// The browser sizes its forward buffer from the proxy's own look-ahead, so
|
|
125
|
-
// the deepest request it can make is one for the segment at the far edge of
|
|
126
|
-
// that cushion. Refusing it would be refusing the very depth this proxy
|
|
127
|
-
// asked for: at the old width — eight segments ahead of the viewer — three
|
|
128
|
-
// quarters of a full cushion's requests were stale by definition.
|
|
129
|
-
const edge = Math.floor((SEEK_TO_SECONDS + manager.lookaheadSeconds) / SEGMENT_SECONDS);
|
|
130
|
-
|
|
131
|
-
assert.ok(edge - SEGMENT_AT_SEEK > 8, "the case only exists past the old eight-segment width");
|
|
132
|
-
assert.equal(
|
|
133
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(edge).padStart(5, "0")}.mp4`),
|
|
134
|
-
true,
|
|
135
|
-
"the far edge of the cushion the proxy itself keeps produced"
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("the viewer who made the request is the one it is judged against", async () => {
|
|
142
|
-
const { manager, dirPath } = await managerAfterSeek();
|
|
143
|
-
const session = manager.outputs.get(SESSION_ID);
|
|
144
|
-
session.viewers = new Map();
|
|
145
|
-
// Two people watching one copied picture: one at the seek target, one a
|
|
146
|
-
// hundred segments back and waiting for a segment there. Asked about nobody
|
|
147
|
-
// in particular the answer is the FURTHEST of them, because what lies behind
|
|
148
|
-
// that one has already been made.
|
|
149
|
-
const behind = SEGMENT_AT_SEEK - 100;
|
|
150
|
-
manager.viewers.of(session, "behind").moveTo(behind * SEGMENT_SECONDS);
|
|
151
|
-
manager.viewers.of(session, "behind").playing = false;
|
|
152
|
-
manager.viewers.of(session, "ahead").moveTo(SEEK_TO_SECONDS);
|
|
153
|
-
manager.viewers.of(session, "ahead").playing = false;
|
|
154
|
-
|
|
155
|
-
assert.equal(
|
|
156
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(behind).padStart(5, "0")}.mp4`, "behind"),
|
|
157
|
-
true,
|
|
158
|
-
"held for the viewer who is there, whatever the viewer in front is doing"
|
|
159
|
-
);
|
|
160
|
-
assert.equal(
|
|
161
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(behind).padStart(5, "0")}.mp4`),
|
|
162
|
-
false,
|
|
163
|
-
"unnamed, it is judged against the furthest viewer, which is the leader"
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("a seek moves the seeking viewer's own head, and nobody else's", async () => {
|
|
170
|
-
const { manager, dirPath } = await managerAfterSeek();
|
|
171
|
-
const session = manager.outputs.get(SESSION_ID);
|
|
172
|
-
session.viewers = new Map();
|
|
173
|
-
const staying = SEGMENT_AT_SEEK - 40;
|
|
174
|
-
manager.viewers.of(session, "staying").position = {
|
|
175
|
-
segment: staying,
|
|
176
|
-
seconds: staying * SEGMENT_SECONDS,
|
|
177
|
-
at: Date.now()
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const jumpTo = 120;
|
|
181
|
-
manager.viewerRequests.requestSeek(SESSION_ID, jumpTo, "jumping");
|
|
182
|
-
|
|
183
|
-
assert.equal(
|
|
184
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(Math.floor(jumpTo / SEGMENT_SECONDS)).padStart(5, "0")}.mp4`, "jumping"),
|
|
185
|
-
true,
|
|
186
|
-
"the segment at the seek target — the request that raced the epoch in 2026-08-18"
|
|
187
|
-
);
|
|
188
|
-
assert.equal(
|
|
189
|
-
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(staying).padStart(5, "0")}.mp4`, "staying"),
|
|
190
|
-
true,
|
|
191
|
-
"somebody else's seek does not move where this viewer is"
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
195
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The segment the viewer just seeked TO must not be refused as stale.
|
|
3
|
+
*
|
|
4
|
+
* A seek bumps the wait epoch so that requests made for the position being left
|
|
5
|
+
* stop being held. The request for the position being ARRIVED at races that
|
|
6
|
+
* bump: hls.js asks for the new segment within milliseconds, and if the epoch
|
|
7
|
+
* moves underneath it, the epoch check alone cannot tell the two apart.
|
|
8
|
+
*
|
|
9
|
+
* Measured 2026-08-18: the viewer seeked to 1061.0 s, `segment-00101` — the
|
|
10
|
+
* segment at that very position — was answered 503 twice within 80 ms, the
|
|
11
|
+
* player never asked for it again, and instead looped `a/0/segment-00103` and
|
|
12
|
+
* `a/0/segment-00104` 737 and 736 times over 149 seconds while the picture
|
|
13
|
+
* stood at `t=1061.0s readyState=1`. The session ended without another frame.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
19
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
20
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
24
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
25
|
+
|
|
26
|
+
const SESSION_ID = "aaaaaaaa11112222";
|
|
27
|
+
const SEGMENT_SECONDS = 10.4;
|
|
28
|
+
const SEEK_TO_SECONDS = 1061;
|
|
29
|
+
const SEGMENT_AT_SEEK = Math.floor(SEEK_TO_SECONDS / SEGMENT_SECONDS);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A session whose viewer has just landed at {@link SEEK_TO_SECONDS}.
|
|
33
|
+
*
|
|
34
|
+
* @returns {Promise<{ manager: object, dirPath: string }>}
|
|
35
|
+
*/
|
|
36
|
+
async function managerAfterSeek() {
|
|
37
|
+
const dirPath = await mkdtemp(path.join(os.tmpdir(), "seek-target-"));
|
|
38
|
+
const manager = wireOutputs({
|
|
39
|
+
enabled: true,
|
|
40
|
+
ffmpegBin: "ffmpeg",
|
|
41
|
+
localBindHost: "127.0.0.1",
|
|
42
|
+
localPort: 9090
|
|
43
|
+
});
|
|
44
|
+
const session = {
|
|
45
|
+
id: SESSION_ID,
|
|
46
|
+
dirPath,
|
|
47
|
+
// Where this file is cut, held by the file. A fixture that stated it
|
|
48
|
+
// on the session was describing what production no longer does.
|
|
49
|
+
timeline: new Timeline({
|
|
50
|
+
boundaries: Array.from({ length: 400 }, (_, index) => index * SEGMENT_SECONDS),
|
|
51
|
+
cutGrid: "uniform"
|
|
52
|
+
}),
|
|
53
|
+
state: "ready",
|
|
54
|
+
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
|
|
55
|
+
// An ordinary session reads its own file, and its sound is inside it. The
|
|
56
|
+
// three differ only for a soundtrack shipped as a file of its own.
|
|
57
|
+
get inputFile() { return this.file; },
|
|
58
|
+
get audioFile() { return this.file; },
|
|
59
|
+
startedAt: Date.now(),
|
|
60
|
+
lastAccessedAt: Date.now(),
|
|
61
|
+
consumers: new Set(),
|
|
62
|
+
segmentFormat: fmp4Format,
|
|
63
|
+
transcodeVideo: false,
|
|
64
|
+
useSyntheticPlaylist: true,
|
|
65
|
+
segmentCount: 399,
|
|
66
|
+
encodeStartIndex: SEGMENT_AT_SEEK,
|
|
67
|
+
waitEpoch: 1,
|
|
68
|
+
runState: "PRODUCING"
|
|
69
|
+
};
|
|
70
|
+
manager.outputs.set(SESSION_ID, session);
|
|
71
|
+
manager.viewers.of(session, "viewer").moveTo(SEEK_TO_SECONDS);
|
|
72
|
+
manager.viewers.of(session, "viewer").playing = false;
|
|
73
|
+
return { manager, dirPath };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test("the segment at the seek target is still wanted", async () => {
|
|
77
|
+
const { manager, dirPath } = await managerAfterSeek();
|
|
78
|
+
|
|
79
|
+
assert.equal(
|
|
80
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(SEGMENT_AT_SEEK).padStart(5, "0")}.mp4`),
|
|
81
|
+
true,
|
|
82
|
+
"this is the segment the viewer is waiting for; refusing it is what froze the field session"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a segment behind the viewer is not wanted any more", async () => {
|
|
89
|
+
const { manager, dirPath } = await managerAfterSeek();
|
|
90
|
+
|
|
91
|
+
assert.equal(
|
|
92
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(SEGMENT_AT_SEEK - 3).padStart(5, "0")}.mp4`),
|
|
93
|
+
false,
|
|
94
|
+
"a request for the position the viewer left is exactly what the epoch exists to release"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a segment far beyond what the run will reach is not wanted", async () => {
|
|
101
|
+
const { manager, dirPath } = await managerAfterSeek();
|
|
102
|
+
|
|
103
|
+
assert.equal(
|
|
104
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(SEGMENT_AT_SEEK + 200).padStart(5, "0")}.mp4`),
|
|
105
|
+
false,
|
|
106
|
+
"nothing will produce it before the viewer moves again"
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a playlist belongs to no position and is never stale", async () => {
|
|
113
|
+
const { manager, dirPath } = await managerAfterSeek();
|
|
114
|
+
|
|
115
|
+
assert.equal(manager.serving.requestStillWanted(SESSION_ID, "index.m3u8"), true);
|
|
116
|
+
assert.equal(manager.serving.requestStillWanted(SESSION_ID, "init.mp4"), true);
|
|
117
|
+
|
|
118
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a request as deep as the cushion the browser is told to hold is still wanted", async () => {
|
|
122
|
+
const { manager, dirPath } = await managerAfterSeek();
|
|
123
|
+
|
|
124
|
+
// The browser sizes its forward buffer from the proxy's own look-ahead, so
|
|
125
|
+
// the deepest request it can make is one for the segment at the far edge of
|
|
126
|
+
// that cushion. Refusing it would be refusing the very depth this proxy
|
|
127
|
+
// asked for: at the old width — eight segments ahead of the viewer — three
|
|
128
|
+
// quarters of a full cushion's requests were stale by definition.
|
|
129
|
+
const edge = Math.floor((SEEK_TO_SECONDS + manager.lookaheadSeconds) / SEGMENT_SECONDS);
|
|
130
|
+
|
|
131
|
+
assert.ok(edge - SEGMENT_AT_SEEK > 8, "the case only exists past the old eight-segment width");
|
|
132
|
+
assert.equal(
|
|
133
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(edge).padStart(5, "0")}.mp4`),
|
|
134
|
+
true,
|
|
135
|
+
"the far edge of the cushion the proxy itself keeps produced"
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the viewer who made the request is the one it is judged against", async () => {
|
|
142
|
+
const { manager, dirPath } = await managerAfterSeek();
|
|
143
|
+
const session = manager.outputs.get(SESSION_ID);
|
|
144
|
+
session.viewers = new Map();
|
|
145
|
+
// Two people watching one copied picture: one at the seek target, one a
|
|
146
|
+
// hundred segments back and waiting for a segment there. Asked about nobody
|
|
147
|
+
// in particular the answer is the FURTHEST of them, because what lies behind
|
|
148
|
+
// that one has already been made.
|
|
149
|
+
const behind = SEGMENT_AT_SEEK - 100;
|
|
150
|
+
manager.viewers.of(session, "behind").moveTo(behind * SEGMENT_SECONDS);
|
|
151
|
+
manager.viewers.of(session, "behind").playing = false;
|
|
152
|
+
manager.viewers.of(session, "ahead").moveTo(SEEK_TO_SECONDS);
|
|
153
|
+
manager.viewers.of(session, "ahead").playing = false;
|
|
154
|
+
|
|
155
|
+
assert.equal(
|
|
156
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(behind).padStart(5, "0")}.mp4`, "behind"),
|
|
157
|
+
true,
|
|
158
|
+
"held for the viewer who is there, whatever the viewer in front is doing"
|
|
159
|
+
);
|
|
160
|
+
assert.equal(
|
|
161
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(behind).padStart(5, "0")}.mp4`),
|
|
162
|
+
false,
|
|
163
|
+
"unnamed, it is judged against the furthest viewer, which is the leader"
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a seek moves the seeking viewer's own head, and nobody else's", async () => {
|
|
170
|
+
const { manager, dirPath } = await managerAfterSeek();
|
|
171
|
+
const session = manager.outputs.get(SESSION_ID);
|
|
172
|
+
session.viewers = new Map();
|
|
173
|
+
const staying = SEGMENT_AT_SEEK - 40;
|
|
174
|
+
manager.viewers.of(session, "staying").position = {
|
|
175
|
+
segment: staying,
|
|
176
|
+
seconds: staying * SEGMENT_SECONDS,
|
|
177
|
+
at: Date.now()
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const jumpTo = 120;
|
|
181
|
+
manager.viewerRequests.requestSeek(SESSION_ID, jumpTo, "jumping");
|
|
182
|
+
|
|
183
|
+
assert.equal(
|
|
184
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(Math.floor(jumpTo / SEGMENT_SECONDS)).padStart(5, "0")}.mp4`, "jumping"),
|
|
185
|
+
true,
|
|
186
|
+
"the segment at the seek target — the request that raced the epoch in 2026-08-18"
|
|
187
|
+
);
|
|
188
|
+
assert.equal(
|
|
189
|
+
manager.serving.requestStillWanted(SESSION_ID, `segment-${String(staying).padStart(5, "0")}.mp4`, "staying"),
|
|
190
|
+
true,
|
|
191
|
+
"somebody else's seek does not move where this viewer is"
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
195
|
+
});
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The playlist and the real segments must describe the same thing.
|
|
3
|
-
*
|
|
4
|
-
* They did not. The playlist was built from the container index while ffmpeg
|
|
5
|
-
* chose its own cut points from a target duration, and the two only ever agreed
|
|
6
|
-
* by luck — on a field file the index listed 1902 keyframes, ffmpeg found about
|
|
7
|
-
* twice as many, and segment #876 meant 1:26:50 to the player and roughly
|
|
8
|
-
* minute 58 to ffmpeg. Seeks landed nowhere near where they were aimed.
|
|
9
|
-
*
|
|
10
|
-
* These tests pin the arithmetic that ties the two together.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { segmentCutTimesFrom } from "../services/encode/run-command.js";
|
|
16
|
-
|
|
17
|
-
test("cut times are the interior boundaries, rebased on the run start", () => {
|
|
18
|
-
// Segments 0-10, 10-20, 20-30, 30-40 (file ends at 40).
|
|
19
|
-
const boundaries = [0, 10, 20, 30, 40];
|
|
20
|
-
|
|
21
|
-
assert.deepEqual(
|
|
22
|
-
segmentCutTimesFrom(boundaries, 0),
|
|
23
|
-
[10, 20, 30],
|
|
24
|
-
"from the start, the cuts are every boundary except 0 and the file end"
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
// Restarting at segment 2 means the run begins at 20 s; a cut at 30 s is 10 s
|
|
28
|
-
// into the run. Sending 30 would put it at 50 — `-segment_times` counts from
|
|
29
|
-
// the run, which is what made a seek land in the wrong place.
|
|
30
|
-
assert.deepEqual(
|
|
31
|
-
segmentCutTimesFrom(boundaries, 2),
|
|
32
|
-
[10],
|
|
33
|
-
"after a restart the times must be relative to where the run starts"
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("uneven, real-world boundaries survive the rebasing exactly", () => {
|
|
38
|
-
// Keyframes never land on round numbers; rounding here is what would push a
|
|
39
|
-
// cut past its keyframe and silently double a segment.
|
|
40
|
-
// Starting at segment 1 leaves segments 1, 2 and 3 — three segments, so two
|
|
41
|
-
// cuts between them, each measured from the run start at 10.427.
|
|
42
|
-
const boundaries = [0, 10.427, 20.854, 31.281, 41.708];
|
|
43
|
-
assert.deepEqual(segmentCutTimesFrom(boundaries, 1), [10.427, 20.854]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("boundaries that cannot serve are refused rather than half-used", () => {
|
|
47
|
-
assert.equal(segmentCutTimesFrom(null, 0), null);
|
|
48
|
-
assert.equal(segmentCutTimesFrom([], 0), null);
|
|
49
|
-
assert.equal(segmentCutTimesFrom([0], 0), null, "a single boundary describes no segment");
|
|
50
|
-
assert.equal(
|
|
51
|
-
segmentCutTimesFrom([0, 10, 20], 2),
|
|
52
|
-
null,
|
|
53
|
-
"a start index at or past the last boundary has no run to describe"
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("the final run has no interior cuts and asks for none", () => {
|
|
58
|
-
assert.deepEqual(
|
|
59
|
-
segmentCutTimesFrom([0, 10, 20], 1),
|
|
60
|
-
[],
|
|
61
|
-
"starting at the last segment leaves nothing to cut, which is not an error"
|
|
62
|
-
);
|
|
63
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* The playlist and the real segments must describe the same thing.
|
|
3
|
+
*
|
|
4
|
+
* They did not. The playlist was built from the container index while ffmpeg
|
|
5
|
+
* chose its own cut points from a target duration, and the two only ever agreed
|
|
6
|
+
* by luck — on a field file the index listed 1902 keyframes, ffmpeg found about
|
|
7
|
+
* twice as many, and segment #876 meant 1:26:50 to the player and roughly
|
|
8
|
+
* minute 58 to ffmpeg. Seeks landed nowhere near where they were aimed.
|
|
9
|
+
*
|
|
10
|
+
* These tests pin the arithmetic that ties the two together.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { segmentCutTimesFrom } from "../services/encode/run-command.js";
|
|
16
|
+
|
|
17
|
+
test("cut times are the interior boundaries, rebased on the run start", () => {
|
|
18
|
+
// Segments 0-10, 10-20, 20-30, 30-40 (file ends at 40).
|
|
19
|
+
const boundaries = [0, 10, 20, 30, 40];
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(
|
|
22
|
+
segmentCutTimesFrom(boundaries, 0),
|
|
23
|
+
[10, 20, 30],
|
|
24
|
+
"from the start, the cuts are every boundary except 0 and the file end"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Restarting at segment 2 means the run begins at 20 s; a cut at 30 s is 10 s
|
|
28
|
+
// into the run. Sending 30 would put it at 50 — `-segment_times` counts from
|
|
29
|
+
// the run, which is what made a seek land in the wrong place.
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
segmentCutTimesFrom(boundaries, 2),
|
|
32
|
+
[10],
|
|
33
|
+
"after a restart the times must be relative to where the run starts"
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("uneven, real-world boundaries survive the rebasing exactly", () => {
|
|
38
|
+
// Keyframes never land on round numbers; rounding here is what would push a
|
|
39
|
+
// cut past its keyframe and silently double a segment.
|
|
40
|
+
// Starting at segment 1 leaves segments 1, 2 and 3 — three segments, so two
|
|
41
|
+
// cuts between them, each measured from the run start at 10.427.
|
|
42
|
+
const boundaries = [0, 10.427, 20.854, 31.281, 41.708];
|
|
43
|
+
assert.deepEqual(segmentCutTimesFrom(boundaries, 1), [10.427, 20.854]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("boundaries that cannot serve are refused rather than half-used", () => {
|
|
47
|
+
assert.equal(segmentCutTimesFrom(null, 0), null);
|
|
48
|
+
assert.equal(segmentCutTimesFrom([], 0), null);
|
|
49
|
+
assert.equal(segmentCutTimesFrom([0], 0), null, "a single boundary describes no segment");
|
|
50
|
+
assert.equal(
|
|
51
|
+
segmentCutTimesFrom([0, 10, 20], 2),
|
|
52
|
+
null,
|
|
53
|
+
"a start index at or past the last boundary has no run to describe"
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the final run has no interior cuts and asks for none", () => {
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
segmentCutTimesFrom([0, 10, 20], 1),
|
|
60
|
+
[],
|
|
61
|
+
"starting at the last segment leaves nothing to cut, which is not an error"
|
|
62
|
+
);
|
|
63
|
+
});
|