@torrent-tv/proxy 2.89.7 → 2.89.8
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 +358 -338
- package/CHANGELOG.md +2340 -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 +1373 -1373
- 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 +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 +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +475 -475
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +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,159 +1,159 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Minimal EBML reader — the encoding Matroska/WebM are built from.
|
|
3
|
-
*
|
|
4
|
-
* Only what a keyframe index needs: walk elements, read unsigned integers,
|
|
5
|
-
* descend into containers. Deliberately not a general EBML implementation.
|
|
6
|
-
*
|
|
7
|
-
* Why not the `ebml` npm package (MIT, otherwise fine): its decoder is a
|
|
8
|
-
* *stream* decoder — it must be fed the file from byte zero. We read two small
|
|
9
|
-
* ranges out of a multi-gigabyte torrent-backed file and never the whole thing,
|
|
10
|
-
* so a parser that starts mid-file is exactly what we need and exactly what it
|
|
11
|
-
* cannot do (verified 2026-08-02: feeding it the Cues range throws
|
|
12
|
-
* "Unrepresentable length"). The element encoding itself is small enough to
|
|
13
|
-
* implement directly, so we do — the shape follows the Matroska specification
|
|
14
|
-
* and the same logic those libraries implement.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* A variable-length integer, as EBML encodes both element ids and sizes.
|
|
19
|
-
*
|
|
20
|
-
* The first set bit of the leading byte marks the width: `1xxxxxxx` is one
|
|
21
|
-
* byte, `01xxxxxx` two, and so on up to eight. For a SIZE the marker bit is
|
|
22
|
-
* removed and the remainder is the value; for an ID the bytes are kept intact,
|
|
23
|
-
* because the id *is* those bytes (that is how `0x1C53BB6B` identifies Cues).
|
|
24
|
-
*
|
|
25
|
-
* @param {Buffer} buffer
|
|
26
|
-
* @param {number} offset
|
|
27
|
-
* @param {boolean} keepMarker - True for ids, false for sizes.
|
|
28
|
-
* @returns {{ value: number, length: number } | null} Null when truncated or malformed.
|
|
29
|
-
*/
|
|
30
|
-
export function readVint(buffer, offset, keepMarker) {
|
|
31
|
-
if (offset >= buffer.length) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const first = buffer[offset];
|
|
35
|
-
if (first === 0) {
|
|
36
|
-
return null; // No marker bit in the first byte: not a valid vint start.
|
|
37
|
-
}
|
|
38
|
-
let length = 1;
|
|
39
|
-
let mask = 0x80;
|
|
40
|
-
while (length <= 8 && (first & mask) === 0) {
|
|
41
|
-
length += 1;
|
|
42
|
-
mask >>= 1;
|
|
43
|
-
}
|
|
44
|
-
if (length > 8 || offset + length > buffer.length) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
let value = keepMarker ? first : first & (mask - 1);
|
|
48
|
-
for (let index = 1; index < length; index += 1) {
|
|
49
|
-
// Values beyond 2^53 cannot be represented exactly; sizes and positions in
|
|
50
|
-
// real files stay far below that, so plain arithmetic is safe here.
|
|
51
|
-
value = value * 256 + buffer[offset + index];
|
|
52
|
-
}
|
|
53
|
-
return { value, length };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Iterate the elements directly inside `buffer`, without descending.
|
|
58
|
-
*
|
|
59
|
-
* Yields each element's id, the offset of its payload and its size. An element
|
|
60
|
-
* whose payload runs past the end of the buffer is still yielded (its header is
|
|
61
|
-
* intact and the caller may only need the id), but iteration stops after it.
|
|
62
|
-
*
|
|
63
|
-
* @param {Buffer} buffer
|
|
64
|
-
* @param {number} [start=0]
|
|
65
|
-
* @param {number} [end=buffer.length]
|
|
66
|
-
* @yields {{ id: number, dataOffset: number, size: number }}
|
|
67
|
-
*/
|
|
68
|
-
export function* iterateElements(buffer, start = 0, end = buffer.length) {
|
|
69
|
-
let offset = start;
|
|
70
|
-
while (offset < end) {
|
|
71
|
-
const id = readVint(buffer, offset, true);
|
|
72
|
-
if (!id) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const size = readVint(buffer, offset + id.length, false);
|
|
76
|
-
if (!size) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const dataOffset = offset + id.length + size.length;
|
|
80
|
-
yield { id: id.value, dataOffset, size: size.value };
|
|
81
|
-
if (dataOffset + size.value > end) {
|
|
82
|
-
return; // Truncated payload — nothing dependable follows it.
|
|
83
|
-
}
|
|
84
|
-
offset = dataOffset + size.value;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Read an EBML unsigned integer payload (big-endian, variable width).
|
|
90
|
-
*
|
|
91
|
-
* @param {Buffer} buffer
|
|
92
|
-
* @param {number} offset
|
|
93
|
-
* @param {number} size
|
|
94
|
-
* @returns {number}
|
|
95
|
-
*/
|
|
96
|
-
export function readUint(buffer, offset, size) {
|
|
97
|
-
let value = 0;
|
|
98
|
-
for (let index = 0; index < size; index += 1) {
|
|
99
|
-
value = value * 256 + buffer[offset + index];
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* An EBML float, which RFC 9559 §11.3.3 allows to be 4 or 8 bytes, big-endian
|
|
106
|
-
* IEEE 754, and 0 bytes for the value zero.
|
|
107
|
-
*
|
|
108
|
-
* Needed because Matroska writes `Duration` as a float, not as an integer:
|
|
109
|
-
* reading it with {@link readUint} yields the bit pattern rather than the
|
|
110
|
-
* number. Any other size is malformed and answered with null rather than a
|
|
111
|
-
* guess.
|
|
112
|
-
*
|
|
113
|
-
* @param {Buffer} buffer
|
|
114
|
-
* @param {number} offset
|
|
115
|
-
* @param {number} size
|
|
116
|
-
* @returns {number | null}
|
|
117
|
-
*/
|
|
118
|
-
export function readFloat(buffer, offset, size) {
|
|
119
|
-
if (size === 0) {
|
|
120
|
-
return 0;
|
|
121
|
-
}
|
|
122
|
-
if (offset + size > buffer.length) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
if (size === 4) {
|
|
126
|
-
return buffer.readFloatBE(offset);
|
|
127
|
-
}
|
|
128
|
-
if (size === 8) {
|
|
129
|
-
return buffer.readDoubleBE(offset);
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Depth-first search for the first element with `id`, descending only into the
|
|
136
|
-
* container ids listed in `descendInto`.
|
|
137
|
-
*
|
|
138
|
-
* @param {Buffer} buffer
|
|
139
|
-
* @param {number} id - Element id to find.
|
|
140
|
-
* @param {number[]} descendInto - Container ids worth entering.
|
|
141
|
-
* @param {number} [start=0]
|
|
142
|
-
* @param {number} [end=buffer.length]
|
|
143
|
-
* @returns {{ dataOffset: number, size: number } | null}
|
|
144
|
-
*/
|
|
145
|
-
export function findElement(buffer, id, descendInto, start = 0, end = buffer.length) {
|
|
146
|
-
for (const element of iterateElements(buffer, start, end)) {
|
|
147
|
-
if (element.id === id) {
|
|
148
|
-
return { dataOffset: element.dataOffset, size: element.size };
|
|
149
|
-
}
|
|
150
|
-
if (descendInto.includes(element.id)) {
|
|
151
|
-
const limit = Math.min(end, element.dataOffset + element.size);
|
|
152
|
-
const found = findElement(buffer, id, descendInto, element.dataOffset, limit);
|
|
153
|
-
if (found) {
|
|
154
|
-
return found;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Minimal EBML reader — the encoding Matroska/WebM are built from.
|
|
3
|
+
*
|
|
4
|
+
* Only what a keyframe index needs: walk elements, read unsigned integers,
|
|
5
|
+
* descend into containers. Deliberately not a general EBML implementation.
|
|
6
|
+
*
|
|
7
|
+
* Why not the `ebml` npm package (MIT, otherwise fine): its decoder is a
|
|
8
|
+
* *stream* decoder — it must be fed the file from byte zero. We read two small
|
|
9
|
+
* ranges out of a multi-gigabyte torrent-backed file and never the whole thing,
|
|
10
|
+
* so a parser that starts mid-file is exactly what we need and exactly what it
|
|
11
|
+
* cannot do (verified 2026-08-02: feeding it the Cues range throws
|
|
12
|
+
* "Unrepresentable length"). The element encoding itself is small enough to
|
|
13
|
+
* implement directly, so we do — the shape follows the Matroska specification
|
|
14
|
+
* and the same logic those libraries implement.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A variable-length integer, as EBML encodes both element ids and sizes.
|
|
19
|
+
*
|
|
20
|
+
* The first set bit of the leading byte marks the width: `1xxxxxxx` is one
|
|
21
|
+
* byte, `01xxxxxx` two, and so on up to eight. For a SIZE the marker bit is
|
|
22
|
+
* removed and the remainder is the value; for an ID the bytes are kept intact,
|
|
23
|
+
* because the id *is* those bytes (that is how `0x1C53BB6B` identifies Cues).
|
|
24
|
+
*
|
|
25
|
+
* @param {Buffer} buffer
|
|
26
|
+
* @param {number} offset
|
|
27
|
+
* @param {boolean} keepMarker - True for ids, false for sizes.
|
|
28
|
+
* @returns {{ value: number, length: number } | null} Null when truncated or malformed.
|
|
29
|
+
*/
|
|
30
|
+
export function readVint(buffer, offset, keepMarker) {
|
|
31
|
+
if (offset >= buffer.length) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const first = buffer[offset];
|
|
35
|
+
if (first === 0) {
|
|
36
|
+
return null; // No marker bit in the first byte: not a valid vint start.
|
|
37
|
+
}
|
|
38
|
+
let length = 1;
|
|
39
|
+
let mask = 0x80;
|
|
40
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
41
|
+
length += 1;
|
|
42
|
+
mask >>= 1;
|
|
43
|
+
}
|
|
44
|
+
if (length > 8 || offset + length > buffer.length) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
let value = keepMarker ? first : first & (mask - 1);
|
|
48
|
+
for (let index = 1; index < length; index += 1) {
|
|
49
|
+
// Values beyond 2^53 cannot be represented exactly; sizes and positions in
|
|
50
|
+
// real files stay far below that, so plain arithmetic is safe here.
|
|
51
|
+
value = value * 256 + buffer[offset + index];
|
|
52
|
+
}
|
|
53
|
+
return { value, length };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Iterate the elements directly inside `buffer`, without descending.
|
|
58
|
+
*
|
|
59
|
+
* Yields each element's id, the offset of its payload and its size. An element
|
|
60
|
+
* whose payload runs past the end of the buffer is still yielded (its header is
|
|
61
|
+
* intact and the caller may only need the id), but iteration stops after it.
|
|
62
|
+
*
|
|
63
|
+
* @param {Buffer} buffer
|
|
64
|
+
* @param {number} [start=0]
|
|
65
|
+
* @param {number} [end=buffer.length]
|
|
66
|
+
* @yields {{ id: number, dataOffset: number, size: number }}
|
|
67
|
+
*/
|
|
68
|
+
export function* iterateElements(buffer, start = 0, end = buffer.length) {
|
|
69
|
+
let offset = start;
|
|
70
|
+
while (offset < end) {
|
|
71
|
+
const id = readVint(buffer, offset, true);
|
|
72
|
+
if (!id) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const size = readVint(buffer, offset + id.length, false);
|
|
76
|
+
if (!size) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const dataOffset = offset + id.length + size.length;
|
|
80
|
+
yield { id: id.value, dataOffset, size: size.value };
|
|
81
|
+
if (dataOffset + size.value > end) {
|
|
82
|
+
return; // Truncated payload — nothing dependable follows it.
|
|
83
|
+
}
|
|
84
|
+
offset = dataOffset + size.value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read an EBML unsigned integer payload (big-endian, variable width).
|
|
90
|
+
*
|
|
91
|
+
* @param {Buffer} buffer
|
|
92
|
+
* @param {number} offset
|
|
93
|
+
* @param {number} size
|
|
94
|
+
* @returns {number}
|
|
95
|
+
*/
|
|
96
|
+
export function readUint(buffer, offset, size) {
|
|
97
|
+
let value = 0;
|
|
98
|
+
for (let index = 0; index < size; index += 1) {
|
|
99
|
+
value = value * 256 + buffer[offset + index];
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* An EBML float, which RFC 9559 §11.3.3 allows to be 4 or 8 bytes, big-endian
|
|
106
|
+
* IEEE 754, and 0 bytes for the value zero.
|
|
107
|
+
*
|
|
108
|
+
* Needed because Matroska writes `Duration` as a float, not as an integer:
|
|
109
|
+
* reading it with {@link readUint} yields the bit pattern rather than the
|
|
110
|
+
* number. Any other size is malformed and answered with null rather than a
|
|
111
|
+
* guess.
|
|
112
|
+
*
|
|
113
|
+
* @param {Buffer} buffer
|
|
114
|
+
* @param {number} offset
|
|
115
|
+
* @param {number} size
|
|
116
|
+
* @returns {number | null}
|
|
117
|
+
*/
|
|
118
|
+
export function readFloat(buffer, offset, size) {
|
|
119
|
+
if (size === 0) {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
if (offset + size > buffer.length) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (size === 4) {
|
|
126
|
+
return buffer.readFloatBE(offset);
|
|
127
|
+
}
|
|
128
|
+
if (size === 8) {
|
|
129
|
+
return buffer.readDoubleBE(offset);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Depth-first search for the first element with `id`, descending only into the
|
|
136
|
+
* container ids listed in `descendInto`.
|
|
137
|
+
*
|
|
138
|
+
* @param {Buffer} buffer
|
|
139
|
+
* @param {number} id - Element id to find.
|
|
140
|
+
* @param {number[]} descendInto - Container ids worth entering.
|
|
141
|
+
* @param {number} [start=0]
|
|
142
|
+
* @param {number} [end=buffer.length]
|
|
143
|
+
* @returns {{ dataOffset: number, size: number } | null}
|
|
144
|
+
*/
|
|
145
|
+
export function findElement(buffer, id, descendInto, start = 0, end = buffer.length) {
|
|
146
|
+
for (const element of iterateElements(buffer, start, end)) {
|
|
147
|
+
if (element.id === id) {
|
|
148
|
+
return { dataOffset: element.dataOffset, size: element.size };
|
|
149
|
+
}
|
|
150
|
+
if (descendInto.includes(element.id)) {
|
|
151
|
+
const limit = Math.min(end, element.dataOffset + element.size);
|
|
152
|
+
const found = findElement(buffer, id, descendInto, element.dataOffset, limit);
|
|
153
|
+
if (found) {
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file EBML elements read where they lie, a portion at a time.
|
|
3
|
-
*
|
|
4
|
-
* `ebml-reader.js` parses a buffer the caller already holds. That is enough for
|
|
5
|
-
* a head window, and it is not enough for the elements that matter here: a Cues
|
|
6
|
-
* table whose size is the file's business, a Tracks element pushed out of the
|
|
7
|
-
* head by a large CodecPrivate, a cluster of a high-bitrate film. Reading any
|
|
8
|
-
* of them "in one go" needs a bound, and a bound is a statement about this
|
|
9
|
-
* program dressed up as a statement about the file — an element over it was
|
|
10
|
-
* reported as absent.
|
|
11
|
-
*
|
|
12
|
-
* This reads element HEADERS one at a time and element DATA only when asked,
|
|
13
|
-
* through a window no larger than one portion. The portion is handed in by
|
|
14
|
-
* whoever knows the medium (a torrent's piece length) and is never chosen here.
|
|
15
|
-
* Skipping an element costs one header read; an element whose data is wanted
|
|
16
|
-
* and is larger than a portion is refused by name, with its size.
|
|
17
|
-
*
|
|
18
|
-
* Grammar only, like `ebml-reader.js`: nothing here knows Matroska's ids.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { readVint } from "./ebml-reader.js";
|
|
22
|
-
|
|
23
|
-
/** An element id is at most four bytes and a size at most eight (RFC 8794 §4). */
|
|
24
|
-
export const MAX_HEADER_BYTES = 12;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Whether a size field says "unknown" — every value bit set (RFC 8794 §6.2).
|
|
28
|
-
*
|
|
29
|
-
* @param {{ value: number, length: number }} size
|
|
30
|
-
* @returns {boolean}
|
|
31
|
-
*/
|
|
32
|
-
export function sizeIsUnknown(size) {
|
|
33
|
-
return size.value === 2 ** (7 * size.length) - 1;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @typedef {object} ElementHeader
|
|
38
|
-
* @property {number} id
|
|
39
|
-
* @property {number} at - Where the element begins, its id included.
|
|
40
|
-
* @property {number} dataOffset - Where its data begins.
|
|
41
|
-
* @property {number | null} size - Null when the size is unknown.
|
|
42
|
-
* @property {number | null} end - One past the last data byte, null when unknown.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
export class ElementReader {
|
|
46
|
-
/** @type {(start: number, end: number) => Promise<Buffer>} */
|
|
47
|
-
#read;
|
|
48
|
-
|
|
49
|
-
/** @type {number} */
|
|
50
|
-
#fileSize;
|
|
51
|
-
|
|
52
|
-
/** @type {number} */
|
|
53
|
-
#portion;
|
|
54
|
-
|
|
55
|
-
/** @type {boolean} */
|
|
56
|
-
#prefetch;
|
|
57
|
-
|
|
58
|
-
/** @type {(start: number) => number} */
|
|
59
|
-
#readableUntil;
|
|
60
|
-
|
|
61
|
-
/** @type {Buffer | null} */
|
|
62
|
-
#window = null;
|
|
63
|
-
|
|
64
|
-
/** @type {number} */
|
|
65
|
-
#windowAt = 0;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @param {object} params
|
|
69
|
-
* @param {(start: number, end: number) => Promise<Buffer>} params.read - Answers
|
|
70
|
-
* with every byte of the range or throws (`strictReader`).
|
|
71
|
-
* @param {number} params.fileSize
|
|
72
|
-
* @param {number} [params.portionBytes] - The largest read this makes. Absent,
|
|
73
|
-
* an element is read in one go, which is right for a file on a local disk.
|
|
74
|
-
* @param {boolean} [params.prefetch] - Read a whole portion on a miss. Right
|
|
75
|
-
* when every byte of a span will be looked at (a cluster being walked);
|
|
76
|
-
* wrong when only a few headers will (the elements of a Segment, where a
|
|
77
|
-
* fetching read of a portion per header would pull megabytes for nothing).
|
|
78
|
-
* @param {(start: number) => number} [params.readableUntil] - One past the
|
|
79
|
-
* last byte that can be read from `start` in one go. A read of what is
|
|
80
|
-
* downloaded must not run a prefetch past the end of the downloaded run,
|
|
81
|
-
* or a cluster that is whole would fail for the bytes after it.
|
|
82
|
-
*/
|
|
83
|
-
constructor({ read, fileSize, portionBytes = Number.POSITIVE_INFINITY, prefetch = false, readableUntil }) {
|
|
84
|
-
this.#read = read;
|
|
85
|
-
this.#fileSize = fileSize;
|
|
86
|
-
this.#readableUntil = typeof readableUntil === "function" ? readableUntil : () => fileSize;
|
|
87
|
-
this.#portion = Number.isFinite(portionBytes) && portionBytes > 0 ? portionBytes : Number.POSITIVE_INFINITY;
|
|
88
|
-
this.#prefetch = prefetch;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** @returns {number} */
|
|
92
|
-
get portionBytes() {
|
|
93
|
-
return this.#portion;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* `length` bytes from `start`, or fewer where the file ends first.
|
|
98
|
-
*
|
|
99
|
-
* @param {number} start
|
|
100
|
-
* @param {number} length
|
|
101
|
-
* @returns {Promise<Buffer>}
|
|
102
|
-
*/
|
|
103
|
-
async bytes(start, length) {
|
|
104
|
-
const end = Math.min(this.#fileSize, start + length);
|
|
105
|
-
if (end <= start) {
|
|
106
|
-
return Buffer.alloc(0);
|
|
107
|
-
}
|
|
108
|
-
const window = this.#window;
|
|
109
|
-
if (window && start >= this.#windowAt && end <= this.#windowAt + window.length) {
|
|
110
|
-
return window.subarray(start - this.#windowAt, end - this.#windowAt);
|
|
111
|
-
}
|
|
112
|
-
const reach = Math.min(this.#fileSize, Math.max(end, this.#readableUntil(start)));
|
|
113
|
-
const span = this.#prefetch ? Math.max(end - start, Math.min(this.#portion, reach - start)) : end - start;
|
|
114
|
-
const got = await this.#read(start, start + span - 1);
|
|
115
|
-
this.#window = got;
|
|
116
|
-
this.#windowAt = start;
|
|
117
|
-
return got.subarray(0, end - start);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* The element whose header begins at `at`.
|
|
122
|
-
*
|
|
123
|
-
* @param {number} at
|
|
124
|
-
* @param {number} limit - One past the last byte the element may occupy.
|
|
125
|
-
* @returns {Promise<ElementHeader | null>} Null when the bytes there are not
|
|
126
|
-
* an element header — a malformed vint, or no room for one.
|
|
127
|
-
*/
|
|
128
|
-
async header(at, limit) {
|
|
129
|
-
const room = Math.min(limit, this.#fileSize) - at;
|
|
130
|
-
if (room <= 1) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
const bytes = await this.bytes(at, Math.min(MAX_HEADER_BYTES, room));
|
|
134
|
-
const id = readVint(bytes, 0, true);
|
|
135
|
-
if (!id) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
const size = readVint(bytes, id.length, false);
|
|
139
|
-
if (!size) {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const dataOffset = at + id.length + size.length;
|
|
143
|
-
if (sizeIsUnknown(size)) {
|
|
144
|
-
return { id: id.value, at, dataOffset, size: null, end: null };
|
|
145
|
-
}
|
|
146
|
-
return { id: id.value, at, dataOffset, size: size.value, end: dataOffset + size.value };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* An element's data, copied out of the window so the window can move on.
|
|
151
|
-
*
|
|
152
|
-
* @param {ElementHeader} element
|
|
153
|
-
* @returns {Promise<Buffer | null>} Null when the data is larger than one
|
|
154
|
-
* portion: refused, never assembled.
|
|
155
|
-
*/
|
|
156
|
-
async data(element) {
|
|
157
|
-
if (element.size === null || element.size > this.#portion) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
return Buffer.from(await this.bytes(element.dataOffset, element.size));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* The first `length` bytes of an element's data, without reading the rest.
|
|
165
|
-
*
|
|
166
|
-
* @param {ElementHeader} element
|
|
167
|
-
* @param {number} length
|
|
168
|
-
* @returns {Promise<Buffer>}
|
|
169
|
-
*/
|
|
170
|
-
async peek(element, length) {
|
|
171
|
-
const available = element.size === null ? length : Math.min(length, element.size);
|
|
172
|
-
return this.bytes(element.dataOffset, available);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file EBML elements read where they lie, a portion at a time.
|
|
3
|
+
*
|
|
4
|
+
* `ebml-reader.js` parses a buffer the caller already holds. That is enough for
|
|
5
|
+
* a head window, and it is not enough for the elements that matter here: a Cues
|
|
6
|
+
* table whose size is the file's business, a Tracks element pushed out of the
|
|
7
|
+
* head by a large CodecPrivate, a cluster of a high-bitrate film. Reading any
|
|
8
|
+
* of them "in one go" needs a bound, and a bound is a statement about this
|
|
9
|
+
* program dressed up as a statement about the file — an element over it was
|
|
10
|
+
* reported as absent.
|
|
11
|
+
*
|
|
12
|
+
* This reads element HEADERS one at a time and element DATA only when asked,
|
|
13
|
+
* through a window no larger than one portion. The portion is handed in by
|
|
14
|
+
* whoever knows the medium (a torrent's piece length) and is never chosen here.
|
|
15
|
+
* Skipping an element costs one header read; an element whose data is wanted
|
|
16
|
+
* and is larger than a portion is refused by name, with its size.
|
|
17
|
+
*
|
|
18
|
+
* Grammar only, like `ebml-reader.js`: nothing here knows Matroska's ids.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readVint } from "./ebml-reader.js";
|
|
22
|
+
|
|
23
|
+
/** An element id is at most four bytes and a size at most eight (RFC 8794 §4). */
|
|
24
|
+
export const MAX_HEADER_BYTES = 12;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a size field says "unknown" — every value bit set (RFC 8794 §6.2).
|
|
28
|
+
*
|
|
29
|
+
* @param {{ value: number, length: number }} size
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
export function sizeIsUnknown(size) {
|
|
33
|
+
return size.value === 2 ** (7 * size.length) - 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} ElementHeader
|
|
38
|
+
* @property {number} id
|
|
39
|
+
* @property {number} at - Where the element begins, its id included.
|
|
40
|
+
* @property {number} dataOffset - Where its data begins.
|
|
41
|
+
* @property {number | null} size - Null when the size is unknown.
|
|
42
|
+
* @property {number | null} end - One past the last data byte, null when unknown.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export class ElementReader {
|
|
46
|
+
/** @type {(start: number, end: number) => Promise<Buffer>} */
|
|
47
|
+
#read;
|
|
48
|
+
|
|
49
|
+
/** @type {number} */
|
|
50
|
+
#fileSize;
|
|
51
|
+
|
|
52
|
+
/** @type {number} */
|
|
53
|
+
#portion;
|
|
54
|
+
|
|
55
|
+
/** @type {boolean} */
|
|
56
|
+
#prefetch;
|
|
57
|
+
|
|
58
|
+
/** @type {(start: number) => number} */
|
|
59
|
+
#readableUntil;
|
|
60
|
+
|
|
61
|
+
/** @type {Buffer | null} */
|
|
62
|
+
#window = null;
|
|
63
|
+
|
|
64
|
+
/** @type {number} */
|
|
65
|
+
#windowAt = 0;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {object} params
|
|
69
|
+
* @param {(start: number, end: number) => Promise<Buffer>} params.read - Answers
|
|
70
|
+
* with every byte of the range or throws (`strictReader`).
|
|
71
|
+
* @param {number} params.fileSize
|
|
72
|
+
* @param {number} [params.portionBytes] - The largest read this makes. Absent,
|
|
73
|
+
* an element is read in one go, which is right for a file on a local disk.
|
|
74
|
+
* @param {boolean} [params.prefetch] - Read a whole portion on a miss. Right
|
|
75
|
+
* when every byte of a span will be looked at (a cluster being walked);
|
|
76
|
+
* wrong when only a few headers will (the elements of a Segment, where a
|
|
77
|
+
* fetching read of a portion per header would pull megabytes for nothing).
|
|
78
|
+
* @param {(start: number) => number} [params.readableUntil] - One past the
|
|
79
|
+
* last byte that can be read from `start` in one go. A read of what is
|
|
80
|
+
* downloaded must not run a prefetch past the end of the downloaded run,
|
|
81
|
+
* or a cluster that is whole would fail for the bytes after it.
|
|
82
|
+
*/
|
|
83
|
+
constructor({ read, fileSize, portionBytes = Number.POSITIVE_INFINITY, prefetch = false, readableUntil }) {
|
|
84
|
+
this.#read = read;
|
|
85
|
+
this.#fileSize = fileSize;
|
|
86
|
+
this.#readableUntil = typeof readableUntil === "function" ? readableUntil : () => fileSize;
|
|
87
|
+
this.#portion = Number.isFinite(portionBytes) && portionBytes > 0 ? portionBytes : Number.POSITIVE_INFINITY;
|
|
88
|
+
this.#prefetch = prefetch;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @returns {number} */
|
|
92
|
+
get portionBytes() {
|
|
93
|
+
return this.#portion;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `length` bytes from `start`, or fewer where the file ends first.
|
|
98
|
+
*
|
|
99
|
+
* @param {number} start
|
|
100
|
+
* @param {number} length
|
|
101
|
+
* @returns {Promise<Buffer>}
|
|
102
|
+
*/
|
|
103
|
+
async bytes(start, length) {
|
|
104
|
+
const end = Math.min(this.#fileSize, start + length);
|
|
105
|
+
if (end <= start) {
|
|
106
|
+
return Buffer.alloc(0);
|
|
107
|
+
}
|
|
108
|
+
const window = this.#window;
|
|
109
|
+
if (window && start >= this.#windowAt && end <= this.#windowAt + window.length) {
|
|
110
|
+
return window.subarray(start - this.#windowAt, end - this.#windowAt);
|
|
111
|
+
}
|
|
112
|
+
const reach = Math.min(this.#fileSize, Math.max(end, this.#readableUntil(start)));
|
|
113
|
+
const span = this.#prefetch ? Math.max(end - start, Math.min(this.#portion, reach - start)) : end - start;
|
|
114
|
+
const got = await this.#read(start, start + span - 1);
|
|
115
|
+
this.#window = got;
|
|
116
|
+
this.#windowAt = start;
|
|
117
|
+
return got.subarray(0, end - start);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The element whose header begins at `at`.
|
|
122
|
+
*
|
|
123
|
+
* @param {number} at
|
|
124
|
+
* @param {number} limit - One past the last byte the element may occupy.
|
|
125
|
+
* @returns {Promise<ElementHeader | null>} Null when the bytes there are not
|
|
126
|
+
* an element header — a malformed vint, or no room for one.
|
|
127
|
+
*/
|
|
128
|
+
async header(at, limit) {
|
|
129
|
+
const room = Math.min(limit, this.#fileSize) - at;
|
|
130
|
+
if (room <= 1) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const bytes = await this.bytes(at, Math.min(MAX_HEADER_BYTES, room));
|
|
134
|
+
const id = readVint(bytes, 0, true);
|
|
135
|
+
if (!id) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const size = readVint(bytes, id.length, false);
|
|
139
|
+
if (!size) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const dataOffset = at + id.length + size.length;
|
|
143
|
+
if (sizeIsUnknown(size)) {
|
|
144
|
+
return { id: id.value, at, dataOffset, size: null, end: null };
|
|
145
|
+
}
|
|
146
|
+
return { id: id.value, at, dataOffset, size: size.value, end: dataOffset + size.value };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* An element's data, copied out of the window so the window can move on.
|
|
151
|
+
*
|
|
152
|
+
* @param {ElementHeader} element
|
|
153
|
+
* @returns {Promise<Buffer | null>} Null when the data is larger than one
|
|
154
|
+
* portion: refused, never assembled.
|
|
155
|
+
*/
|
|
156
|
+
async data(element) {
|
|
157
|
+
if (element.size === null || element.size > this.#portion) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return Buffer.from(await this.bytes(element.dataOffset, element.size));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The first `length` bytes of an element's data, without reading the rest.
|
|
165
|
+
*
|
|
166
|
+
* @param {ElementHeader} element
|
|
167
|
+
* @param {number} length
|
|
168
|
+
* @returns {Promise<Buffer>}
|
|
169
|
+
*/
|
|
170
|
+
async peek(element, length) {
|
|
171
|
+
const available = element.size === null ? length : Math.min(length, element.size);
|
|
172
|
+
return this.bytes(element.dataOffset, available);
|
|
173
|
+
}
|
|
174
|
+
}
|