@torrent-tv/proxy 2.88.3 → 2.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2259 -2250
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +23 -4
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1150 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -254
- package/services/encode/quality/output-format.js +319 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +298 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1244
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -0
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +995 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/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 +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +70 -70
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -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-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +270 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -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 +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -0
- 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 +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,324 +1,324 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Whether two init segments can serve one another's pieces.
|
|
3
|
-
*
|
|
4
|
-
* A player fetches `#EXT-X-MAP` once and decodes every fragment of that address
|
|
5
|
-
* against it. So a piece made under one header and served under another is only
|
|
6
|
-
* safe when the two headers say the same thing to a decoder — and "the same
|
|
7
|
-
* thing" has to be decided BEFORE any experiment, or the rule becomes a list of
|
|
8
|
-
* whatever differences happened to turn up while it was being written.
|
|
9
|
-
*
|
|
10
|
-
* THE RULE, conservative by construction:
|
|
11
|
-
*
|
|
12
|
-
* 1. the WHOLE header is compared, box by box, leaf by leaf, byte for byte;
|
|
13
|
-
* 2. every difference is reported by the path it sits at;
|
|
14
|
-
* 3. a difference makes the two headers INCOMPATIBLE unless it is covered by an
|
|
15
|
-
* explicitly listed exception;
|
|
16
|
-
* 4. **each exception carries the reason it is one.** A field nobody can name
|
|
17
|
-
* is not an exception — an unreadable or unexpected difference refuses.
|
|
18
|
-
*
|
|
19
|
-
* What this gives up deliberately: it will refuse pairs that would in fact have
|
|
20
|
-
* played. That is the direction to be wrong in — the other direction is a
|
|
21
|
-
* viewer whose picture turns to blocks, which this project has already paid for
|
|
22
|
-
* once (`research/budget-downshift-breaks-the-picture-2026-08-21.md`).
|
|
23
|
-
*
|
|
24
|
-
* Only a format that HAS an init is described here. MPEG-TS carries its
|
|
25
|
-
* parameter sets in the stream and has no header to compare, so two of its
|
|
26
|
-
* outputs are never joined by this question at all.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { walkBoxes } from "./mp4-boxes.js";
|
|
30
|
-
|
|
31
|
-
/** Boxes whose payload is a list of child boxes, mirroring `walkBoxes`. */
|
|
32
|
-
const CONTAINER_BOXES = new Set(["moov", "trak", "mdia", "minf", "stbl", "edts", "mvex"]);
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Sample entries, and how many bytes of fixed preamble stand before their child
|
|
36
|
-
* boxes.
|
|
37
|
-
*
|
|
38
|
-
* A sample entry is not a plain container: it opens with fields at fixed
|
|
39
|
-
* offsets and only then carries boxes. The two shapes are the ones ISO/IEC
|
|
40
|
-
* 14496-12 defines — `SampleEntry` is 8 bytes (six reserved plus a data
|
|
41
|
-
* reference index), `VisualSampleEntry` adds 70 more and `AudioSampleEntry`
|
|
42
|
-
* adds 20.
|
|
43
|
-
*
|
|
44
|
-
* WHY THIS MATTERS ENOUGH TO PARSE: without descending here, a difference
|
|
45
|
-
* anywhere inside a sample entry is reported as one difference of the whole
|
|
46
|
-
* `stsd`. Allowing it would allow `avcC` — the parameter sets — with it, which
|
|
47
|
-
* is the one thing this rule exists to refuse. The preamble is a stated
|
|
48
|
-
* constant and a wrong one would mis-read every child, so the descent is
|
|
49
|
-
* CHECKED rather than trusted: see `tilesExactly`.
|
|
50
|
-
*
|
|
51
|
-
* @type {ReadonlyMap<string, number>}
|
|
52
|
-
*/
|
|
53
|
-
const SAMPLE_ENTRY_PREAMBLE = new Map([
|
|
54
|
-
["avc1", 78], ["avc3", 78], ["hvc1", 78], ["hev1", 78], ["av01", 78], ["vp09", 78],
|
|
55
|
-
["mp4a", 28], ["ac-3", 28], ["ec-3", 28], ["opus", 28], ["fLaC", 28]
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
/** `stsd` is a FullBox: version and flags, then an entry count, then entries. */
|
|
59
|
-
const SAMPLE_DESCRIPTION_PREAMBLE = 8;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The differences that do NOT refuse, each with the reason it does not.
|
|
63
|
-
*
|
|
64
|
-
* An entry is either an exact `path`, or a `sampleEntryBox` — a box type
|
|
65
|
-
* appearing directly inside a sample entry, whichever track and whichever
|
|
66
|
-
* entry. The second form exists because the reason is a property of the BOX
|
|
67
|
-
* and not of where it sits: a file with the picture on the second track states
|
|
68
|
-
* the same thing about it.
|
|
69
|
-
*
|
|
70
|
-
* @type {ReadonlyArray<{ path?: string, sampleEntryBox?: string, why: string }>}
|
|
71
|
-
*/
|
|
72
|
-
export const INIT_DIFFERENCE_EXCEPTIONS = [
|
|
73
|
-
{
|
|
74
|
-
sampleEntryBox: "btrt",
|
|
75
|
-
why:
|
|
76
|
-
"BitRateBox declares a decoding buffer size and a maximum and average rate. " +
|
|
77
|
-
"Four readings, none of them from this experiment's own output: the W3C " +
|
|
78
|
-
"Media Source Extensions byte stream format for ISOBMFF, which is what " +
|
|
79
|
-
"governs a browser's handling of an initialization segment, does not " +
|
|
80
|
-
"mention the box at all and requires only `ftyp` and `moov`; hls.js — the " +
|
|
81
|
-
"player this product ships — never reads one, and writes a HARDCODED " +
|
|
82
|
-
"3 Mbit/s into every init it builds for every stream whatever its real " +
|
|
83
|
-
"rate (`MP4.types.btrt`, the avc1 and hvc1 sample entries); this proxy's " +
|
|
84
|
-
"own code neither reads nor writes it; and what actually configures a " +
|
|
85
|
-
"decoder, `avcC`, is compared separately by this same rule and a " +
|
|
86
|
-
"difference there still refuses."
|
|
87
|
-
}
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Where a box's child boxes begin, counted from the start of its payload, or
|
|
92
|
-
* null when the box is a leaf.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} type
|
|
95
|
-
* @returns {number | null}
|
|
96
|
-
*/
|
|
97
|
-
function childrenStartOf(type) {
|
|
98
|
-
if (CONTAINER_BOXES.has(type)) {
|
|
99
|
-
return 0;
|
|
100
|
-
}
|
|
101
|
-
if (type === "stsd") {
|
|
102
|
-
return SAMPLE_DESCRIPTION_PREAMBLE;
|
|
103
|
-
}
|
|
104
|
-
return SAMPLE_ENTRY_PREAMBLE.get(type) ?? null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Whether the range holds whole boxes and nothing else.
|
|
109
|
-
*
|
|
110
|
-
* The descent above rests on stated offsets, and a stated offset can be wrong —
|
|
111
|
-
* for a sample entry shape this does not know, or a box carrying a 64-bit size.
|
|
112
|
-
* Rather than mis-read the children, the walk asks first whether they tile the
|
|
113
|
-
* range exactly. They do not, and the parent is compared whole, which is the
|
|
114
|
-
* conservative answer.
|
|
115
|
-
*
|
|
116
|
-
* Size handling mirrors `walkBoxes`, so what this accepts is exactly what that
|
|
117
|
-
* would then walk.
|
|
118
|
-
*
|
|
119
|
-
* @param {Buffer} buffer
|
|
120
|
-
* @param {number} start
|
|
121
|
-
* @param {number} end
|
|
122
|
-
* @returns {boolean}
|
|
123
|
-
*/
|
|
124
|
-
function tilesExactly(buffer, start, end) {
|
|
125
|
-
if (!(end > start)) {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
let offset = start;
|
|
129
|
-
while (offset + 8 <= end) {
|
|
130
|
-
let size = buffer.readUInt32BE(offset);
|
|
131
|
-
let headerSize = 8;
|
|
132
|
-
if (size === 1) {
|
|
133
|
-
if (offset + 16 > end) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
size = Number(buffer.readBigUInt64BE(offset + 8));
|
|
137
|
-
headerSize = 16;
|
|
138
|
-
} else if (size === 0) {
|
|
139
|
-
size = end - offset;
|
|
140
|
-
}
|
|
141
|
-
if (size < headerSize || offset + size > end) {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
offset += size;
|
|
145
|
-
}
|
|
146
|
-
return offset === end;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Every leaf of the box tree, by the path it sits at.
|
|
151
|
-
*
|
|
152
|
-
* A path is `type` names joined by `/`, each carrying its index among its
|
|
153
|
-
* siblings OF THE SAME TYPE — so two tracks are `moov/trak[0]` and
|
|
154
|
-
* `moov/trak[1]` and a field of one is never compared against the other's.
|
|
155
|
-
*
|
|
156
|
-
* @param {Buffer} initSegment
|
|
157
|
-
* @returns {Map<string, Buffer>} path → the leaf's payload bytes
|
|
158
|
-
*/
|
|
159
|
-
export function initLeaves(initSegment) {
|
|
160
|
-
/** @type {Map<string, Buffer>} */
|
|
161
|
-
const leaves = new Map();
|
|
162
|
-
if (!Buffer.isBuffer(initSegment) || initSegment.length === 0) {
|
|
163
|
-
return leaves;
|
|
164
|
-
}
|
|
165
|
-
const walk = (start, end, prefix) => {
|
|
166
|
-
/** @type {Map<string, number>} */
|
|
167
|
-
const seen = new Map();
|
|
168
|
-
// `walkBoxes` recurses into containers itself, so this pass is given one
|
|
169
|
-
// level at a time and asks for the children of each container explicitly.
|
|
170
|
-
// That is what makes the path unambiguous: a bare walk would report a
|
|
171
|
-
// grandchild without saying whose it is.
|
|
172
|
-
walkBoxes(
|
|
173
|
-
initSegment,
|
|
174
|
-
(type, bodyStart, bodyEnd) => {
|
|
175
|
-
const index = seen.get(type) ?? 0;
|
|
176
|
-
seen.set(type, index + 1);
|
|
177
|
-
const path = `${prefix}${type}[${index}]`;
|
|
178
|
-
const childrenAt = childrenStartOf(type);
|
|
179
|
-
if (childrenAt !== null && tilesExactly(initSegment, bodyStart + childrenAt, bodyEnd)) {
|
|
180
|
-
walk(bodyStart + childrenAt, bodyEnd, `${path}/`);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
leaves.set(path, initSegment.subarray(bodyStart, bodyEnd));
|
|
184
|
-
},
|
|
185
|
-
start,
|
|
186
|
-
end,
|
|
187
|
-
// One level only: the recursion above is this function's own, and it is
|
|
188
|
-
// what makes a path say whose child a box is.
|
|
189
|
-
() => false
|
|
190
|
-
);
|
|
191
|
-
return leaves;
|
|
192
|
-
};
|
|
193
|
-
walk(0, initSegment.length, "");
|
|
194
|
-
return leaves;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* The box type a path's last step names, without its index.
|
|
199
|
-
*
|
|
200
|
-
* @param {string} path
|
|
201
|
-
* @param {number} [fromEnd=0] - 0 is the leaf, 1 its parent.
|
|
202
|
-
* @returns {string}
|
|
203
|
-
*/
|
|
204
|
-
function stepOf(path, fromEnd = 0) {
|
|
205
|
-
const steps = path.split("/");
|
|
206
|
-
const step = steps[steps.length - 1 - fromEnd] ?? "";
|
|
207
|
-
return step.replace(/\[\d+\]$/, "");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Whether an exception covers this path.
|
|
212
|
-
*
|
|
213
|
-
* @param {ReadonlyArray<{ path?: string, sampleEntryBox?: string }>} exceptions
|
|
214
|
-
* @param {string} path
|
|
215
|
-
* @returns {boolean}
|
|
216
|
-
*/
|
|
217
|
-
function permits(exceptions, path) {
|
|
218
|
-
for (const exception of exceptions) {
|
|
219
|
-
if (exception.path !== undefined && exception.path === path) {
|
|
220
|
-
return true;
|
|
221
|
-
}
|
|
222
|
-
if (
|
|
223
|
-
exception.sampleEntryBox !== undefined &&
|
|
224
|
-
stepOf(path) === exception.sampleEntryBox &&
|
|
225
|
-
SAMPLE_ENTRY_PREAMBLE.has(stepOf(path, 1))
|
|
226
|
-
) {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* What a difference at this path amounts to, in words.
|
|
235
|
-
*
|
|
236
|
-
* Decoding is for READING the report, never for deciding it: the verdict is the
|
|
237
|
-
* byte comparison, and a path this cannot decode is still a difference.
|
|
238
|
-
*
|
|
239
|
-
* @param {string} path
|
|
240
|
-
* @param {Buffer | null} left
|
|
241
|
-
* @param {Buffer | null} right
|
|
242
|
-
* @returns {string}
|
|
243
|
-
*/
|
|
244
|
-
function describeDifference(path, left, right) {
|
|
245
|
-
if (!left) return `${path}: absent from the first header`;
|
|
246
|
-
if (!right) return `${path}: absent from the second header`;
|
|
247
|
-
if (stepOf(path) === "btrt" && left.length >= 12 && right.length >= 12) {
|
|
248
|
-
const rates = (buffer) =>
|
|
249
|
-
`buffer ${buffer.readUInt32BE(0)}B, max ${buffer.readUInt32BE(4)}, avg ${buffer.readUInt32BE(8)} bit/s`;
|
|
250
|
-
return `${path}: ${rates(left)} against ${rates(right)}`;
|
|
251
|
-
}
|
|
252
|
-
if (path.includes("mdhd")) {
|
|
253
|
-
// FullBox: version+flags, then creation/modification, then timescale.
|
|
254
|
-
const at = left.readUInt8(0) === 1 ? 20 : 12;
|
|
255
|
-
if (left.length > at + 4 && right.length > at + 4) {
|
|
256
|
-
return `${path}: timescale ${left.readUInt32BE(at)} against ${right.readUInt32BE(at)}`;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (stepOf(path) === "stsd") {
|
|
260
|
-
// Reached only where the entries did not tile, so the walk could not
|
|
261
|
-
// descend. The entry's own type is at a fixed place and is worth naming.
|
|
262
|
-
const entryType = (buffer) => (buffer.length >= 16 ? buffer.toString("latin1", 12, 16) : "?");
|
|
263
|
-
const left4 = entryType(left);
|
|
264
|
-
const right4 = entryType(right);
|
|
265
|
-
const kind = SAMPLE_ENTRY_PREAMBLE.has(left4) || SAMPLE_ENTRY_PREAMBLE.has(right4) ? "" : " (unrecognised)";
|
|
266
|
-
return `${path}: sample entry ${left4} against ${right4}${kind}, ${left.length} bytes against ${right.length}`;
|
|
267
|
-
}
|
|
268
|
-
if (left.length !== right.length) {
|
|
269
|
-
return `${path}: ${left.length} bytes against ${right.length}`;
|
|
270
|
-
}
|
|
271
|
-
let firstDifferent = 0;
|
|
272
|
-
while (firstDifferent < left.length && left[firstDifferent] === right[firstDifferent]) {
|
|
273
|
-
firstDifferent += 1;
|
|
274
|
-
}
|
|
275
|
-
return `${path}: differs from byte ${firstDifferent}`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Whether a piece made under `made` may be served under `served`.
|
|
280
|
-
*
|
|
281
|
-
* @param {Buffer} served - The header the player already holds.
|
|
282
|
-
* @param {Buffer} made - The header the other output produces under.
|
|
283
|
-
* @param {{ exceptions?: ReadonlyArray<{ path?: string, sampleEntryBox?: string }> }} [options]
|
|
284
|
-
* @returns {{ compatible: boolean, differences: string[], refusedPaths: string[] }}
|
|
285
|
-
*/
|
|
286
|
-
export function compareInits(served, made, { exceptions = INIT_DIFFERENCE_EXCEPTIONS } = {}) {
|
|
287
|
-
if (!Buffer.isBuffer(served) || !Buffer.isBuffer(made) || served.length === 0 || made.length === 0) {
|
|
288
|
-
return {
|
|
289
|
-
compatible: false,
|
|
290
|
-
differences: ["one of the two headers is missing"],
|
|
291
|
-
refusedPaths: ["<missing>"]
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
if (served.equals(made)) {
|
|
295
|
-
return { compatible: true, differences: [], refusedPaths: [] };
|
|
296
|
-
}
|
|
297
|
-
const left = initLeaves(served);
|
|
298
|
-
const right = initLeaves(made);
|
|
299
|
-
/** @type {string[]} */
|
|
300
|
-
const differences = [];
|
|
301
|
-
/** @type {string[]} */
|
|
302
|
-
const refusedPaths = [];
|
|
303
|
-
const paths = new Set([...left.keys(), ...right.keys()]);
|
|
304
|
-
for (const path of paths) {
|
|
305
|
-
const a = left.get(path) ?? null;
|
|
306
|
-
const b = right.get(path) ?? null;
|
|
307
|
-
if (a && b && a.equals(b)) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
differences.push(describeDifference(path, a, b));
|
|
311
|
-
if (!permits(exceptions, path)) {
|
|
312
|
-
refusedPaths.push(path);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (differences.length === 0) {
|
|
316
|
-
// The bytes differ but every leaf agrees, so what moved is a box header —
|
|
317
|
-
// a size field, or a box present in one tree and absent from the other at a
|
|
318
|
-
// level this walk treats as a container. Not attributable, therefore not
|
|
319
|
-
// permitted.
|
|
320
|
-
differences.push("the headers differ outside any leaf this walk can name");
|
|
321
|
-
refusedPaths.push("<unattributed>");
|
|
322
|
-
}
|
|
323
|
-
return { compatible: refusedPaths.length === 0, differences, refusedPaths };
|
|
324
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether two init segments can serve one another's pieces.
|
|
3
|
+
*
|
|
4
|
+
* A player fetches `#EXT-X-MAP` once and decodes every fragment of that address
|
|
5
|
+
* against it. So a piece made under one header and served under another is only
|
|
6
|
+
* safe when the two headers say the same thing to a decoder — and "the same
|
|
7
|
+
* thing" has to be decided BEFORE any experiment, or the rule becomes a list of
|
|
8
|
+
* whatever differences happened to turn up while it was being written.
|
|
9
|
+
*
|
|
10
|
+
* THE RULE, conservative by construction:
|
|
11
|
+
*
|
|
12
|
+
* 1. the WHOLE header is compared, box by box, leaf by leaf, byte for byte;
|
|
13
|
+
* 2. every difference is reported by the path it sits at;
|
|
14
|
+
* 3. a difference makes the two headers INCOMPATIBLE unless it is covered by an
|
|
15
|
+
* explicitly listed exception;
|
|
16
|
+
* 4. **each exception carries the reason it is one.** A field nobody can name
|
|
17
|
+
* is not an exception — an unreadable or unexpected difference refuses.
|
|
18
|
+
*
|
|
19
|
+
* What this gives up deliberately: it will refuse pairs that would in fact have
|
|
20
|
+
* played. That is the direction to be wrong in — the other direction is a
|
|
21
|
+
* viewer whose picture turns to blocks, which this project has already paid for
|
|
22
|
+
* once (`research/budget-downshift-breaks-the-picture-2026-08-21.md`).
|
|
23
|
+
*
|
|
24
|
+
* Only a format that HAS an init is described here. MPEG-TS carries its
|
|
25
|
+
* parameter sets in the stream and has no header to compare, so two of its
|
|
26
|
+
* outputs are never joined by this question at all.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { walkBoxes } from "./mp4-boxes.js";
|
|
30
|
+
|
|
31
|
+
/** Boxes whose payload is a list of child boxes, mirroring `walkBoxes`. */
|
|
32
|
+
const CONTAINER_BOXES = new Set(["moov", "trak", "mdia", "minf", "stbl", "edts", "mvex"]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sample entries, and how many bytes of fixed preamble stand before their child
|
|
36
|
+
* boxes.
|
|
37
|
+
*
|
|
38
|
+
* A sample entry is not a plain container: it opens with fields at fixed
|
|
39
|
+
* offsets and only then carries boxes. The two shapes are the ones ISO/IEC
|
|
40
|
+
* 14496-12 defines — `SampleEntry` is 8 bytes (six reserved plus a data
|
|
41
|
+
* reference index), `VisualSampleEntry` adds 70 more and `AudioSampleEntry`
|
|
42
|
+
* adds 20.
|
|
43
|
+
*
|
|
44
|
+
* WHY THIS MATTERS ENOUGH TO PARSE: without descending here, a difference
|
|
45
|
+
* anywhere inside a sample entry is reported as one difference of the whole
|
|
46
|
+
* `stsd`. Allowing it would allow `avcC` — the parameter sets — with it, which
|
|
47
|
+
* is the one thing this rule exists to refuse. The preamble is a stated
|
|
48
|
+
* constant and a wrong one would mis-read every child, so the descent is
|
|
49
|
+
* CHECKED rather than trusted: see `tilesExactly`.
|
|
50
|
+
*
|
|
51
|
+
* @type {ReadonlyMap<string, number>}
|
|
52
|
+
*/
|
|
53
|
+
const SAMPLE_ENTRY_PREAMBLE = new Map([
|
|
54
|
+
["avc1", 78], ["avc3", 78], ["hvc1", 78], ["hev1", 78], ["av01", 78], ["vp09", 78],
|
|
55
|
+
["mp4a", 28], ["ac-3", 28], ["ec-3", 28], ["opus", 28], ["fLaC", 28]
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** `stsd` is a FullBox: version and flags, then an entry count, then entries. */
|
|
59
|
+
const SAMPLE_DESCRIPTION_PREAMBLE = 8;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The differences that do NOT refuse, each with the reason it does not.
|
|
63
|
+
*
|
|
64
|
+
* An entry is either an exact `path`, or a `sampleEntryBox` — a box type
|
|
65
|
+
* appearing directly inside a sample entry, whichever track and whichever
|
|
66
|
+
* entry. The second form exists because the reason is a property of the BOX
|
|
67
|
+
* and not of where it sits: a file with the picture on the second track states
|
|
68
|
+
* the same thing about it.
|
|
69
|
+
*
|
|
70
|
+
* @type {ReadonlyArray<{ path?: string, sampleEntryBox?: string, why: string }>}
|
|
71
|
+
*/
|
|
72
|
+
export const INIT_DIFFERENCE_EXCEPTIONS = [
|
|
73
|
+
{
|
|
74
|
+
sampleEntryBox: "btrt",
|
|
75
|
+
why:
|
|
76
|
+
"BitRateBox declares a decoding buffer size and a maximum and average rate. " +
|
|
77
|
+
"Four readings, none of them from this experiment's own output: the W3C " +
|
|
78
|
+
"Media Source Extensions byte stream format for ISOBMFF, which is what " +
|
|
79
|
+
"governs a browser's handling of an initialization segment, does not " +
|
|
80
|
+
"mention the box at all and requires only `ftyp` and `moov`; hls.js — the " +
|
|
81
|
+
"player this product ships — never reads one, and writes a HARDCODED " +
|
|
82
|
+
"3 Mbit/s into every init it builds for every stream whatever its real " +
|
|
83
|
+
"rate (`MP4.types.btrt`, the avc1 and hvc1 sample entries); this proxy's " +
|
|
84
|
+
"own code neither reads nor writes it; and what actually configures a " +
|
|
85
|
+
"decoder, `avcC`, is compared separately by this same rule and a " +
|
|
86
|
+
"difference there still refuses."
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Where a box's child boxes begin, counted from the start of its payload, or
|
|
92
|
+
* null when the box is a leaf.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} type
|
|
95
|
+
* @returns {number | null}
|
|
96
|
+
*/
|
|
97
|
+
function childrenStartOf(type) {
|
|
98
|
+
if (CONTAINER_BOXES.has(type)) {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (type === "stsd") {
|
|
102
|
+
return SAMPLE_DESCRIPTION_PREAMBLE;
|
|
103
|
+
}
|
|
104
|
+
return SAMPLE_ENTRY_PREAMBLE.get(type) ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether the range holds whole boxes and nothing else.
|
|
109
|
+
*
|
|
110
|
+
* The descent above rests on stated offsets, and a stated offset can be wrong —
|
|
111
|
+
* for a sample entry shape this does not know, or a box carrying a 64-bit size.
|
|
112
|
+
* Rather than mis-read the children, the walk asks first whether they tile the
|
|
113
|
+
* range exactly. They do not, and the parent is compared whole, which is the
|
|
114
|
+
* conservative answer.
|
|
115
|
+
*
|
|
116
|
+
* Size handling mirrors `walkBoxes`, so what this accepts is exactly what that
|
|
117
|
+
* would then walk.
|
|
118
|
+
*
|
|
119
|
+
* @param {Buffer} buffer
|
|
120
|
+
* @param {number} start
|
|
121
|
+
* @param {number} end
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function tilesExactly(buffer, start, end) {
|
|
125
|
+
if (!(end > start)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
let offset = start;
|
|
129
|
+
while (offset + 8 <= end) {
|
|
130
|
+
let size = buffer.readUInt32BE(offset);
|
|
131
|
+
let headerSize = 8;
|
|
132
|
+
if (size === 1) {
|
|
133
|
+
if (offset + 16 > end) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
size = Number(buffer.readBigUInt64BE(offset + 8));
|
|
137
|
+
headerSize = 16;
|
|
138
|
+
} else if (size === 0) {
|
|
139
|
+
size = end - offset;
|
|
140
|
+
}
|
|
141
|
+
if (size < headerSize || offset + size > end) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
offset += size;
|
|
145
|
+
}
|
|
146
|
+
return offset === end;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Every leaf of the box tree, by the path it sits at.
|
|
151
|
+
*
|
|
152
|
+
* A path is `type` names joined by `/`, each carrying its index among its
|
|
153
|
+
* siblings OF THE SAME TYPE — so two tracks are `moov/trak[0]` and
|
|
154
|
+
* `moov/trak[1]` and a field of one is never compared against the other's.
|
|
155
|
+
*
|
|
156
|
+
* @param {Buffer} initSegment
|
|
157
|
+
* @returns {Map<string, Buffer>} path → the leaf's payload bytes
|
|
158
|
+
*/
|
|
159
|
+
export function initLeaves(initSegment) {
|
|
160
|
+
/** @type {Map<string, Buffer>} */
|
|
161
|
+
const leaves = new Map();
|
|
162
|
+
if (!Buffer.isBuffer(initSegment) || initSegment.length === 0) {
|
|
163
|
+
return leaves;
|
|
164
|
+
}
|
|
165
|
+
const walk = (start, end, prefix) => {
|
|
166
|
+
/** @type {Map<string, number>} */
|
|
167
|
+
const seen = new Map();
|
|
168
|
+
// `walkBoxes` recurses into containers itself, so this pass is given one
|
|
169
|
+
// level at a time and asks for the children of each container explicitly.
|
|
170
|
+
// That is what makes the path unambiguous: a bare walk would report a
|
|
171
|
+
// grandchild without saying whose it is.
|
|
172
|
+
walkBoxes(
|
|
173
|
+
initSegment,
|
|
174
|
+
(type, bodyStart, bodyEnd) => {
|
|
175
|
+
const index = seen.get(type) ?? 0;
|
|
176
|
+
seen.set(type, index + 1);
|
|
177
|
+
const path = `${prefix}${type}[${index}]`;
|
|
178
|
+
const childrenAt = childrenStartOf(type);
|
|
179
|
+
if (childrenAt !== null && tilesExactly(initSegment, bodyStart + childrenAt, bodyEnd)) {
|
|
180
|
+
walk(bodyStart + childrenAt, bodyEnd, `${path}/`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
leaves.set(path, initSegment.subarray(bodyStart, bodyEnd));
|
|
184
|
+
},
|
|
185
|
+
start,
|
|
186
|
+
end,
|
|
187
|
+
// One level only: the recursion above is this function's own, and it is
|
|
188
|
+
// what makes a path say whose child a box is.
|
|
189
|
+
() => false
|
|
190
|
+
);
|
|
191
|
+
return leaves;
|
|
192
|
+
};
|
|
193
|
+
walk(0, initSegment.length, "");
|
|
194
|
+
return leaves;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The box type a path's last step names, without its index.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} path
|
|
201
|
+
* @param {number} [fromEnd=0] - 0 is the leaf, 1 its parent.
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
function stepOf(path, fromEnd = 0) {
|
|
205
|
+
const steps = path.split("/");
|
|
206
|
+
const step = steps[steps.length - 1 - fromEnd] ?? "";
|
|
207
|
+
return step.replace(/\[\d+\]$/, "");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Whether an exception covers this path.
|
|
212
|
+
*
|
|
213
|
+
* @param {ReadonlyArray<{ path?: string, sampleEntryBox?: string }>} exceptions
|
|
214
|
+
* @param {string} path
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
217
|
+
function permits(exceptions, path) {
|
|
218
|
+
for (const exception of exceptions) {
|
|
219
|
+
if (exception.path !== undefined && exception.path === path) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
exception.sampleEntryBox !== undefined &&
|
|
224
|
+
stepOf(path) === exception.sampleEntryBox &&
|
|
225
|
+
SAMPLE_ENTRY_PREAMBLE.has(stepOf(path, 1))
|
|
226
|
+
) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* What a difference at this path amounts to, in words.
|
|
235
|
+
*
|
|
236
|
+
* Decoding is for READING the report, never for deciding it: the verdict is the
|
|
237
|
+
* byte comparison, and a path this cannot decode is still a difference.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} path
|
|
240
|
+
* @param {Buffer | null} left
|
|
241
|
+
* @param {Buffer | null} right
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
function describeDifference(path, left, right) {
|
|
245
|
+
if (!left) return `${path}: absent from the first header`;
|
|
246
|
+
if (!right) return `${path}: absent from the second header`;
|
|
247
|
+
if (stepOf(path) === "btrt" && left.length >= 12 && right.length >= 12) {
|
|
248
|
+
const rates = (buffer) =>
|
|
249
|
+
`buffer ${buffer.readUInt32BE(0)}B, max ${buffer.readUInt32BE(4)}, avg ${buffer.readUInt32BE(8)} bit/s`;
|
|
250
|
+
return `${path}: ${rates(left)} against ${rates(right)}`;
|
|
251
|
+
}
|
|
252
|
+
if (path.includes("mdhd")) {
|
|
253
|
+
// FullBox: version+flags, then creation/modification, then timescale.
|
|
254
|
+
const at = left.readUInt8(0) === 1 ? 20 : 12;
|
|
255
|
+
if (left.length > at + 4 && right.length > at + 4) {
|
|
256
|
+
return `${path}: timescale ${left.readUInt32BE(at)} against ${right.readUInt32BE(at)}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (stepOf(path) === "stsd") {
|
|
260
|
+
// Reached only where the entries did not tile, so the walk could not
|
|
261
|
+
// descend. The entry's own type is at a fixed place and is worth naming.
|
|
262
|
+
const entryType = (buffer) => (buffer.length >= 16 ? buffer.toString("latin1", 12, 16) : "?");
|
|
263
|
+
const left4 = entryType(left);
|
|
264
|
+
const right4 = entryType(right);
|
|
265
|
+
const kind = SAMPLE_ENTRY_PREAMBLE.has(left4) || SAMPLE_ENTRY_PREAMBLE.has(right4) ? "" : " (unrecognised)";
|
|
266
|
+
return `${path}: sample entry ${left4} against ${right4}${kind}, ${left.length} bytes against ${right.length}`;
|
|
267
|
+
}
|
|
268
|
+
if (left.length !== right.length) {
|
|
269
|
+
return `${path}: ${left.length} bytes against ${right.length}`;
|
|
270
|
+
}
|
|
271
|
+
let firstDifferent = 0;
|
|
272
|
+
while (firstDifferent < left.length && left[firstDifferent] === right[firstDifferent]) {
|
|
273
|
+
firstDifferent += 1;
|
|
274
|
+
}
|
|
275
|
+
return `${path}: differs from byte ${firstDifferent}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Whether a piece made under `made` may be served under `served`.
|
|
280
|
+
*
|
|
281
|
+
* @param {Buffer} served - The header the player already holds.
|
|
282
|
+
* @param {Buffer} made - The header the other output produces under.
|
|
283
|
+
* @param {{ exceptions?: ReadonlyArray<{ path?: string, sampleEntryBox?: string }> }} [options]
|
|
284
|
+
* @returns {{ compatible: boolean, differences: string[], refusedPaths: string[] }}
|
|
285
|
+
*/
|
|
286
|
+
export function compareInits(served, made, { exceptions = INIT_DIFFERENCE_EXCEPTIONS } = {}) {
|
|
287
|
+
if (!Buffer.isBuffer(served) || !Buffer.isBuffer(made) || served.length === 0 || made.length === 0) {
|
|
288
|
+
return {
|
|
289
|
+
compatible: false,
|
|
290
|
+
differences: ["one of the two headers is missing"],
|
|
291
|
+
refusedPaths: ["<missing>"]
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (served.equals(made)) {
|
|
295
|
+
return { compatible: true, differences: [], refusedPaths: [] };
|
|
296
|
+
}
|
|
297
|
+
const left = initLeaves(served);
|
|
298
|
+
const right = initLeaves(made);
|
|
299
|
+
/** @type {string[]} */
|
|
300
|
+
const differences = [];
|
|
301
|
+
/** @type {string[]} */
|
|
302
|
+
const refusedPaths = [];
|
|
303
|
+
const paths = new Set([...left.keys(), ...right.keys()]);
|
|
304
|
+
for (const path of paths) {
|
|
305
|
+
const a = left.get(path) ?? null;
|
|
306
|
+
const b = right.get(path) ?? null;
|
|
307
|
+
if (a && b && a.equals(b)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
differences.push(describeDifference(path, a, b));
|
|
311
|
+
if (!permits(exceptions, path)) {
|
|
312
|
+
refusedPaths.push(path);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (differences.length === 0) {
|
|
316
|
+
// The bytes differ but every leaf agrees, so what moved is a box header —
|
|
317
|
+
// a size field, or a box present in one tree and absent from the other at a
|
|
318
|
+
// level this walk treats as a container. Not attributable, therefore not
|
|
319
|
+
// permitted.
|
|
320
|
+
differences.push("the headers differ outside any leaf this walk can name");
|
|
321
|
+
refusedPaths.push("<unattributed>");
|
|
322
|
+
}
|
|
323
|
+
return { compatible: refusedPaths.length === 0, differences, refusedPaths };
|
|
324
|
+
}
|