@torrent-tv/proxy 2.88.4 → 2.88.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2266 -2259
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/calibration/audio/NOTICE.md +19 -0
- package/assets/calibration/audio/aac.m4a +0 -0
- package/assets/calibration/audio/ac3.ac3 +0 -0
- package/assets/calibration/audio/alac.m4a +0 -0
- package/assets/calibration/audio/dts.dts +0 -0
- package/assets/calibration/audio/eac3.eac3 +0 -0
- package/assets/calibration/audio/flac.flac +0 -0
- package/assets/calibration/audio/manifest.json +98 -0
- package/assets/calibration/audio/mp2.mp2 +0 -0
- package/assets/calibration/audio/mp3.mp3 +0 -0
- package/assets/calibration/audio/opus.opus +0 -0
- package/assets/calibration/audio/pcm.wav +0 -0
- package/assets/calibration/audio/truehd.thd +0 -0
- package/assets/calibration/audio/vorbis.ogg +0 -0
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -411
- package/docs/browser-proxy-contract.md +155 -152
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +56 -0
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/build-audio-samples.mjs +31 -0
- package/scripts/render-run-graph.js +173 -173
- package/server.js +730 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1237 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -783
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2282
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/audio-calibration.js +67 -0
- package/services/encode/audio-work-rate.js +15 -0
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1167 -1150
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -349
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +458 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +12 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +299 -298
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -609
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1344
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +785 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +516 -708
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1020 -995
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1937 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1255 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2688 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-calibration.test.js +23 -0
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-presentation-clock.test.js +162 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -112
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +273 -270
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +483 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +338 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stamping a segment's position onto its fragments.
|
|
3
|
-
*
|
|
4
|
-
* A segment produced by the explicit-cut muxer holds SEVERAL fragments per
|
|
5
|
-
* track — `frag_keyframe` opens one at every keyframe, while a cut point comes
|
|
6
|
-
* only every few keyframes. Writing the segment's start into each of them made
|
|
7
|
-
* them all claim the same decode time, and the player re-fetched the segment
|
|
8
|
-
* forever. The positions inside a segment must therefore survive.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { stampSegmentStartTime } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} type
|
|
17
|
-
* @param {Buffer} body
|
|
18
|
-
* @returns {Buffer}
|
|
19
|
-
*/
|
|
20
|
-
function box(type, body) {
|
|
21
|
-
const header = Buffer.alloc(8);
|
|
22
|
-
header.writeUInt32BE(8 + body.length, 0);
|
|
23
|
-
header.write(type, 4, "latin1");
|
|
24
|
-
return Buffer.concat([header, body]);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* One fragment: `moof > traf > (tfhd, tfdt)`, plus the payload it describes.
|
|
29
|
-
*
|
|
30
|
-
* @param {{ trackId: number, decodeTime: number, version?: 0 | 1 }} fragment
|
|
31
|
-
* @returns {Buffer}
|
|
32
|
-
*/
|
|
33
|
-
function makeFragment({ trackId, decodeTime, version = 1 }) {
|
|
34
|
-
const tfhdBody = Buffer.alloc(8);
|
|
35
|
-
tfhdBody.writeUInt32BE(trackId, 4);
|
|
36
|
-
const tfdtBody = Buffer.alloc(version === 1 ? 12 : 8);
|
|
37
|
-
tfdtBody[0] = version;
|
|
38
|
-
if (version === 1) {
|
|
39
|
-
tfdtBody.writeBigUInt64BE(BigInt(decodeTime), 4);
|
|
40
|
-
} else {
|
|
41
|
-
tfdtBody.writeUInt32BE(decodeTime, 4);
|
|
42
|
-
}
|
|
43
|
-
const traf = box("traf", Buffer.concat([box("tfhd", tfhdBody), box("tfdt", tfdtBody)]));
|
|
44
|
-
return Buffer.concat([box("moof", traf), box("mdat", Buffer.alloc(16, 0x5a))]);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Every `tfdt` in order, as `[trackId, decodeTime]`.
|
|
49
|
-
*
|
|
50
|
-
* @param {Buffer} segment
|
|
51
|
-
* @returns {Array<[number, number]>}
|
|
52
|
-
*/
|
|
53
|
-
function readDecodeTimes(segment) {
|
|
54
|
-
const found = [];
|
|
55
|
-
let offset = 0;
|
|
56
|
-
let trackId = 0;
|
|
57
|
-
while (offset + 8 <= segment.length) {
|
|
58
|
-
const size = segment.readUInt32BE(offset);
|
|
59
|
-
const type = segment.toString("latin1", offset + 4, offset + 8);
|
|
60
|
-
if (type === "tfhd") {
|
|
61
|
-
trackId = segment.readUInt32BE(offset + 12);
|
|
62
|
-
} else if (type === "tfdt") {
|
|
63
|
-
const version = segment[offset + 8];
|
|
64
|
-
found.push([
|
|
65
|
-
trackId,
|
|
66
|
-
version === 1 ? Number(segment.readBigUInt64BE(offset + 12)) : segment.readUInt32BE(offset + 12)
|
|
67
|
-
]);
|
|
68
|
-
}
|
|
69
|
-
// Descend into the containers on the way to `tfdt`; skip anything else
|
|
70
|
-
// whole, so `mdat` is never walked into.
|
|
71
|
-
offset += type === "moof" || type === "traf" ? 8 : size;
|
|
72
|
-
}
|
|
73
|
-
return found;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
test("fragments keep their distance from the start of the segment", () => {
|
|
77
|
-
// A 6 s segment holding three fragments at 0, 2 and 4 s of its own clock —
|
|
78
|
-
// the shape measured on the field host.
|
|
79
|
-
const timescale = 16_000;
|
|
80
|
-
const segment = Buffer.concat([
|
|
81
|
-
makeFragment({ trackId: 1, decodeTime: 0 }),
|
|
82
|
-
makeFragment({ trackId: 1, decodeTime: 2 * timescale }),
|
|
83
|
-
makeFragment({ trackId: 1, decodeTime: 4 * timescale })
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
const stamped = stampSegmentStartTime(segment, 6, new Map([[1, timescale]]));
|
|
87
|
-
|
|
88
|
-
assert.deepEqual(
|
|
89
|
-
readDecodeTimes(stamped).map(([, time]) => time / timescale),
|
|
90
|
-
[6, 8, 10],
|
|
91
|
-
"each fragment must land at the segment's start plus its own offset"
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("each track is shifted by its own base", () => {
|
|
96
|
-
const videoScale = 16_000;
|
|
97
|
-
const audioScale = 44_100;
|
|
98
|
-
// Audio does not start at zero: its frames do not align with the video's.
|
|
99
|
-
const segment = Buffer.concat([
|
|
100
|
-
makeFragment({ trackId: 1, decodeTime: 0 }),
|
|
101
|
-
makeFragment({ trackId: 2, decodeTime: 1024 }),
|
|
102
|
-
makeFragment({ trackId: 1, decodeTime: 2 * videoScale }),
|
|
103
|
-
makeFragment({ trackId: 2, decodeTime: 1024 + 2 * audioScale })
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
const stamped = stampSegmentStartTime(
|
|
107
|
-
segment,
|
|
108
|
-
6,
|
|
109
|
-
new Map([
|
|
110
|
-
[1, videoScale],
|
|
111
|
-
[2, audioScale]
|
|
112
|
-
])
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
assert.deepEqual(readDecodeTimes(stamped), [
|
|
116
|
-
[1, 6 * videoScale],
|
|
117
|
-
[2, 6 * audioScale],
|
|
118
|
-
[1, 8 * videoScale],
|
|
119
|
-
[2, 8 * audioScale]
|
|
120
|
-
]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("a single fragment is written outright, as before", () => {
|
|
124
|
-
const stamped = stampSegmentStartTime(
|
|
125
|
-
makeFragment({ trackId: 1, decodeTime: 1280 }),
|
|
126
|
-
12,
|
|
127
|
-
new Map([[1, 16_000]])
|
|
128
|
-
);
|
|
129
|
-
assert.deepEqual(readDecodeTimes(stamped), [[1, 12 * 16_000]]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("a 32-bit field too small for the position is left alone", () => {
|
|
133
|
-
const segment = makeFragment({ trackId: 1, decodeTime: 0, version: 0 });
|
|
134
|
-
const stamped = stampSegmentStartTime(segment, 1_000_000, new Map([[1, 90_000]]));
|
|
135
|
-
assert.deepEqual(
|
|
136
|
-
readDecodeTimes(stamped),
|
|
137
|
-
[[1, 0]],
|
|
138
|
-
"a wrapped value would send the player somewhere arbitrary; leaving it is the lesser harm"
|
|
139
|
-
);
|
|
140
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Stamping a segment's position onto its fragments.
|
|
3
|
+
*
|
|
4
|
+
* A segment produced by the explicit-cut muxer holds SEVERAL fragments per
|
|
5
|
+
* track — `frag_keyframe` opens one at every keyframe, while a cut point comes
|
|
6
|
+
* only every few keyframes. Writing the segment's start into each of them made
|
|
7
|
+
* them all claim the same decode time, and the player re-fetched the segment
|
|
8
|
+
* forever. The positions inside a segment must therefore survive.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { stampSegmentStartTime } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} type
|
|
17
|
+
* @param {Buffer} body
|
|
18
|
+
* @returns {Buffer}
|
|
19
|
+
*/
|
|
20
|
+
function box(type, body) {
|
|
21
|
+
const header = Buffer.alloc(8);
|
|
22
|
+
header.writeUInt32BE(8 + body.length, 0);
|
|
23
|
+
header.write(type, 4, "latin1");
|
|
24
|
+
return Buffer.concat([header, body]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One fragment: `moof > traf > (tfhd, tfdt)`, plus the payload it describes.
|
|
29
|
+
*
|
|
30
|
+
* @param {{ trackId: number, decodeTime: number, version?: 0 | 1 }} fragment
|
|
31
|
+
* @returns {Buffer}
|
|
32
|
+
*/
|
|
33
|
+
function makeFragment({ trackId, decodeTime, version = 1 }) {
|
|
34
|
+
const tfhdBody = Buffer.alloc(8);
|
|
35
|
+
tfhdBody.writeUInt32BE(trackId, 4);
|
|
36
|
+
const tfdtBody = Buffer.alloc(version === 1 ? 12 : 8);
|
|
37
|
+
tfdtBody[0] = version;
|
|
38
|
+
if (version === 1) {
|
|
39
|
+
tfdtBody.writeBigUInt64BE(BigInt(decodeTime), 4);
|
|
40
|
+
} else {
|
|
41
|
+
tfdtBody.writeUInt32BE(decodeTime, 4);
|
|
42
|
+
}
|
|
43
|
+
const traf = box("traf", Buffer.concat([box("tfhd", tfhdBody), box("tfdt", tfdtBody)]));
|
|
44
|
+
return Buffer.concat([box("moof", traf), box("mdat", Buffer.alloc(16, 0x5a))]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every `tfdt` in order, as `[trackId, decodeTime]`.
|
|
49
|
+
*
|
|
50
|
+
* @param {Buffer} segment
|
|
51
|
+
* @returns {Array<[number, number]>}
|
|
52
|
+
*/
|
|
53
|
+
function readDecodeTimes(segment) {
|
|
54
|
+
const found = [];
|
|
55
|
+
let offset = 0;
|
|
56
|
+
let trackId = 0;
|
|
57
|
+
while (offset + 8 <= segment.length) {
|
|
58
|
+
const size = segment.readUInt32BE(offset);
|
|
59
|
+
const type = segment.toString("latin1", offset + 4, offset + 8);
|
|
60
|
+
if (type === "tfhd") {
|
|
61
|
+
trackId = segment.readUInt32BE(offset + 12);
|
|
62
|
+
} else if (type === "tfdt") {
|
|
63
|
+
const version = segment[offset + 8];
|
|
64
|
+
found.push([
|
|
65
|
+
trackId,
|
|
66
|
+
version === 1 ? Number(segment.readBigUInt64BE(offset + 12)) : segment.readUInt32BE(offset + 12)
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
// Descend into the containers on the way to `tfdt`; skip anything else
|
|
70
|
+
// whole, so `mdat` is never walked into.
|
|
71
|
+
offset += type === "moof" || type === "traf" ? 8 : size;
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test("fragments keep their distance from the start of the segment", () => {
|
|
77
|
+
// A 6 s segment holding three fragments at 0, 2 and 4 s of its own clock —
|
|
78
|
+
// the shape measured on the field host.
|
|
79
|
+
const timescale = 16_000;
|
|
80
|
+
const segment = Buffer.concat([
|
|
81
|
+
makeFragment({ trackId: 1, decodeTime: 0 }),
|
|
82
|
+
makeFragment({ trackId: 1, decodeTime: 2 * timescale }),
|
|
83
|
+
makeFragment({ trackId: 1, decodeTime: 4 * timescale })
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const stamped = stampSegmentStartTime(segment, 6, new Map([[1, timescale]]));
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
readDecodeTimes(stamped).map(([, time]) => time / timescale),
|
|
90
|
+
[6, 8, 10],
|
|
91
|
+
"each fragment must land at the segment's start plus its own offset"
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("each track is shifted by its own base", () => {
|
|
96
|
+
const videoScale = 16_000;
|
|
97
|
+
const audioScale = 44_100;
|
|
98
|
+
// Audio does not start at zero: its frames do not align with the video's.
|
|
99
|
+
const segment = Buffer.concat([
|
|
100
|
+
makeFragment({ trackId: 1, decodeTime: 0 }),
|
|
101
|
+
makeFragment({ trackId: 2, decodeTime: 1024 }),
|
|
102
|
+
makeFragment({ trackId: 1, decodeTime: 2 * videoScale }),
|
|
103
|
+
makeFragment({ trackId: 2, decodeTime: 1024 + 2 * audioScale })
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const stamped = stampSegmentStartTime(
|
|
107
|
+
segment,
|
|
108
|
+
6,
|
|
109
|
+
new Map([
|
|
110
|
+
[1, videoScale],
|
|
111
|
+
[2, audioScale]
|
|
112
|
+
])
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
assert.deepEqual(readDecodeTimes(stamped), [
|
|
116
|
+
[1, 6 * videoScale],
|
|
117
|
+
[2, 6 * audioScale],
|
|
118
|
+
[1, 8 * videoScale],
|
|
119
|
+
[2, 8 * audioScale]
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a single fragment is written outright, as before", () => {
|
|
124
|
+
const stamped = stampSegmentStartTime(
|
|
125
|
+
makeFragment({ trackId: 1, decodeTime: 1280 }),
|
|
126
|
+
12,
|
|
127
|
+
new Map([[1, 16_000]])
|
|
128
|
+
);
|
|
129
|
+
assert.deepEqual(readDecodeTimes(stamped), [[1, 12 * 16_000]]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("a 32-bit field too small for the position is left alone", () => {
|
|
133
|
+
const segment = makeFragment({ trackId: 1, decodeTime: 0, version: 0 });
|
|
134
|
+
const stamped = stampSegmentStartTime(segment, 1_000_000, new Map([[1, 90_000]]));
|
|
135
|
+
assert.deepEqual(
|
|
136
|
+
readDecodeTimes(stamped),
|
|
137
|
+
[[1, 0]],
|
|
138
|
+
"a wrapped value would send the player somewhere arbitrary; leaving it is the lesser harm"
|
|
139
|
+
);
|
|
140
|
+
});
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A segment that is short of a track must not be served.
|
|
3
|
-
*
|
|
4
|
-
* A run that is terminated closes its current output file properly — trailing
|
|
5
|
-
* index and all — but the file holds only what had been muxed by then, and
|
|
6
|
-
* after a seek-restart that is routinely one track of two. Nothing about it
|
|
7
|
-
* looks unfinished, so the readiness rule called it done and served it, and the
|
|
8
|
-
* player could not complete the seek. Measured 2026-08-06: segment #133 carried
|
|
9
|
-
* one `tfdt` where its neighbours carried two, every request for it was
|
|
10
|
-
* answered in 98 ms, and the viewer sat on a spinner.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
16
|
-
// Build a minimal init declaring two tracks, and fragments declaring one/two.
|
|
17
|
-
function box(type, body) {
|
|
18
|
-
const b = Buffer.alloc(8 + body.length);
|
|
19
|
-
b.writeUInt32BE(8 + body.length, 0); b.write(type, 4, "latin1"); body.copy(b, 8);
|
|
20
|
-
return b;
|
|
21
|
-
}
|
|
22
|
-
function mdhd(timescale) { const b = Buffer.alloc(20); b.writeUInt32BE(timescale, 12); return box("mdhd", b); }
|
|
23
|
-
function tkhd(id) { const b = Buffer.alloc(84); b.writeUInt32BE(id, 12); return box("tkhd", b); }
|
|
24
|
-
function trak(id, ts) { return box("trak", Buffer.concat([tkhd(id), box("mdia", mdhd(ts))])); }
|
|
25
|
-
const init = box("moov", Buffer.concat([trak(1, 16000), trak(2, 48000)]));
|
|
26
|
-
function tfhd(id) { const b = Buffer.alloc(8); b.writeUInt32BE(id, 4); return box("tfhd", b); }
|
|
27
|
-
function moof(ids) { return box("moof", Buffer.concat(ids.map((id) => box("traf", tfhd(id))))); }
|
|
28
|
-
|
|
29
|
-
test("a segment with every track is accepted", () => {
|
|
30
|
-
assert.equal(fmp4Format.hasEveryTrack(moof([1, 2]), init), true);
|
|
31
|
-
});
|
|
32
|
-
test("a segment left short of a track by a killed run is refused", () => {
|
|
33
|
-
assert.equal(fmp4Format.hasEveryTrack(moof([1]), init), false,
|
|
34
|
-
"one track of two is what a terminated run leaves behind, and it looks complete");
|
|
35
|
-
});
|
|
36
|
-
test("an empty file is refused", () => {
|
|
37
|
-
assert.equal(fmp4Format.hasEveryTrack(Buffer.alloc(0), init), false);
|
|
38
|
-
});
|
|
39
|
-
test("without an init there is nothing to compare against, so it is allowed", () => {
|
|
40
|
-
assert.equal(fmp4Format.hasEveryTrack(moof([1]), null), true);
|
|
41
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file A segment that is short of a track must not be served.
|
|
3
|
+
*
|
|
4
|
+
* A run that is terminated closes its current output file properly — trailing
|
|
5
|
+
* index and all — but the file holds only what had been muxed by then, and
|
|
6
|
+
* after a seek-restart that is routinely one track of two. Nothing about it
|
|
7
|
+
* looks unfinished, so the readiness rule called it done and served it, and the
|
|
8
|
+
* player could not complete the seek. Measured 2026-08-06: segment #133 carried
|
|
9
|
+
* one `tfdt` where its neighbours carried two, every request for it was
|
|
10
|
+
* answered in 98 ms, and the viewer sat on a spinner.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
16
|
+
// Build a minimal init declaring two tracks, and fragments declaring one/two.
|
|
17
|
+
function box(type, body) {
|
|
18
|
+
const b = Buffer.alloc(8 + body.length);
|
|
19
|
+
b.writeUInt32BE(8 + body.length, 0); b.write(type, 4, "latin1"); body.copy(b, 8);
|
|
20
|
+
return b;
|
|
21
|
+
}
|
|
22
|
+
function mdhd(timescale) { const b = Buffer.alloc(20); b.writeUInt32BE(timescale, 12); return box("mdhd", b); }
|
|
23
|
+
function tkhd(id) { const b = Buffer.alloc(84); b.writeUInt32BE(id, 12); return box("tkhd", b); }
|
|
24
|
+
function trak(id, ts) { return box("trak", Buffer.concat([tkhd(id), box("mdia", mdhd(ts))])); }
|
|
25
|
+
const init = box("moov", Buffer.concat([trak(1, 16000), trak(2, 48000)]));
|
|
26
|
+
function tfhd(id) { const b = Buffer.alloc(8); b.writeUInt32BE(id, 4); return box("tfhd", b); }
|
|
27
|
+
function moof(ids) { return box("moof", Buffer.concat(ids.map((id) => box("traf", tfhd(id))))); }
|
|
28
|
+
|
|
29
|
+
test("a segment with every track is accepted", () => {
|
|
30
|
+
assert.equal(fmp4Format.hasEveryTrack(moof([1, 2]), init), true);
|
|
31
|
+
});
|
|
32
|
+
test("a segment left short of a track by a killed run is refused", () => {
|
|
33
|
+
assert.equal(fmp4Format.hasEveryTrack(moof([1]), init), false,
|
|
34
|
+
"one track of two is what a terminated run leaves behind, and it looks complete");
|
|
35
|
+
});
|
|
36
|
+
test("an empty file is refused", () => {
|
|
37
|
+
assert.equal(fmp4Format.hasEveryTrack(Buffer.alloc(0), init), false);
|
|
38
|
+
});
|
|
39
|
+
test("without an init there is nothing to compare against, so it is allowed", () => {
|
|
40
|
+
assert.equal(fmp4Format.hasEveryTrack(moof([1]), null), true);
|
|
41
|
+
});
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file An output is named by the format it produces, and nothing may put
|
|
3
|
-
* pieces of another format under that name.
|
|
4
|
-
*
|
|
5
|
-
* Two ways that could still happen once the name became the format: a
|
|
6
|
-
* directory left by an earlier version, whose key named the box a viewer asked
|
|
7
|
-
* for and so cannot say what format is inside; and a hardware encoder failing,
|
|
8
|
-
* after which the proxy encodes with software while outputs named by the failed
|
|
9
|
-
* encoder are still being watched.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
17
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
18
|
-
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
19
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
20
|
-
|
|
21
|
-
test("a directory whose key names an asked-for box is not adopted, and one naming a format is", (t) => {
|
|
22
|
-
const { store, root, manager, cleanup } = managerWithOwnStore();
|
|
23
|
-
t.after(cleanup);
|
|
24
|
-
const oldKey = "torrent:abc:fmt=fmp4:grid=even@0:video-only:v=0/enc:1280x720:budget";
|
|
25
|
-
const newKey = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, audioSeparate: true }).toKey();
|
|
26
|
-
const oldDir = store.directoryFor(oldKey);
|
|
27
|
-
const newDir = store.directoryFor(newKey);
|
|
28
|
-
writeFileSync(path.join(oldDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
29
|
-
writeFileSync(path.join(newDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
30
|
-
|
|
31
|
-
const result = manager.lifecycle.adoptSegmentsLeftBehind();
|
|
32
|
-
|
|
33
|
-
assert.equal(result.adopted, 1);
|
|
34
|
-
assert.equal(result.dropped, 1);
|
|
35
|
-
assert.equal(existsSync(oldDir), false, "the old directory goes");
|
|
36
|
-
assert.equal(existsSync(newDir), true, "the named format stays");
|
|
37
|
-
assert.ok(root);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("when a hardware encoder fails, the outputs it named are closed rather than continued by software", async (t) => {
|
|
41
|
-
const { manager, cleanup } = managerWithOwnStore({
|
|
42
|
-
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
43
|
-
});
|
|
44
|
-
t.after(async () => {
|
|
45
|
-
await manager.lifecycle.disposeAll();
|
|
46
|
-
cleanup();
|
|
47
|
-
});
|
|
48
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
49
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
50
|
-
const file = new SourceFile({ sourceKey: "torrent:abc", fileIndex: 0, name: "film.mkv" });
|
|
51
|
-
const record = (id, encoder) => {
|
|
52
|
-
const spec = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, encoder });
|
|
53
|
-
const output = { id, spec, get outputKey() { return this.spec.toKey(); }, file };
|
|
54
|
-
manager.outputs.set(id, output);
|
|
55
|
-
return output;
|
|
56
|
-
};
|
|
57
|
-
const failing = record("aaaaaaaa00000001", "h264_vaapi");
|
|
58
|
-
const software = record("aaaaaaaa00000002", "libx264");
|
|
59
|
-
const run = { from: 0, to: -1, argsDescribed: "ffmpeg …" };
|
|
60
|
-
manager.encodeOrchestrator.adopt(failing.outputKey, run);
|
|
61
|
-
|
|
62
|
-
manager.encodeRuns.noteRunEnded(failing, run, {
|
|
63
|
-
address: failing.outputKey,
|
|
64
|
-
run,
|
|
65
|
-
ending: ENCODE_EXIT.FAILED,
|
|
66
|
-
from: 0,
|
|
67
|
-
to: -1,
|
|
68
|
-
livedMs: 30_000,
|
|
69
|
-
because: "vaapi failed",
|
|
70
|
-
lastError: "vaapi failed",
|
|
71
|
-
producedCount: 3
|
|
72
|
-
});
|
|
73
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
74
|
-
|
|
75
|
-
assert.equal(manager.videoEncoder.kind, "software");
|
|
76
|
-
assert.equal(manager.outputs.has(failing.id), false, "an output of the failed encoder is closed");
|
|
77
|
-
assert.equal(manager.outputs.has(software.id), true, "an output of another encoder is untouched");
|
|
78
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file An output is named by the format it produces, and nothing may put
|
|
3
|
+
* pieces of another format under that name.
|
|
4
|
+
*
|
|
5
|
+
* Two ways that could still happen once the name became the format: a
|
|
6
|
+
* directory left by an earlier version, whose key named the box a viewer asked
|
|
7
|
+
* for and so cannot say what format is inside; and a hardware encoder failing,
|
|
8
|
+
* after which the proxy encodes with software while outputs named by the failed
|
|
9
|
+
* encoder are still being watched.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
17
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
18
|
+
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
19
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
20
|
+
|
|
21
|
+
test("a directory whose key names an asked-for box is not adopted, and one naming a format is", (t) => {
|
|
22
|
+
const { store, root, manager, cleanup } = managerWithOwnStore();
|
|
23
|
+
t.after(cleanup);
|
|
24
|
+
const oldKey = "torrent:abc:fmt=fmp4:grid=even@0:video-only:v=0/enc:1280x720:budget";
|
|
25
|
+
const newKey = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, audioSeparate: true }).toKey();
|
|
26
|
+
const oldDir = store.directoryFor(oldKey);
|
|
27
|
+
const newDir = store.directoryFor(newKey);
|
|
28
|
+
writeFileSync(path.join(oldDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
29
|
+
writeFileSync(path.join(newDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
30
|
+
|
|
31
|
+
const result = manager.lifecycle.adoptSegmentsLeftBehind();
|
|
32
|
+
|
|
33
|
+
assert.equal(result.adopted, 1);
|
|
34
|
+
assert.equal(result.dropped, 1);
|
|
35
|
+
assert.equal(existsSync(oldDir), false, "the old directory goes");
|
|
36
|
+
assert.equal(existsSync(newDir), true, "the named format stays");
|
|
37
|
+
assert.ok(root);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("when a hardware encoder fails, the outputs it named are closed rather than continued by software", async (t) => {
|
|
41
|
+
const { manager, cleanup } = managerWithOwnStore({
|
|
42
|
+
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
43
|
+
});
|
|
44
|
+
t.after(async () => {
|
|
45
|
+
await manager.lifecycle.disposeAll();
|
|
46
|
+
cleanup();
|
|
47
|
+
});
|
|
48
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
49
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
50
|
+
const file = new SourceFile({ sourceKey: "torrent:abc", fileIndex: 0, name: "film.mkv" });
|
|
51
|
+
const record = (id, encoder) => {
|
|
52
|
+
const spec = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, encoder });
|
|
53
|
+
const output = { id, spec, get outputKey() { return this.spec.toKey(); }, file };
|
|
54
|
+
manager.outputs.set(id, output);
|
|
55
|
+
return output;
|
|
56
|
+
};
|
|
57
|
+
const failing = record("aaaaaaaa00000001", "h264_vaapi");
|
|
58
|
+
const software = record("aaaaaaaa00000002", "libx264");
|
|
59
|
+
const run = { from: 0, to: -1, argsDescribed: "ffmpeg …" };
|
|
60
|
+
manager.encodeOrchestrator.adopt(failing.outputKey, run);
|
|
61
|
+
|
|
62
|
+
manager.encodeRuns.noteRunEnded(failing, run, {
|
|
63
|
+
address: failing.outputKey,
|
|
64
|
+
run,
|
|
65
|
+
ending: ENCODE_EXIT.FAILED,
|
|
66
|
+
from: 0,
|
|
67
|
+
to: -1,
|
|
68
|
+
livedMs: 30_000,
|
|
69
|
+
because: "vaapi failed",
|
|
70
|
+
lastError: "vaapi failed",
|
|
71
|
+
producedCount: 3
|
|
72
|
+
});
|
|
73
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
74
|
+
|
|
75
|
+
assert.equal(manager.videoEncoder.kind, "software");
|
|
76
|
+
assert.equal(manager.outputs.has(failing.id), false, "an output of the failed encoder is closed");
|
|
77
|
+
assert.equal(manager.outputs.has(software.id), true, "an output of another encoder is untouched");
|
|
78
|
+
});
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a proxy says about itself, and what the pool scores it on.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import os from "node:os";
|
|
8
|
-
|
|
9
|
-
import { collectHealthMetrics } from "../services/transport/health-collector.js";
|
|
10
|
-
import { availableMemoryBytes } from "../services/storage/machine-memory.js";
|
|
11
|
-
|
|
12
|
-
test("free memory is what could be given out, not what is idle this instant", () => {
|
|
13
|
-
const available = availableMemoryBytes();
|
|
14
|
-
assert.ok(Number.isFinite(available) && available > 0);
|
|
15
|
-
|
|
16
|
-
// On Linux this reads the kernel's own `MemAvailable`, and it is at least
|
|
17
|
-
// `os.freemem()` by construction: the kernel keeps free memory low on purpose
|
|
18
|
-
// and fills the rest with cache, which it hands back the moment anything
|
|
19
|
-
// asks. The old reading was `os.freemem()`, so a host with 4 GB of cache and
|
|
20
|
-
// 200 MB genuinely free reported itself nearly full while it had 4.2 GB to
|
|
21
|
-
// give — and that figure weighs 0.4 of every proxy's score.
|
|
22
|
-
if (os.platform() === "linux") {
|
|
23
|
-
assert.ok(
|
|
24
|
-
available >= os.freemem(),
|
|
25
|
-
`MemAvailable ${available} is below freemem ${os.freemem()}, which cannot be`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("the health report is three bounded numbers", () => {
|
|
31
|
-
const metrics = collectHealthMetrics({ availableMemoryBytes });
|
|
32
|
-
assert.ok(metrics.cpuLoad >= 0);
|
|
33
|
-
assert.ok(metrics.memFree > 0 && metrics.memFree <= 1);
|
|
34
|
-
assert.ok(Number.isInteger(metrics.uptime) && metrics.uptime >= 0);
|
|
35
|
-
// Three decimals, so a value that has not really moved does not produce a
|
|
36
|
-
// different message on every poll.
|
|
37
|
-
assert.equal(metrics.memFree, Math.round(metrics.memFree * 1000) / 1000);
|
|
38
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What a proxy says about itself, and what the pool scores it on.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
|
|
9
|
+
import { collectHealthMetrics } from "../services/transport/health-collector.js";
|
|
10
|
+
import { availableMemoryBytes } from "../services/storage/machine-memory.js";
|
|
11
|
+
|
|
12
|
+
test("free memory is what could be given out, not what is idle this instant", () => {
|
|
13
|
+
const available = availableMemoryBytes();
|
|
14
|
+
assert.ok(Number.isFinite(available) && available > 0);
|
|
15
|
+
|
|
16
|
+
// On Linux this reads the kernel's own `MemAvailable`, and it is at least
|
|
17
|
+
// `os.freemem()` by construction: the kernel keeps free memory low on purpose
|
|
18
|
+
// and fills the rest with cache, which it hands back the moment anything
|
|
19
|
+
// asks. The old reading was `os.freemem()`, so a host with 4 GB of cache and
|
|
20
|
+
// 200 MB genuinely free reported itself nearly full while it had 4.2 GB to
|
|
21
|
+
// give — and that figure weighs 0.4 of every proxy's score.
|
|
22
|
+
if (os.platform() === "linux") {
|
|
23
|
+
assert.ok(
|
|
24
|
+
available >= os.freemem(),
|
|
25
|
+
`MemAvailable ${available} is below freemem ${os.freemem()}, which cannot be`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("the health report is three bounded numbers", () => {
|
|
31
|
+
const metrics = collectHealthMetrics({ availableMemoryBytes });
|
|
32
|
+
assert.ok(metrics.cpuLoad >= 0);
|
|
33
|
+
assert.ok(metrics.memFree > 0 && metrics.memFree <= 1);
|
|
34
|
+
assert.ok(Number.isInteger(metrics.uptime) && metrics.uptime >= 0);
|
|
35
|
+
// Three decimals, so a value that has not really moved does not produce a
|
|
36
|
+
// different message on every poll.
|
|
37
|
+
assert.equal(metrics.memFree, Math.round(metrics.memFree * 1000) / 1000);
|
|
38
|
+
});
|