@torrent-tv/proxy 2.89.7 → 2.89.9
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 +363 -338
- package/CHANGELOG.md +2345 -2335
- 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 -19
- package/assets/calibration/audio/manifest.json +98 -98
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -412
- package/docs/browser-proxy-contract.md +155 -155
- package/docs/container-architecture.md +273 -273
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +113 -113
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +51 -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 -106
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/build-audio-samples.mjs +31 -31
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +36 -12
- 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 +784 -784
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/audio-calibration.js +67 -67
- package/services/encode/audio-work-rate.js +15 -15
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -116
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/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 +24 -24
- 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 +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +757 -757
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/init-compat.js +324 -324
- 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 +202 -202
- package/services/media/KeyframeTables.js +356 -356
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +592 -592
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +299 -299
- package/services/media/container/AviContainer.js +268 -268
- package/services/media/container/Container.js +632 -632
- package/services/media/container/ContainerFactory.js +38 -38
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1031 -1031
- package/services/media/container/Mp4Container.js +1354 -1354
- package/services/media/container/SubtitleFileContainer.js +339 -339
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/container/ebml-stream.js +174 -174
- package/services/media/container/matroska-clusters.js +1165 -1165
- package/services/media/container/unavailable.js +77 -77
- 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 +84 -84
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +295 -295
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1937 -1937
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1294 -1294
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +382 -382
- 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 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/torrent-pool.js +2688 -2688
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +836 -836
- 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 +231 -231
- 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 +1137 -1137
- package/services/transport/data-channel-handler.js +1406 -1373
- package/services/transport/delivery-probe.js +798 -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 +421 -421
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-calibration.test.js +23 -23
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -762
- 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/coalesce.test.js +39 -39
- 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 +233 -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 +162 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +475 -475
- package/test/delivery-probe-field.test.js +173 -0
- package/test/delivery-probe.test.js +382 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +20 -0
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +68 -68
- package/test/episode-naming.test.js +131 -131
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/audio-frame-boundary.json +1 -1
- 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 +36 -38
- package/test/held-read-ownership.test.js +77 -77
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +248 -240
- package/test/helpers/held-file.js +77 -77
- package/test/helpers/manager.js +62 -62
- package/test/helpers/matroska-file.js +296 -296
- 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 +123 -123
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +291 -291
- package/test/matroska-cluster-discovery.test.js +323 -323
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +273 -273
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +92 -87
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +160 -160
- package/test/quality-variants.test.js +1274 -1270
- package/test/rate-control-in-the-output.test.js +181 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/reads-not-yet-arrived.test.js +218 -218
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +64 -64
- package/test/same-height-switch.test.js +663 -663
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +114 -114
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +222 -222
- package/test/segment-serve-wiring.test.js +481 -481
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +111 -111
- 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-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +385 -385
- 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-metadata.test.js +15 -15
- package/test/subtitle-plan-waits.test.js +125 -125
- package/test/subtitle-track-numbering.test.js +391 -391
- package/test/supply-margin.test.js +190 -190
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +508 -508
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +92 -92
- package/test/viewer-starts-on-map.test.js +27 -27
- package/test/viewer-states.test.js +192 -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/whole-file-held-read.test.js +30 -30
- 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/coalesce.js +41 -41
- 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,218 +1,218 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A read whose bytes have not arrived is not an answer about the file.
|
|
3
|
-
*
|
|
4
|
-
* Field 2026-10-01: the subtitle plan of an episode was read while its Cues
|
|
5
|
-
* table was still downloading, the read was given up, and the plan kept "no
|
|
6
|
-
* clusters" for the life of the process — the track showed nothing for a whole
|
|
7
|
-
* session while the same table, read eleven seconds later for the keyframes,
|
|
8
|
-
* was there. The same shape was in the container choice, the track table, the
|
|
9
|
-
* keyframe table, MP4's `moov` and AVI's `idx1`. Every check here makes the
|
|
10
|
-
* bytes missing first and present afterwards, and asks the same object again.
|
|
11
|
-
*
|
|
12
|
-
* Fakes only: a buffer and a reader over it. No torrent, no thread.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
|
|
18
|
-
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
19
|
-
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
20
|
-
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
21
|
-
import { ContainerOrchestrator } from "../services/media/ContainerOrchestrator.js";
|
|
22
|
-
import { KeyframeTables } from "../services/media/KeyframeTables.js";
|
|
23
|
-
import { isUnavailable } from "../services/media/container/unavailable.js";
|
|
24
|
-
import {
|
|
25
|
-
ID,
|
|
26
|
-
buildMatroska,
|
|
27
|
-
clusterData,
|
|
28
|
-
cueBlock,
|
|
29
|
-
element,
|
|
30
|
-
readerOver,
|
|
31
|
-
stringElement,
|
|
32
|
-
trackEntry,
|
|
33
|
-
uintElement
|
|
34
|
-
} from "./helpers/matroska-file.js";
|
|
35
|
-
|
|
36
|
-
const quiet = { info() {}, warn() {} };
|
|
37
|
-
|
|
38
|
-
function episode({ subtitleLanguage = null, cuesBeforeClusters = false, cues = [1], extraTrack = null } = {}) {
|
|
39
|
-
const tracks = [
|
|
40
|
-
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "jpn" }),
|
|
41
|
-
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: subtitleLanguage })
|
|
42
|
-
];
|
|
43
|
-
if (extraTrack) {
|
|
44
|
-
tracks.push(extraTrack);
|
|
45
|
-
}
|
|
46
|
-
return buildMatroska({
|
|
47
|
-
tracks,
|
|
48
|
-
cues,
|
|
49
|
-
cuesBeforeClusters,
|
|
50
|
-
clusters: [
|
|
51
|
-
{ ticks: 0, data: clusterData({ ticks: 0, blocks: [cueBlock({ track: 3, relativeTicks: 100, durationTicks: 900, text: "one" })] }) },
|
|
52
|
-
{ ticks: 5000, data: clusterData({ ticks: 5000, blocks: [cueBlock({ track: 3, relativeTicks: 0, durationTicks: 900, text: "two" })] }) }
|
|
53
|
-
]
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
test("a subtitle plan read before the Cues table arrived is not kept; read again, it names the clusters", async () => {
|
|
58
|
-
const { file, cuesAt, clusterAt } = episode({ cues: [1, 3] });
|
|
59
|
-
let tailHere = false;
|
|
60
|
-
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
61
|
-
const container = new MatroskaContainer({ readRange: read, fileSize: file.length });
|
|
62
|
-
|
|
63
|
-
await assert.rejects(container.readSubtitlePlan(), (error) => isUnavailable(error));
|
|
64
|
-
|
|
65
|
-
tailHere = true;
|
|
66
|
-
const plan = await container.readSubtitlePlan();
|
|
67
|
-
assert.equal(plan.cuesState, "complete");
|
|
68
|
-
assert.deepEqual(plan.tracks[0].clusterPositions, clusterAt, "both clusters the table names for the track");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("a read that returns fewer bytes than asked is not taken for the element", async () => {
|
|
72
|
-
const { file } = episode();
|
|
73
|
-
const { read } = readerOver(file, { short: true });
|
|
74
|
-
await assert.rejects(new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks(), (error) =>
|
|
75
|
-
isUnavailable(error)
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("a head that has not arrived leaves no container behind, and the next ask builds one", async () => {
|
|
80
|
-
const { file } = episode();
|
|
81
|
-
let headHere = false;
|
|
82
|
-
const { read } = readerOver(file, { missing: (start) => !headHere && start === 0 });
|
|
83
|
-
const containers = new ContainerOrchestrator();
|
|
84
|
-
const params = { sourceKey: "s", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
85
|
-
|
|
86
|
-
await assert.rejects(containers.containerFor(params), (error) => isUnavailable(error));
|
|
87
|
-
assert.equal(containers.known("s", 0), undefined, "nothing was kept for a head that is not here");
|
|
88
|
-
assert.deepEqual(await containers.getTracks(params), [], "the track table answers nothing yet");
|
|
89
|
-
|
|
90
|
-
headHere = true;
|
|
91
|
-
const container = await containers.containerFor(params);
|
|
92
|
-
assert.equal(container.formatName, "matroska");
|
|
93
|
-
assert.equal((await containers.getTracks(params)).length, 2);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("a keyframe table asked for while its Cues are missing stays unanswered, and is filled when they arrive", async () => {
|
|
97
|
-
const { file, cuesAt } = episode();
|
|
98
|
-
let tailHere = false;
|
|
99
|
-
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
100
|
-
const containers = new ContainerOrchestrator();
|
|
101
|
-
const params = { sourceKey: "k", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
102
|
-
const tables = new KeyframeTables({
|
|
103
|
-
readTable: async () => {
|
|
104
|
-
const index = await containers.getKeyframeIndex(params);
|
|
105
|
-
return { times: index?.times ?? null, tolerance: 0, format: "matroska" };
|
|
106
|
-
},
|
|
107
|
-
logger: quiet
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await assert.rejects(tables.read({ sourceKey: "k", fileIndex: 0 }), (error) => isUnavailable(error));
|
|
111
|
-
const table = tables.of({ sourceKey: "k", fileIndex: 0 });
|
|
112
|
-
assert.equal(table.answered, false, "a read that did not arrive is not an answer");
|
|
113
|
-
|
|
114
|
-
tailHere = true;
|
|
115
|
-
tables.readAgainIfUnanswered({ sourceKey: "k", fileIndex: 0 });
|
|
116
|
-
await tables.read({ sourceKey: "k", fileIndex: 0 });
|
|
117
|
-
assert.equal(table.answered, true);
|
|
118
|
-
assert.deepEqual(table.times, [0, 5], "the same object the sessions hold now carries the times");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("a Cues element stored before the first cluster, with no SeekHead entry, is found", async () => {
|
|
122
|
-
const { file } = episode({ cuesBeforeClusters: true });
|
|
123
|
-
const { read } = readerOver(file);
|
|
124
|
-
const index = await new MatroskaContainer({ readRange: read, fileSize: file.length }).parseKeyframeIndex();
|
|
125
|
-
assert.deepEqual(index.times, [0, 5]);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("a Tracks element larger than the head window is read by its own size", async () => {
|
|
129
|
-
// A CodecPrivate of 80 KB pushes the end of Tracks past the 64 KB head.
|
|
130
|
-
const big = element(
|
|
131
|
-
ID.TRACK_ENTRY,
|
|
132
|
-
Buffer.concat([
|
|
133
|
-
uintElement(ID.TRACK_NUMBER, 4),
|
|
134
|
-
uintElement(ID.TRACK_TYPE, 17),
|
|
135
|
-
stringElement(ID.CODEC_ID, "S_TEXT/ASS"),
|
|
136
|
-
element(0x63a2, Buffer.alloc(80 * 1024, 0x20))
|
|
137
|
-
])
|
|
138
|
-
);
|
|
139
|
-
const { file } = episode({ extraTrack: big });
|
|
140
|
-
const { read } = readerOver(file);
|
|
141
|
-
const tracks = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
142
|
-
assert.deepEqual(tracks.map((track) => track.trackNumber), [1, 3, 4]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("the language: LanguageBCP47 wins, then Language, then the format's default `eng`; `und` stays unknown", async () => {
|
|
146
|
-
const cases = [
|
|
147
|
-
{ entry: { language: null }, language: "eng", source: "default" },
|
|
148
|
-
{ entry: { language: "" }, language: "eng", source: "language" },
|
|
149
|
-
{ entry: { language: "rus" }, language: "rus", source: "language" },
|
|
150
|
-
{ entry: { language: null, languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
151
|
-
{ entry: { language: "por", languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
152
|
-
{ entry: { language: "und" }, language: "und", source: "language" }
|
|
153
|
-
];
|
|
154
|
-
for (const { entry, language, source } of cases) {
|
|
155
|
-
const { file } = buildMatroska({
|
|
156
|
-
tracks: [trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", ...entry })],
|
|
157
|
-
cues: null,
|
|
158
|
-
clusters: [{ ticks: 0, data: clusterData({ ticks: 0, blocks: [] }) }]
|
|
159
|
-
});
|
|
160
|
-
const { read } = readerOver(file);
|
|
161
|
-
const [track] = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
162
|
-
assert.equal(track.language, language, JSON.stringify(entry));
|
|
163
|
-
assert.equal(track.languageSource, source, JSON.stringify(entry));
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
/** @param {string} type @param {Buffer} payload */
|
|
168
|
-
function box(type, payload) {
|
|
169
|
-
const header = Buffer.alloc(8);
|
|
170
|
-
header.writeUInt32BE(8 + payload.length, 0);
|
|
171
|
-
header.write(type, 4, "latin1");
|
|
172
|
-
return Buffer.concat([header, payload]);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
test("an MP4 whose `moov` has not arrived keeps nothing; read again, it answers", async () => {
|
|
176
|
-
const mvhd = Buffer.alloc(100);
|
|
177
|
-
mvhd.writeUInt32BE(1000, 12); // timescale
|
|
178
|
-
mvhd.writeUInt32BE(5000, 16); // duration
|
|
179
|
-
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
180
|
-
const moov = box("moov", box("mvhd", mvhd));
|
|
181
|
-
const file = Buffer.concat([ftyp, moov]);
|
|
182
|
-
let moovHere = false;
|
|
183
|
-
const { read } = readerOver(file, { missing: (start) => !moovHere && start >= ftyp.length });
|
|
184
|
-
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
185
|
-
|
|
186
|
-
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
187
|
-
moovHere = true;
|
|
188
|
-
assert.equal((await container.readMediaInfo()).durationSeconds, 5);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("an MP4 `moov` larger than this reads whole is refused by name, not reported absent", async () => {
|
|
192
|
-
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
193
|
-
const header = Buffer.alloc(8);
|
|
194
|
-
header.writeUInt32BE(40 * 1024 * 1024, 0);
|
|
195
|
-
header.write("moov", 4, "latin1");
|
|
196
|
-
const file = Buffer.concat([ftyp, header, Buffer.alloc(64)]);
|
|
197
|
-
const { read } = readerOver(file);
|
|
198
|
-
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
199
|
-
assert.equal(await container.readSubtitlePlan(), null);
|
|
200
|
-
assert.deepEqual(container.moovRefused, { size: 40 * 1024 * 1024, limit: 32 * 1024 * 1024 });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("an AVI head that has not arrived leaves no media info behind", async () => {
|
|
204
|
-
const avih = Buffer.alloc(56);
|
|
205
|
-
avih.writeUInt32LE(40_000, 0); // 25 frames a second
|
|
206
|
-
avih.writeUInt32LE(250, 16); // ten seconds
|
|
207
|
-
const chunk = Buffer.concat([Buffer.from("avih", "latin1"), Buffer.from([56, 0, 0, 0]), avih]);
|
|
208
|
-
const list = Buffer.concat([Buffer.from("LIST", "latin1"), Buffer.alloc(4), Buffer.from("hdrl", "latin1"), chunk]);
|
|
209
|
-
list.writeUInt32LE(list.length - 8, 4);
|
|
210
|
-
const riff = Buffer.concat([Buffer.from("RIFF", "latin1"), Buffer.alloc(4), Buffer.from("AVI ", "latin1"), list]);
|
|
211
|
-
riff.writeUInt32LE(riff.length - 8, 4);
|
|
212
|
-
let here = false;
|
|
213
|
-
const { read } = readerOver(riff, { missing: () => !here });
|
|
214
|
-
const container = new AviContainer({ readRange: read, fileSize: riff.length });
|
|
215
|
-
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
216
|
-
here = true;
|
|
217
|
-
assert.equal((await container.readMediaInfo()).durationSeconds, 10);
|
|
218
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file A read whose bytes have not arrived is not an answer about the file.
|
|
3
|
+
*
|
|
4
|
+
* Field 2026-10-01: the subtitle plan of an episode was read while its Cues
|
|
5
|
+
* table was still downloading, the read was given up, and the plan kept "no
|
|
6
|
+
* clusters" for the life of the process — the track showed nothing for a whole
|
|
7
|
+
* session while the same table, read eleven seconds later for the keyframes,
|
|
8
|
+
* was there. The same shape was in the container choice, the track table, the
|
|
9
|
+
* keyframe table, MP4's `moov` and AVI's `idx1`. Every check here makes the
|
|
10
|
+
* bytes missing first and present afterwards, and asks the same object again.
|
|
11
|
+
*
|
|
12
|
+
* Fakes only: a buffer and a reader over it. No torrent, no thread.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
19
|
+
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
20
|
+
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
21
|
+
import { ContainerOrchestrator } from "../services/media/ContainerOrchestrator.js";
|
|
22
|
+
import { KeyframeTables } from "../services/media/KeyframeTables.js";
|
|
23
|
+
import { isUnavailable } from "../services/media/container/unavailable.js";
|
|
24
|
+
import {
|
|
25
|
+
ID,
|
|
26
|
+
buildMatroska,
|
|
27
|
+
clusterData,
|
|
28
|
+
cueBlock,
|
|
29
|
+
element,
|
|
30
|
+
readerOver,
|
|
31
|
+
stringElement,
|
|
32
|
+
trackEntry,
|
|
33
|
+
uintElement
|
|
34
|
+
} from "./helpers/matroska-file.js";
|
|
35
|
+
|
|
36
|
+
const quiet = { info() {}, warn() {} };
|
|
37
|
+
|
|
38
|
+
function episode({ subtitleLanguage = null, cuesBeforeClusters = false, cues = [1], extraTrack = null } = {}) {
|
|
39
|
+
const tracks = [
|
|
40
|
+
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "jpn" }),
|
|
41
|
+
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: subtitleLanguage })
|
|
42
|
+
];
|
|
43
|
+
if (extraTrack) {
|
|
44
|
+
tracks.push(extraTrack);
|
|
45
|
+
}
|
|
46
|
+
return buildMatroska({
|
|
47
|
+
tracks,
|
|
48
|
+
cues,
|
|
49
|
+
cuesBeforeClusters,
|
|
50
|
+
clusters: [
|
|
51
|
+
{ ticks: 0, data: clusterData({ ticks: 0, blocks: [cueBlock({ track: 3, relativeTicks: 100, durationTicks: 900, text: "one" })] }) },
|
|
52
|
+
{ ticks: 5000, data: clusterData({ ticks: 5000, blocks: [cueBlock({ track: 3, relativeTicks: 0, durationTicks: 900, text: "two" })] }) }
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("a subtitle plan read before the Cues table arrived is not kept; read again, it names the clusters", async () => {
|
|
58
|
+
const { file, cuesAt, clusterAt } = episode({ cues: [1, 3] });
|
|
59
|
+
let tailHere = false;
|
|
60
|
+
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
61
|
+
const container = new MatroskaContainer({ readRange: read, fileSize: file.length });
|
|
62
|
+
|
|
63
|
+
await assert.rejects(container.readSubtitlePlan(), (error) => isUnavailable(error));
|
|
64
|
+
|
|
65
|
+
tailHere = true;
|
|
66
|
+
const plan = await container.readSubtitlePlan();
|
|
67
|
+
assert.equal(plan.cuesState, "complete");
|
|
68
|
+
assert.deepEqual(plan.tracks[0].clusterPositions, clusterAt, "both clusters the table names for the track");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a read that returns fewer bytes than asked is not taken for the element", async () => {
|
|
72
|
+
const { file } = episode();
|
|
73
|
+
const { read } = readerOver(file, { short: true });
|
|
74
|
+
await assert.rejects(new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks(), (error) =>
|
|
75
|
+
isUnavailable(error)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a head that has not arrived leaves no container behind, and the next ask builds one", async () => {
|
|
80
|
+
const { file } = episode();
|
|
81
|
+
let headHere = false;
|
|
82
|
+
const { read } = readerOver(file, { missing: (start) => !headHere && start === 0 });
|
|
83
|
+
const containers = new ContainerOrchestrator();
|
|
84
|
+
const params = { sourceKey: "s", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
85
|
+
|
|
86
|
+
await assert.rejects(containers.containerFor(params), (error) => isUnavailable(error));
|
|
87
|
+
assert.equal(containers.known("s", 0), undefined, "nothing was kept for a head that is not here");
|
|
88
|
+
assert.deepEqual(await containers.getTracks(params), [], "the track table answers nothing yet");
|
|
89
|
+
|
|
90
|
+
headHere = true;
|
|
91
|
+
const container = await containers.containerFor(params);
|
|
92
|
+
assert.equal(container.formatName, "matroska");
|
|
93
|
+
assert.equal((await containers.getTracks(params)).length, 2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a keyframe table asked for while its Cues are missing stays unanswered, and is filled when they arrive", async () => {
|
|
97
|
+
const { file, cuesAt } = episode();
|
|
98
|
+
let tailHere = false;
|
|
99
|
+
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
100
|
+
const containers = new ContainerOrchestrator();
|
|
101
|
+
const params = { sourceKey: "k", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
102
|
+
const tables = new KeyframeTables({
|
|
103
|
+
readTable: async () => {
|
|
104
|
+
const index = await containers.getKeyframeIndex(params);
|
|
105
|
+
return { times: index?.times ?? null, tolerance: 0, format: "matroska" };
|
|
106
|
+
},
|
|
107
|
+
logger: quiet
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await assert.rejects(tables.read({ sourceKey: "k", fileIndex: 0 }), (error) => isUnavailable(error));
|
|
111
|
+
const table = tables.of({ sourceKey: "k", fileIndex: 0 });
|
|
112
|
+
assert.equal(table.answered, false, "a read that did not arrive is not an answer");
|
|
113
|
+
|
|
114
|
+
tailHere = true;
|
|
115
|
+
tables.readAgainIfUnanswered({ sourceKey: "k", fileIndex: 0 });
|
|
116
|
+
await tables.read({ sourceKey: "k", fileIndex: 0 });
|
|
117
|
+
assert.equal(table.answered, true);
|
|
118
|
+
assert.deepEqual(table.times, [0, 5], "the same object the sessions hold now carries the times");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a Cues element stored before the first cluster, with no SeekHead entry, is found", async () => {
|
|
122
|
+
const { file } = episode({ cuesBeforeClusters: true });
|
|
123
|
+
const { read } = readerOver(file);
|
|
124
|
+
const index = await new MatroskaContainer({ readRange: read, fileSize: file.length }).parseKeyframeIndex();
|
|
125
|
+
assert.deepEqual(index.times, [0, 5]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a Tracks element larger than the head window is read by its own size", async () => {
|
|
129
|
+
// A CodecPrivate of 80 KB pushes the end of Tracks past the 64 KB head.
|
|
130
|
+
const big = element(
|
|
131
|
+
ID.TRACK_ENTRY,
|
|
132
|
+
Buffer.concat([
|
|
133
|
+
uintElement(ID.TRACK_NUMBER, 4),
|
|
134
|
+
uintElement(ID.TRACK_TYPE, 17),
|
|
135
|
+
stringElement(ID.CODEC_ID, "S_TEXT/ASS"),
|
|
136
|
+
element(0x63a2, Buffer.alloc(80 * 1024, 0x20))
|
|
137
|
+
])
|
|
138
|
+
);
|
|
139
|
+
const { file } = episode({ extraTrack: big });
|
|
140
|
+
const { read } = readerOver(file);
|
|
141
|
+
const tracks = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
142
|
+
assert.deepEqual(tracks.map((track) => track.trackNumber), [1, 3, 4]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("the language: LanguageBCP47 wins, then Language, then the format's default `eng`; `und` stays unknown", async () => {
|
|
146
|
+
const cases = [
|
|
147
|
+
{ entry: { language: null }, language: "eng", source: "default" },
|
|
148
|
+
{ entry: { language: "" }, language: "eng", source: "language" },
|
|
149
|
+
{ entry: { language: "rus" }, language: "rus", source: "language" },
|
|
150
|
+
{ entry: { language: null, languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
151
|
+
{ entry: { language: "por", languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
152
|
+
{ entry: { language: "und" }, language: "und", source: "language" }
|
|
153
|
+
];
|
|
154
|
+
for (const { entry, language, source } of cases) {
|
|
155
|
+
const { file } = buildMatroska({
|
|
156
|
+
tracks: [trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", ...entry })],
|
|
157
|
+
cues: null,
|
|
158
|
+
clusters: [{ ticks: 0, data: clusterData({ ticks: 0, blocks: [] }) }]
|
|
159
|
+
});
|
|
160
|
+
const { read } = readerOver(file);
|
|
161
|
+
const [track] = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
162
|
+
assert.equal(track.language, language, JSON.stringify(entry));
|
|
163
|
+
assert.equal(track.languageSource, source, JSON.stringify(entry));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/** @param {string} type @param {Buffer} payload */
|
|
168
|
+
function box(type, payload) {
|
|
169
|
+
const header = Buffer.alloc(8);
|
|
170
|
+
header.writeUInt32BE(8 + payload.length, 0);
|
|
171
|
+
header.write(type, 4, "latin1");
|
|
172
|
+
return Buffer.concat([header, payload]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
test("an MP4 whose `moov` has not arrived keeps nothing; read again, it answers", async () => {
|
|
176
|
+
const mvhd = Buffer.alloc(100);
|
|
177
|
+
mvhd.writeUInt32BE(1000, 12); // timescale
|
|
178
|
+
mvhd.writeUInt32BE(5000, 16); // duration
|
|
179
|
+
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
180
|
+
const moov = box("moov", box("mvhd", mvhd));
|
|
181
|
+
const file = Buffer.concat([ftyp, moov]);
|
|
182
|
+
let moovHere = false;
|
|
183
|
+
const { read } = readerOver(file, { missing: (start) => !moovHere && start >= ftyp.length });
|
|
184
|
+
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
185
|
+
|
|
186
|
+
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
187
|
+
moovHere = true;
|
|
188
|
+
assert.equal((await container.readMediaInfo()).durationSeconds, 5);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("an MP4 `moov` larger than this reads whole is refused by name, not reported absent", async () => {
|
|
192
|
+
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
193
|
+
const header = Buffer.alloc(8);
|
|
194
|
+
header.writeUInt32BE(40 * 1024 * 1024, 0);
|
|
195
|
+
header.write("moov", 4, "latin1");
|
|
196
|
+
const file = Buffer.concat([ftyp, header, Buffer.alloc(64)]);
|
|
197
|
+
const { read } = readerOver(file);
|
|
198
|
+
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
199
|
+
assert.equal(await container.readSubtitlePlan(), null);
|
|
200
|
+
assert.deepEqual(container.moovRefused, { size: 40 * 1024 * 1024, limit: 32 * 1024 * 1024 });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("an AVI head that has not arrived leaves no media info behind", async () => {
|
|
204
|
+
const avih = Buffer.alloc(56);
|
|
205
|
+
avih.writeUInt32LE(40_000, 0); // 25 frames a second
|
|
206
|
+
avih.writeUInt32LE(250, 16); // ten seconds
|
|
207
|
+
const chunk = Buffer.concat([Buffer.from("avih", "latin1"), Buffer.from([56, 0, 0, 0]), avih]);
|
|
208
|
+
const list = Buffer.concat([Buffer.from("LIST", "latin1"), Buffer.alloc(4), Buffer.from("hdrl", "latin1"), chunk]);
|
|
209
|
+
list.writeUInt32LE(list.length - 8, 4);
|
|
210
|
+
const riff = Buffer.concat([Buffer.from("RIFF", "latin1"), Buffer.alloc(4), Buffer.from("AVI ", "latin1"), list]);
|
|
211
|
+
riff.writeUInt32LE(riff.length - 8, 4);
|
|
212
|
+
let here = false;
|
|
213
|
+
const { read } = readerOver(riff, { missing: () => !here });
|
|
214
|
+
const container = new AviContainer({ readRange: read, fileSize: riff.length });
|
|
215
|
+
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
216
|
+
here = true;
|
|
217
|
+
assert.equal((await container.readMediaInfo()).durationSeconds, 10);
|
|
218
|
+
});
|
package/test/resume-warm.test.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Where a viewer's resume position falls in the file, in bytes.
|
|
3
|
-
*
|
|
4
|
-
* The warm-up fetches a file's two edges, because that is what the codec probe
|
|
5
|
-
* reads. The region the VIEWER will resume at was asked for by nobody until the
|
|
6
|
-
* encoder opened its input — field 2026-09-03, 53 s after the Retry button on a
|
|
7
|
-
* cold torrent, and the piece it then needed took another 46.3 s to arrive.
|
|
8
|
-
* Turning the position into an offset is the only arithmetic in that path, so it
|
|
9
|
-
* is the only part with anything to get wrong.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
|
|
15
|
-
import { resumeByteOffset } from "../services/torrent/worker/resume-warm.js";
|
|
16
|
-
|
|
17
|
-
test("a position halfway through a film is halfway through its file", () => {
|
|
18
|
-
assert.equal(resumeByteOffset(1000, 100, 50), 500);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("the field case lands where the encoder went looking", () => {
|
|
22
|
-
// The measured session: 171.338 s into a 23:41 episode of 541 MB, and the
|
|
23
|
-
// read that blocked was at 63 MB.
|
|
24
|
-
const at = resumeByteOffset(541 * 1024 * 1024, 23 * 60 + 41, 171.338);
|
|
25
|
-
const megabytes = at / (1024 * 1024);
|
|
26
|
-
assert.ok(megabytes > 60 && megabytes < 68, `landed at ${megabytes.toFixed(1)}MB`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("a position past the end reads the end, rather than past it", () => {
|
|
30
|
-
const length = 1000;
|
|
31
|
-
const at = resumeByteOffset(length, 100, 10_000);
|
|
32
|
-
assert.equal(at, length - 1);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("nothing is known, nothing is guessed", () => {
|
|
36
|
-
assert.equal(resumeByteOffset(0, 100, 50), 0, "no file length");
|
|
37
|
-
assert.equal(resumeByteOffset(1000, 0, 50), 0, "no duration — the container did not declare one");
|
|
38
|
-
assert.equal(resumeByteOffset(1000, 100, 0), 0, "the viewer is at the beginning, where the edges already are");
|
|
39
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a viewer's resume position falls in the file, in bytes.
|
|
3
|
+
*
|
|
4
|
+
* The warm-up fetches a file's two edges, because that is what the codec probe
|
|
5
|
+
* reads. The region the VIEWER will resume at was asked for by nobody until the
|
|
6
|
+
* encoder opened its input — field 2026-09-03, 53 s after the Retry button on a
|
|
7
|
+
* cold torrent, and the piece it then needed took another 46.3 s to arrive.
|
|
8
|
+
* Turning the position into an offset is the only arithmetic in that path, so it
|
|
9
|
+
* is the only part with anything to get wrong.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
|
|
15
|
+
import { resumeByteOffset } from "../services/torrent/worker/resume-warm.js";
|
|
16
|
+
|
|
17
|
+
test("a position halfway through a film is halfway through its file", () => {
|
|
18
|
+
assert.equal(resumeByteOffset(1000, 100, 50), 500);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("the field case lands where the encoder went looking", () => {
|
|
22
|
+
// The measured session: 171.338 s into a 23:41 episode of 541 MB, and the
|
|
23
|
+
// read that blocked was at 63 MB.
|
|
24
|
+
const at = resumeByteOffset(541 * 1024 * 1024, 23 * 60 + 41, 171.338);
|
|
25
|
+
const megabytes = at / (1024 * 1024);
|
|
26
|
+
assert.ok(megabytes > 60 && megabytes < 68, `landed at ${megabytes.toFixed(1)}MB`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a position past the end reads the end, rather than past it", () => {
|
|
30
|
+
const length = 1000;
|
|
31
|
+
const at = resumeByteOffset(length, 100, 10_000);
|
|
32
|
+
assert.equal(at, length - 1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("nothing is known, nothing is guessed", () => {
|
|
36
|
+
assert.equal(resumeByteOffset(0, 100, 50), 0, "no file length");
|
|
37
|
+
assert.equal(resumeByteOffset(1000, 0, 50), 0, "no duration — the container did not declare one");
|
|
38
|
+
assert.equal(resumeByteOffset(1000, 100, 0), 0, "the viewer is at the beginning, where the edges already are");
|
|
39
|
+
});
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The drawing cannot disagree with the table.
|
|
3
|
-
*
|
|
4
|
-
* `docs/encode-run-state.md` is generated from `services/encode/encode-run-state.js`.
|
|
5
|
-
* A picture kept beside the code drifts — this repository has the receipts —
|
|
6
|
-
* so the check is mechanical: regenerate it and compare. A behavioural change
|
|
7
|
-
* to the table that forgets `npm run graph` fails here, in the same commit.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
|
|
14
|
-
import { GRAPH_DOC_PATH, renderRunGraphMarkdown } from "../scripts/render-run-graph.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Line endings are not part of the comparison. Git is configured with
|
|
18
|
-
* `autocrlf=true` on the machine this is developed on, so the same file is LF
|
|
19
|
-
* in the index and CRLF in the working tree — and a test failing on that would
|
|
20
|
-
* point at the generator, whose output is always LF, and be unfixable by the
|
|
21
|
-
* remedy it suggests.
|
|
22
|
-
*
|
|
23
|
-
* @param {string} text
|
|
24
|
-
* @returns {string}
|
|
25
|
-
*/
|
|
26
|
-
function withoutLineEndings(text) {
|
|
27
|
-
return text.replace(/\r\n/g, "\n");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
test("the committed picture is the one the table produces", () => {
|
|
31
|
-
const committed = readFileSync(GRAPH_DOC_PATH, "utf8");
|
|
32
|
-
assert.equal(
|
|
33
|
-
withoutLineEndings(committed),
|
|
34
|
-
withoutLineEndings(renderRunGraphMarkdown()),
|
|
35
|
-
"docs/encode-run-state.md is out of date — run `npm run graph` and commit the result"
|
|
36
|
-
);
|
|
37
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The drawing cannot disagree with the table.
|
|
3
|
+
*
|
|
4
|
+
* `docs/encode-run-state.md` is generated from `services/encode/encode-run-state.js`.
|
|
5
|
+
* A picture kept beside the code drifts — this repository has the receipts —
|
|
6
|
+
* so the check is mechanical: regenerate it and compare. A behavioural change
|
|
7
|
+
* to the table that forgets `npm run graph` fails here, in the same commit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { GRAPH_DOC_PATH, renderRunGraphMarkdown } from "../scripts/render-run-graph.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Line endings are not part of the comparison. Git is configured with
|
|
18
|
+
* `autocrlf=true` on the machine this is developed on, so the same file is LF
|
|
19
|
+
* in the index and CRLF in the working tree — and a test failing on that would
|
|
20
|
+
* point at the generator, whose output is always LF, and be unfixable by the
|
|
21
|
+
* remedy it suggests.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} text
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function withoutLineEndings(text) {
|
|
27
|
+
return text.replace(/\r\n/g, "\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("the committed picture is the one the table produces", () => {
|
|
31
|
+
const committed = readFileSync(GRAPH_DOC_PATH, "utf8");
|
|
32
|
+
assert.equal(
|
|
33
|
+
withoutLineEndings(committed),
|
|
34
|
+
withoutLineEndings(renderRunGraphMarkdown()),
|
|
35
|
+
"docs/encode-run-state.md is out of date — run `npm run graph` and commit the result"
|
|
36
|
+
);
|
|
37
|
+
});
|