@torrent-tv/proxy 2.87.20 → 2.88.0
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 +8 -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/biome.json +411 -411
- package/docs/browser-proxy-contract.md +133 -133
- 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 +106 -100
- 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 +173 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +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/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +758 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2184 -2184
- 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 +1141 -1141
- 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 +315 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +515 -515
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +245 -245
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +696 -696
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +992 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1908
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +382 -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/episode-naming.js +182 -0
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -0
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +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 +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 -365
- 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 +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +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 +12 -2
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +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,143 +1,143 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Choosing who is asked for the piece a reader is blocked on.
|
|
3
|
-
*
|
|
4
|
-
* The measurement behind it (2026-08-17): a fivefold bandwidth surplus and 47
|
|
5
|
-
* blocking waits in two minutes, on pieces a median of five peers already had.
|
|
6
|
-
* A block belongs to one wire, so the read ends when the SLOWEST holder
|
|
7
|
-
* delivers — which makes "who holds it" the thing worth deciding.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
|
|
13
|
-
import { askFastestWiresFor, canPlaceRequests, wiresForPiece } from "../services/torrent/worker/fastest-wires.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {{ speed: number, has?: boolean, choking?: boolean, destroyed?: boolean }} options
|
|
17
|
-
* @returns {object}
|
|
18
|
-
*/
|
|
19
|
-
function wire({ speed, has = true, choking = false, destroyed = false }) {
|
|
20
|
-
return {
|
|
21
|
-
destroyed,
|
|
22
|
-
peerChoking: choking,
|
|
23
|
-
peerPieces: { get: () => has },
|
|
24
|
-
downloadSpeed: () => speed
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {object[]} wires
|
|
30
|
-
* @returns {{ torrent: object, asked: Array<{ speed: number, hotswap: boolean }> }}
|
|
31
|
-
*/
|
|
32
|
-
function torrentWith(wires, { refuse = false } = {}) {
|
|
33
|
-
/** @type {Array<{ speed: number, hotswap: boolean }>} */
|
|
34
|
-
const asked = [];
|
|
35
|
-
const torrent = {
|
|
36
|
-
wires,
|
|
37
|
-
_request(target, _index, hotswap) {
|
|
38
|
-
asked.push({ speed: target.downloadSpeed(), hotswap });
|
|
39
|
-
return !refuse;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
return { torrent, asked };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
test("the fastest holders are asked first", () => {
|
|
46
|
-
const { torrent, asked } = torrentWith([
|
|
47
|
-
wire({ speed: 100_000 }),
|
|
48
|
-
wire({ speed: 900_000 }),
|
|
49
|
-
wire({ speed: 400_000 })
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const result = askFastestWiresFor(torrent, 42);
|
|
53
|
-
|
|
54
|
-
assert.deepEqual(
|
|
55
|
-
asked.map((entry) => entry.speed),
|
|
56
|
-
[900_000, 400_000, 100_000],
|
|
57
|
-
"fastest first — the read ends when the slowest holder delivers"
|
|
58
|
-
);
|
|
59
|
-
assert.equal(result.asked, 3);
|
|
60
|
-
assert.equal(result.fastestBytesPerSecond, 900_000);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("hotswap is always on, because that is the whole point", () => {
|
|
64
|
-
// The reader is blocked precisely because every block is reserved and one of
|
|
65
|
-
// them sits with a slow wire. Without hotswap the library answers "nothing to
|
|
66
|
-
// reserve" and the lever does nothing at all.
|
|
67
|
-
const { torrent, asked } = torrentWith([wire({ speed: 10 })]);
|
|
68
|
-
askFastestWiresFor(torrent, 7);
|
|
69
|
-
assert.deepEqual(asked.map((entry) => entry.hotswap), [true]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("a wire that cannot deliver is not asked", () => {
|
|
73
|
-
const { torrent, asked } = torrentWith([
|
|
74
|
-
wire({ speed: 900_000, choking: true }),
|
|
75
|
-
wire({ speed: 800_000, has: false }),
|
|
76
|
-
wire({ speed: 700_000, destroyed: true }),
|
|
77
|
-
wire({ speed: 1_000 })
|
|
78
|
-
]);
|
|
79
|
-
|
|
80
|
-
const result = askFastestWiresFor(torrent, 3);
|
|
81
|
-
|
|
82
|
-
assert.equal(asked.length, 1, "a choking, an absent and a dead wire are all unusable");
|
|
83
|
-
assert.equal(result.considered, 1);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("a refusal is counted as a refusal", () => {
|
|
87
|
-
// The library refuses when a wire's pipeline is full or nothing can be
|
|
88
|
-
// reserved even with hotswap. That is information: the piece is waiting on
|
|
89
|
-
// the wire, not on the picker.
|
|
90
|
-
const { torrent } = torrentWith([wire({ speed: 500 }), wire({ speed: 400 })], { refuse: true });
|
|
91
|
-
const result = askFastestWiresFor(torrent, 11);
|
|
92
|
-
assert.equal(result.asked, 0);
|
|
93
|
-
assert.equal(result.considered, 2);
|
|
94
|
-
// Refused, but not because every block was spoken for — this stub has no
|
|
95
|
-
// reservations at all. The two reasons are different and only the second is
|
|
96
|
-
// the one WebTorrent's displacement thresholds decide.
|
|
97
|
-
assert.equal(result.refusedWhileReserved, 0);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("a refusal with every block already reserved is counted separately", () => {
|
|
101
|
-
// The interesting refusal: a fast peer we picked was turned away because
|
|
102
|
-
// every block belongs to somebody else and displacement did not happen. The
|
|
103
|
-
// thresholds that decide it are constants in the library — the asker above
|
|
104
|
-
// 16 KB/s, the holder below 48 KB/s and twice as slow — so a holder at
|
|
105
|
-
// 50 KB/s is never displaced however long the piece has waited. This number
|
|
106
|
-
// is what would justify replacing that rule.
|
|
107
|
-
const { torrent } = torrentWith([wire({ speed: 500 })], { refuse: true });
|
|
108
|
-
torrent.pieces = [];
|
|
109
|
-
torrent.pieces[11] = { reserve: () => -1, cancel: () => undefined };
|
|
110
|
-
const result = askFastestWiresFor(torrent, 11);
|
|
111
|
-
assert.equal(result.asked, 0);
|
|
112
|
-
assert.equal(result.refusedWhileReserved, 1);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("a build without the request entry is reported, not silently skipped", () => {
|
|
116
|
-
assert.equal(canPlaceRequests({ wires: [] }), false);
|
|
117
|
-
assert.equal(canPlaceRequests({ wires: [], _request: () => true }), true);
|
|
118
|
-
const result = askFastestWiresFor({ wires: [wire({ speed: 1 })] }, 1);
|
|
119
|
-
assert.deepEqual(
|
|
120
|
-
result,
|
|
121
|
-
{ asked: 0, refusedWhileReserved: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 }
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("a wire that cannot say how fast it is ranks last rather than throwing", () => {
|
|
126
|
-
const mute = wire({ speed: 0 });
|
|
127
|
-
mute.downloadSpeed = () => {
|
|
128
|
-
throw new Error("destroyed");
|
|
129
|
-
};
|
|
130
|
-
const ranked = wiresForPiece({ wires: [mute, wire({ speed: 5 })] }, 0);
|
|
131
|
-
assert.equal(ranked.length, 2);
|
|
132
|
-
assert.equal(ranked[0].downloadSpeed(), 5);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("a throw from the library is raised, never swallowed", () => {
|
|
136
|
-
const torrent = {
|
|
137
|
-
wires: [wire({ speed: 1 })],
|
|
138
|
-
_request() {
|
|
139
|
-
throw new Error("internal");
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
assert.throws(() => askFastestWiresFor(torrent, 5), /could not place a request for piece 5/);
|
|
143
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Choosing who is asked for the piece a reader is blocked on.
|
|
3
|
+
*
|
|
4
|
+
* The measurement behind it (2026-08-17): a fivefold bandwidth surplus and 47
|
|
5
|
+
* blocking waits in two minutes, on pieces a median of five peers already had.
|
|
6
|
+
* A block belongs to one wire, so the read ends when the SLOWEST holder
|
|
7
|
+
* delivers — which makes "who holds it" the thing worth deciding.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
|
|
13
|
+
import { askFastestWiresFor, canPlaceRequests, wiresForPiece } from "../services/torrent/worker/fastest-wires.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ speed: number, has?: boolean, choking?: boolean, destroyed?: boolean }} options
|
|
17
|
+
* @returns {object}
|
|
18
|
+
*/
|
|
19
|
+
function wire({ speed, has = true, choking = false, destroyed = false }) {
|
|
20
|
+
return {
|
|
21
|
+
destroyed,
|
|
22
|
+
peerChoking: choking,
|
|
23
|
+
peerPieces: { get: () => has },
|
|
24
|
+
downloadSpeed: () => speed
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object[]} wires
|
|
30
|
+
* @returns {{ torrent: object, asked: Array<{ speed: number, hotswap: boolean }> }}
|
|
31
|
+
*/
|
|
32
|
+
function torrentWith(wires, { refuse = false } = {}) {
|
|
33
|
+
/** @type {Array<{ speed: number, hotswap: boolean }>} */
|
|
34
|
+
const asked = [];
|
|
35
|
+
const torrent = {
|
|
36
|
+
wires,
|
|
37
|
+
_request(target, _index, hotswap) {
|
|
38
|
+
asked.push({ speed: target.downloadSpeed(), hotswap });
|
|
39
|
+
return !refuse;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return { torrent, asked };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("the fastest holders are asked first", () => {
|
|
46
|
+
const { torrent, asked } = torrentWith([
|
|
47
|
+
wire({ speed: 100_000 }),
|
|
48
|
+
wire({ speed: 900_000 }),
|
|
49
|
+
wire({ speed: 400_000 })
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const result = askFastestWiresFor(torrent, 42);
|
|
53
|
+
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
asked.map((entry) => entry.speed),
|
|
56
|
+
[900_000, 400_000, 100_000],
|
|
57
|
+
"fastest first — the read ends when the slowest holder delivers"
|
|
58
|
+
);
|
|
59
|
+
assert.equal(result.asked, 3);
|
|
60
|
+
assert.equal(result.fastestBytesPerSecond, 900_000);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("hotswap is always on, because that is the whole point", () => {
|
|
64
|
+
// The reader is blocked precisely because every block is reserved and one of
|
|
65
|
+
// them sits with a slow wire. Without hotswap the library answers "nothing to
|
|
66
|
+
// reserve" and the lever does nothing at all.
|
|
67
|
+
const { torrent, asked } = torrentWith([wire({ speed: 10 })]);
|
|
68
|
+
askFastestWiresFor(torrent, 7);
|
|
69
|
+
assert.deepEqual(asked.map((entry) => entry.hotswap), [true]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a wire that cannot deliver is not asked", () => {
|
|
73
|
+
const { torrent, asked } = torrentWith([
|
|
74
|
+
wire({ speed: 900_000, choking: true }),
|
|
75
|
+
wire({ speed: 800_000, has: false }),
|
|
76
|
+
wire({ speed: 700_000, destroyed: true }),
|
|
77
|
+
wire({ speed: 1_000 })
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const result = askFastestWiresFor(torrent, 3);
|
|
81
|
+
|
|
82
|
+
assert.equal(asked.length, 1, "a choking, an absent and a dead wire are all unusable");
|
|
83
|
+
assert.equal(result.considered, 1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a refusal is counted as a refusal", () => {
|
|
87
|
+
// The library refuses when a wire's pipeline is full or nothing can be
|
|
88
|
+
// reserved even with hotswap. That is information: the piece is waiting on
|
|
89
|
+
// the wire, not on the picker.
|
|
90
|
+
const { torrent } = torrentWith([wire({ speed: 500 }), wire({ speed: 400 })], { refuse: true });
|
|
91
|
+
const result = askFastestWiresFor(torrent, 11);
|
|
92
|
+
assert.equal(result.asked, 0);
|
|
93
|
+
assert.equal(result.considered, 2);
|
|
94
|
+
// Refused, but not because every block was spoken for — this stub has no
|
|
95
|
+
// reservations at all. The two reasons are different and only the second is
|
|
96
|
+
// the one WebTorrent's displacement thresholds decide.
|
|
97
|
+
assert.equal(result.refusedWhileReserved, 0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a refusal with every block already reserved is counted separately", () => {
|
|
101
|
+
// The interesting refusal: a fast peer we picked was turned away because
|
|
102
|
+
// every block belongs to somebody else and displacement did not happen. The
|
|
103
|
+
// thresholds that decide it are constants in the library — the asker above
|
|
104
|
+
// 16 KB/s, the holder below 48 KB/s and twice as slow — so a holder at
|
|
105
|
+
// 50 KB/s is never displaced however long the piece has waited. This number
|
|
106
|
+
// is what would justify replacing that rule.
|
|
107
|
+
const { torrent } = torrentWith([wire({ speed: 500 })], { refuse: true });
|
|
108
|
+
torrent.pieces = [];
|
|
109
|
+
torrent.pieces[11] = { reserve: () => -1, cancel: () => undefined };
|
|
110
|
+
const result = askFastestWiresFor(torrent, 11);
|
|
111
|
+
assert.equal(result.asked, 0);
|
|
112
|
+
assert.equal(result.refusedWhileReserved, 1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a build without the request entry is reported, not silently skipped", () => {
|
|
116
|
+
assert.equal(canPlaceRequests({ wires: [] }), false);
|
|
117
|
+
assert.equal(canPlaceRequests({ wires: [], _request: () => true }), true);
|
|
118
|
+
const result = askFastestWiresFor({ wires: [wire({ speed: 1 })] }, 1);
|
|
119
|
+
assert.deepEqual(
|
|
120
|
+
result,
|
|
121
|
+
{ asked: 0, refusedWhileReserved: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 }
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("a wire that cannot say how fast it is ranks last rather than throwing", () => {
|
|
126
|
+
const mute = wire({ speed: 0 });
|
|
127
|
+
mute.downloadSpeed = () => {
|
|
128
|
+
throw new Error("destroyed");
|
|
129
|
+
};
|
|
130
|
+
const ranked = wiresForPiece({ wires: [mute, wire({ speed: 5 })] }, 0);
|
|
131
|
+
assert.equal(ranked.length, 2);
|
|
132
|
+
assert.equal(ranked[0].downloadSpeed(), 5);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("a throw from the library is raised, never swallowed", () => {
|
|
136
|
+
const torrent = {
|
|
137
|
+
wires: [wire({ speed: 1 })],
|
|
138
|
+
_request() {
|
|
139
|
+
throw new Error("internal");
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
assert.throws(() => askFastestWiresFor(torrent, 5), /could not place a request for piece 5/);
|
|
143
|
+
});
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the source said it holds, read from ffmpeg's own banner.
|
|
3
|
-
*
|
|
4
|
-
* The reading exists for one failure: every `-map` this proxy builds ends in
|
|
5
|
-
* `?`, so ffmpeg drops a mapping for a stream that is not there and says
|
|
6
|
-
* nothing. Map every stream of a run away that way and the output has nothing
|
|
7
|
-
* in it — `Output file does not contain any stream`, exit 255. Three sessions
|
|
8
|
-
* died that way on 2026-08-26 and the log held only the code.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
|
|
14
|
-
import { parseFfmpegStreamCounts } from "../services/media/ffmpeg-banner.js";
|
|
15
|
-
|
|
16
|
-
const FIELD_BANNER = `
|
|
17
|
-
Input #0, matroska,webm, from 'http://127.0.0.1:9090/stream?sourceKey=b3f08efc':
|
|
18
|
-
Duration: 01:32:39.00, start: 0.000000, bitrate: 9354 kb/s
|
|
19
|
-
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080, 23.98 fps
|
|
20
|
-
Stream #0:1(rus): Audio: ac3, 48000 Hz, 5.1, fltp, 640 kb/s
|
|
21
|
-
Stream #0:2(eng): Audio: aac, 48000 Hz, stereo, fltp, 128 kb/s
|
|
22
|
-
Stream #0:3(rus): Subtitle: subrip
|
|
23
|
-
Stream #0:4(eng): Subtitle: hdmv_pgs_subtitle
|
|
24
|
-
`;
|
|
25
|
-
|
|
26
|
-
test("each stream is counted under the type ffmpeg names it", () => {
|
|
27
|
-
assert.deepEqual(parseFfmpegStreamCounts(FIELD_BANNER), {
|
|
28
|
-
video: 1,
|
|
29
|
-
audio: 2,
|
|
30
|
-
subtitle: 2,
|
|
31
|
-
other: 0
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("a file with no audio is what makes an audio-only run produce nothing", () => {
|
|
36
|
-
// This is the shape the diagnostic is for: the rendition asks for `0:a:0?`,
|
|
37
|
-
// the file has no audio at all, the mapping is dropped in silence and the
|
|
38
|
-
// segment muxer is handed an output with no streams.
|
|
39
|
-
const counts = parseFfmpegStreamCounts(`
|
|
40
|
-
Stream #0:0: Video: hevc (Main 10), yuv420p10le, 3840x2160, 23.98 fps
|
|
41
|
-
`);
|
|
42
|
-
assert.equal(counts.audio, 0);
|
|
43
|
-
assert.equal(counts.video, 1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("an audio index past the end of the list is OUR fault, and the counts say so", () => {
|
|
47
|
-
// A viewer picking the third dub on a file carrying one: `0:a:2?` drops, and
|
|
48
|
-
// the count is what separates that from a source that delivered nothing.
|
|
49
|
-
const counts = parseFfmpegStreamCounts(FIELD_BANNER);
|
|
50
|
-
const askedFor = 4;
|
|
51
|
-
assert.ok(askedFor >= counts.audio, "the request is outside what the file holds");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("an attached cover image is counted as video, because that is what would be mapped", () => {
|
|
55
|
-
const counts = parseFfmpegStreamCounts(`
|
|
56
|
-
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 320 kb/s
|
|
57
|
-
Stream #0:1: Video: mjpeg (Baseline), yuvj420p(pc), 600x600 (attached pic)
|
|
58
|
-
`);
|
|
59
|
-
assert.equal(counts.video, 1);
|
|
60
|
-
assert.equal(counts.audio, 1);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("a banner carrying no stream lines is not the same as a file with no streams", () => {
|
|
64
|
-
// Null says "not recorded"; zeroes would claim a measurement nobody took.
|
|
65
|
-
assert.equal(parseFfmpegStreamCounts("Duration: 00:10:00.00, start: 0.000000"), null);
|
|
66
|
-
assert.equal(parseFfmpegStreamCounts(""), null);
|
|
67
|
-
assert.equal(parseFfmpegStreamCounts(undefined), null);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("a stream carrying metadata in brackets is still read", () => {
|
|
71
|
-
// MP4 writes `Stream #0:1[0x2](eng)`, which the earlier shape of this regex
|
|
72
|
-
// would have skipped.
|
|
73
|
-
const counts = parseFfmpegStreamCounts(`
|
|
74
|
-
Stream #0:0[0x1](und): Video: h264 (avc1), yuv420p, 1280x720, 24 fps
|
|
75
|
-
Stream #0:1[0x2](eng): Audio: aac (mp4a), 48000 Hz, stereo, fltp
|
|
76
|
-
`);
|
|
77
|
-
assert.deepEqual(counts, { video: 1, audio: 1, subtitle: 0, other: 0 });
|
|
78
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What the source said it holds, read from ffmpeg's own banner.
|
|
3
|
+
*
|
|
4
|
+
* The reading exists for one failure: every `-map` this proxy builds ends in
|
|
5
|
+
* `?`, so ffmpeg drops a mapping for a stream that is not there and says
|
|
6
|
+
* nothing. Map every stream of a run away that way and the output has nothing
|
|
7
|
+
* in it — `Output file does not contain any stream`, exit 255. Three sessions
|
|
8
|
+
* died that way on 2026-08-26 and the log held only the code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { parseFfmpegStreamCounts } from "../services/media/ffmpeg-banner.js";
|
|
15
|
+
|
|
16
|
+
const FIELD_BANNER = `
|
|
17
|
+
Input #0, matroska,webm, from 'http://127.0.0.1:9090/stream?sourceKey=b3f08efc':
|
|
18
|
+
Duration: 01:32:39.00, start: 0.000000, bitrate: 9354 kb/s
|
|
19
|
+
Stream #0:0: Video: h264 (High), yuv420p(progressive), 1920x1080, 23.98 fps
|
|
20
|
+
Stream #0:1(rus): Audio: ac3, 48000 Hz, 5.1, fltp, 640 kb/s
|
|
21
|
+
Stream #0:2(eng): Audio: aac, 48000 Hz, stereo, fltp, 128 kb/s
|
|
22
|
+
Stream #0:3(rus): Subtitle: subrip
|
|
23
|
+
Stream #0:4(eng): Subtitle: hdmv_pgs_subtitle
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
test("each stream is counted under the type ffmpeg names it", () => {
|
|
27
|
+
assert.deepEqual(parseFfmpegStreamCounts(FIELD_BANNER), {
|
|
28
|
+
video: 1,
|
|
29
|
+
audio: 2,
|
|
30
|
+
subtitle: 2,
|
|
31
|
+
other: 0
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a file with no audio is what makes an audio-only run produce nothing", () => {
|
|
36
|
+
// This is the shape the diagnostic is for: the rendition asks for `0:a:0?`,
|
|
37
|
+
// the file has no audio at all, the mapping is dropped in silence and the
|
|
38
|
+
// segment muxer is handed an output with no streams.
|
|
39
|
+
const counts = parseFfmpegStreamCounts(`
|
|
40
|
+
Stream #0:0: Video: hevc (Main 10), yuv420p10le, 3840x2160, 23.98 fps
|
|
41
|
+
`);
|
|
42
|
+
assert.equal(counts.audio, 0);
|
|
43
|
+
assert.equal(counts.video, 1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("an audio index past the end of the list is OUR fault, and the counts say so", () => {
|
|
47
|
+
// A viewer picking the third dub on a file carrying one: `0:a:2?` drops, and
|
|
48
|
+
// the count is what separates that from a source that delivered nothing.
|
|
49
|
+
const counts = parseFfmpegStreamCounts(FIELD_BANNER);
|
|
50
|
+
const askedFor = 4;
|
|
51
|
+
assert.ok(askedFor >= counts.audio, "the request is outside what the file holds");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("an attached cover image is counted as video, because that is what would be mapped", () => {
|
|
55
|
+
const counts = parseFfmpegStreamCounts(`
|
|
56
|
+
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 320 kb/s
|
|
57
|
+
Stream #0:1: Video: mjpeg (Baseline), yuvj420p(pc), 600x600 (attached pic)
|
|
58
|
+
`);
|
|
59
|
+
assert.equal(counts.video, 1);
|
|
60
|
+
assert.equal(counts.audio, 1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a banner carrying no stream lines is not the same as a file with no streams", () => {
|
|
64
|
+
// Null says "not recorded"; zeroes would claim a measurement nobody took.
|
|
65
|
+
assert.equal(parseFfmpegStreamCounts("Duration: 00:10:00.00, start: 0.000000"), null);
|
|
66
|
+
assert.equal(parseFfmpegStreamCounts(""), null);
|
|
67
|
+
assert.equal(parseFfmpegStreamCounts(undefined), null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a stream carrying metadata in brackets is still read", () => {
|
|
71
|
+
// MP4 writes `Stream #0:1[0x2](eng)`, which the earlier shape of this regex
|
|
72
|
+
// would have skipped.
|
|
73
|
+
const counts = parseFfmpegStreamCounts(`
|
|
74
|
+
Stream #0:0[0x1](und): Video: h264 (avc1), yuv420p, 1280x720, 24 fps
|
|
75
|
+
Stream #0:1[0x2](eng): Audio: aac (mp4a), 48000 Hz, stereo, fltp
|
|
76
|
+
`);
|
|
77
|
+
assert.deepEqual(counts, { video: 1, audio: 1, subtitle: 0, other: 0 });
|
|
78
|
+
});
|