@torrent-tv/proxy 2.86.0 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2138 -1915
- package/CLAUDE.md +318 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +8 -5
- package/routes/transcode/session-file/get.js +429 -268
- package/routes/transcode/variant-file/get.js +47 -7
- package/routes/transcode/variant-warm/get.js +18 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1219 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +745 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +931 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/wire-outputs.js +958 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -45
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -515
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +40 -8
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -202
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9608
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/services/viewer/link-readings.js +0 -56
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -25,9 +25,14 @@ const CONTAINER_BOXES = new Set(["moov", "trak", "mdia", "minf", "stbl", "edts",
|
|
|
25
25
|
* `bodyStart`/`bodyEnd` delimit the box payload (header excluded).
|
|
26
26
|
* @param {number} [start=0]
|
|
27
27
|
* @param {number} [end=buffer.length]
|
|
28
|
+
* @param {(type: string) => boolean} [descend] - Whether to walk into a box's
|
|
29
|
+
* payload. The default is the container list above, which is what every
|
|
30
|
+
* reader of a whole tree wants. A caller that needs to know WHOSE child a box
|
|
31
|
+
* is passes `() => false` and recurses itself, because a walk that descends
|
|
32
|
+
* for you cannot say which parent it descended from.
|
|
28
33
|
* @returns {void}
|
|
29
34
|
*/
|
|
30
|
-
export function walkBoxes(buffer, visit, start = 0, end = buffer.length) {
|
|
35
|
+
export function walkBoxes(buffer, visit, start = 0, end = buffer.length, descend = (type) => CONTAINER_BOXES.has(type)) {
|
|
31
36
|
let offset = start;
|
|
32
37
|
while (offset + 8 <= end) {
|
|
33
38
|
let size = buffer.readUInt32BE(offset);
|
|
@@ -48,8 +53,8 @@ export function walkBoxes(buffer, visit, start = 0, end = buffer.length) {
|
|
|
48
53
|
return; // truncated or malformed — stop rather than read out of bounds
|
|
49
54
|
}
|
|
50
55
|
visit(type, offset + headerSize, offset + size);
|
|
51
|
-
if (
|
|
52
|
-
walkBoxes(buffer, visit, offset + headerSize, offset + size);
|
|
56
|
+
if (descend(type)) {
|
|
57
|
+
walkBoxes(buffer, visit, offset + headerSize, offset + size, descend);
|
|
53
58
|
}
|
|
54
59
|
offset += size;
|
|
55
60
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file How fast a calibrated encoding mode runs at a given frame size, from
|
|
3
|
+
* the readings the startup calibration took (`encode/calibration.js`).
|
|
4
|
+
*
|
|
5
|
+
* Pure arithmetic over measured figures, kept apart from the calibration so
|
|
6
|
+
* that everything which prices an encode can read it without reaching the code
|
|
7
|
+
* that runs ffmpeg.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} SizeReading
|
|
12
|
+
* @property {number} width
|
|
13
|
+
* @property {number} height
|
|
14
|
+
* @property {number} pixelsPerSec
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The rate of pictures this mode would encode at this frame, in pixels per
|
|
19
|
+
* second, or null where this machine has not shown it can.
|
|
20
|
+
*
|
|
21
|
+
* An entry with no readings by size — built before sizes were read, or by a
|
|
22
|
+
* check that states one figure — answers with its one figure, as it always did.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ pixelsPerSec: number, bySize?: SizeReading[], interpolationError?: number | null }} entry
|
|
25
|
+
* @param {{ width: number, height: number } | null | undefined} frame
|
|
26
|
+
* @returns {number | null}
|
|
27
|
+
*/
|
|
28
|
+
export function throughputAt(entry, frame) {
|
|
29
|
+
const bySize = Array.isArray(entry?.bySize) ? entry.bySize : [];
|
|
30
|
+
const width = Number(frame?.width) || 0;
|
|
31
|
+
const height = Number(frame?.height) || 0;
|
|
32
|
+
if (bySize.length === 0 || width <= 0 || height <= 0) {
|
|
33
|
+
const figure = Number(entry?.pixelsPerSec);
|
|
34
|
+
return Number.isFinite(figure) && figure > 0 ? figure : null;
|
|
35
|
+
}
|
|
36
|
+
const area = width * height;
|
|
37
|
+
const sorted = [...bySize].sort((left, right) => left.width * left.height - right.width * right.height);
|
|
38
|
+
const smallest = sorted[0];
|
|
39
|
+
const largest = sorted[sorted.length - 1];
|
|
40
|
+
if (area < smallest.width * smallest.height || area > largest.width * largest.height) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
for (let index = 0; index < sorted.length; index += 1) {
|
|
44
|
+
const reading = sorted[index];
|
|
45
|
+
const readArea = reading.width * reading.height;
|
|
46
|
+
if (readArea === area) {
|
|
47
|
+
return reading.pixelsPerSec;
|
|
48
|
+
}
|
|
49
|
+
if (readArea > area) {
|
|
50
|
+
const error = entry.interpolationError;
|
|
51
|
+
if (!(Number.isFinite(error) && error >= 0 && error < 1)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const below = sorted[index - 1];
|
|
55
|
+
return interpolate(below, reading, area) * (1 - error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pixels per second at an area between two readings, straight in logarithms
|
|
63
|
+
* of both: throughput against frame area is a power law over any short
|
|
64
|
+
* stretch, and a straight line between two points of it is the least that
|
|
65
|
+
* can be said without inventing a shape.
|
|
66
|
+
*
|
|
67
|
+
* @param {SizeReading} below
|
|
68
|
+
* @param {SizeReading} above
|
|
69
|
+
* @param {number} area
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function interpolate(below, above, area) {
|
|
73
|
+
const lowArea = below.width * below.height;
|
|
74
|
+
const highArea = above.width * above.height;
|
|
75
|
+
const share = Math.log(area / lowArea) / Math.log(highArea / lowArea);
|
|
76
|
+
return Math.exp(Math.log(below.pixelsPerSec) + share * (Math.log(above.pixelsPerSec) - Math.log(below.pixelsPerSec)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* How far an interpolation between readings of this mode was measured to be
|
|
81
|
+
* wrong: the largest relative error of a middle reading predicted from the two
|
|
82
|
+
* outside it.
|
|
83
|
+
*
|
|
84
|
+
* @param {SizeReading[]} bySize - Smallest first.
|
|
85
|
+
* @returns {number | null} Null with fewer than three readings.
|
|
86
|
+
*/
|
|
87
|
+
export function interpolationErrorOf(bySize) {
|
|
88
|
+
let largest = null;
|
|
89
|
+
for (let index = 1; index + 1 < bySize.length; index += 1) {
|
|
90
|
+
const middle = bySize[index];
|
|
91
|
+
const predicted = interpolate(bySize[index - 1], bySize[index + 1], middle.width * middle.height);
|
|
92
|
+
const error = Math.abs(predicted - middle.pixelsPerSec) / middle.pixelsPerSec;
|
|
93
|
+
largest = largest === null ? error : Math.max(largest, error);
|
|
94
|
+
}
|
|
95
|
+
return largest;
|
|
96
|
+
}
|
|
97
|
+
|
|
@@ -7,15 +7,37 @@
|
|
|
7
7
|
* readRange, knows nothing about torrents or HTTP.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ContainerFactory } from "
|
|
10
|
+
import { ContainerFactory } from "./container/ContainerFactory.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How much of a file's head its track table lives in.
|
|
14
|
+
*
|
|
15
|
+
* A fact of the FORMATS, not of the torrent: Matroska puts its Tracks element
|
|
16
|
+
* in the head and MP4's `moov` is there or pointed to from there. Whoever
|
|
17
|
+
* fetches bytes before asking is told this figure rather than choosing one.
|
|
18
|
+
*/
|
|
19
|
+
export const CONTAINER_HEAD_BYTES = 256 * 1024;
|
|
11
20
|
import { logger } from "../../utils/logger.js";
|
|
12
21
|
|
|
13
22
|
export class ContainerOrchestrator {
|
|
14
23
|
constructor() {
|
|
15
|
-
/** @type {Map<string, import("
|
|
24
|
+
/** @type {Map<string, import("./container/Container.js").Container|null>} */
|
|
16
25
|
this.cache = new Map();
|
|
17
|
-
/** @type {Map<string, Promise<import("
|
|
26
|
+
/** @type {Map<string, Promise<import("./container/Container.js").Container|null>>} */
|
|
18
27
|
this.pending = new Map();
|
|
28
|
+
/**
|
|
29
|
+
* What each file's container declares, once it has said anything.
|
|
30
|
+
*
|
|
31
|
+
* The container parses on every ask, so without this the header is read
|
|
32
|
+
* again for each question — and the track table is asked for the audio
|
|
33
|
+
* menu, the subtitle defaults and the video facts of one file. It used to
|
|
34
|
+
* be kept in the torrent thread (`torrent-worker/container-tracks.js`), which is where the
|
|
35
|
+
* parse used to happen; it is the same one cache in its new place, not a
|
|
36
|
+
* second one.
|
|
37
|
+
*
|
|
38
|
+
* @type {Map<string, import("./tracks/index.js").ContainerTrack[]>}
|
|
39
|
+
*/
|
|
40
|
+
this.tracks = new Map();
|
|
19
41
|
}
|
|
20
42
|
|
|
21
43
|
/**
|
|
@@ -25,7 +47,7 @@ export class ContainerOrchestrator {
|
|
|
25
47
|
* @param {(start:number,end:number)=>Promise<Buffer|null>} params.readRange
|
|
26
48
|
* @param {number} params.fileSize
|
|
27
49
|
* @param {string} [params.label]
|
|
28
|
-
* @returns {Promise<import("
|
|
50
|
+
* @returns {Promise<import("./container/Container.js").Container|null>}
|
|
29
51
|
*/
|
|
30
52
|
async getContainer({ sourceKey, fileIndex, readRange, fileSize, label = "" }) {
|
|
31
53
|
const key = `${sourceKey}:${fileIndex}`;
|
|
@@ -48,13 +70,27 @@ export class ContainerOrchestrator {
|
|
|
48
70
|
|
|
49
71
|
/**
|
|
50
72
|
* @param {object} params - same as getContainer
|
|
51
|
-
* @returns {Promise<import("
|
|
73
|
+
* @returns {Promise<import("./tracks/index.js").ContainerTrack[]>}
|
|
52
74
|
*/
|
|
53
75
|
async getTracks(params) {
|
|
76
|
+
const key = `${params.sourceKey}:${params.fileIndex}`;
|
|
77
|
+
const known = this.tracks.get(key);
|
|
78
|
+
if (known) {
|
|
79
|
+
return known;
|
|
80
|
+
}
|
|
54
81
|
const container = await this.getContainer(params);
|
|
55
82
|
if (!container) return [];
|
|
56
83
|
try {
|
|
57
|
-
|
|
84
|
+
const tracks = await container.readTracks();
|
|
85
|
+
// ONLY A NON-EMPTY READING IS KEPT. An empty one usually means the header
|
|
86
|
+
// has not arrived off the swarm yet, and caching that would answer "this
|
|
87
|
+
// file declares nothing" for the life of the process — which is what
|
|
88
|
+
// decides whether the viewer is offered a soundtrack at all.
|
|
89
|
+
if (Array.isArray(tracks) && tracks.length > 0) {
|
|
90
|
+
this.tracks.set(key, tracks);
|
|
91
|
+
return tracks;
|
|
92
|
+
}
|
|
93
|
+
return Array.isArray(tracks) ? tracks : [];
|
|
58
94
|
} catch (e) {
|
|
59
95
|
logger.warn(`container: readTracks failed for "${params.label}": ${e?.message ?? e}`);
|
|
60
96
|
return [];
|
|
@@ -70,7 +106,7 @@ export class ContainerOrchestrator {
|
|
|
70
106
|
* a final answer about the container.
|
|
71
107
|
*
|
|
72
108
|
* @param {object} params - same as getContainer
|
|
73
|
-
* @returns {Promise<import("
|
|
109
|
+
* @returns {Promise<import("./container/Container.js").ContainerMediaInfo|null>}
|
|
74
110
|
*/
|
|
75
111
|
async getMediaInfo(params) {
|
|
76
112
|
const container = await this.getContainer(params);
|
|
@@ -101,10 +137,12 @@ export class ContainerOrchestrator {
|
|
|
101
137
|
if (fileIndex === undefined) {
|
|
102
138
|
for (const k of [...this.cache.keys()]) if (k.startsWith(`${sourceKey}:`)) this.cache.delete(k);
|
|
103
139
|
for (const k of [...this.pending.keys()]) if (k.startsWith(`${sourceKey}:`)) this.pending.delete(k);
|
|
140
|
+
for (const k of [...this.tracks.keys()]) if (k.startsWith(`${sourceKey}:`)) this.tracks.delete(k);
|
|
104
141
|
return;
|
|
105
142
|
}
|
|
106
143
|
this.cache.delete(`${sourceKey}:${fileIndex}`);
|
|
107
144
|
this.pending.delete(`${sourceKey}:${fileIndex}`);
|
|
145
|
+
this.tracks.delete(`${sourceKey}:${fileIndex}`);
|
|
108
146
|
}
|
|
109
147
|
}
|
|
110
148
|
|