@torrent-tv/proxy 2.89.6 → 2.89.7
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 +338 -338
- package/CHANGELOG.md +5 -0
- 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/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- 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/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/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/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/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- 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/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/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- 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/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +68 -68
- package/test/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/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- 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/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +240 -240
- 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/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +87 -87
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-completeness.test.js +37 -1
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +160 -160
- package/test/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 +64 -64
- package/test/same-height-switch.test.js +663 -663
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-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-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- 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/supply-margin.test.js +190 -190
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/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-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- 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,228 +1,228 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a file declares about ITSELF — format, duration, and where its own
|
|
3
|
-
* timeline begins — read from its header by the container layer.
|
|
4
|
-
*
|
|
5
|
-
* These fixtures are built byte by byte rather than produced by ffmpeg, on
|
|
6
|
-
* purpose: a test that runs a real encoder measures the machine it runs on, and
|
|
7
|
-
* two such tests in this suite have failed four times in one day for exactly
|
|
8
|
-
* that reason (roadmap item 54). The numbers here are checked against ffmpeg
|
|
9
|
-
* ONCE, by hand, and the result is recorded rather than re-measured on every
|
|
10
|
-
* run — 2026-09-03, a Matroska file offset by 0.130435 s: ffmpeg reported
|
|
11
|
-
* `Duration: 00:00:02.13, start: 0.130000` and this reader answered
|
|
12
|
-
* `durationSeconds 2.131, startTimeSeconds 0.13`, which is the same number at
|
|
13
|
-
* the precision each prints. The same file as MP4: `start: 0.000000` from
|
|
14
|
-
* ffmpeg, 0 from this reader.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import assert from "node:assert/strict";
|
|
18
|
-
import test from "node:test";
|
|
19
|
-
|
|
20
|
-
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
21
|
-
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
22
|
-
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* An EBML element: its id bytes, a four-byte size, then the payload.
|
|
26
|
-
*
|
|
27
|
-
* @param {number[]} idBytes
|
|
28
|
-
* @param {Buffer} payload
|
|
29
|
-
* @returns {Buffer}
|
|
30
|
-
*/
|
|
31
|
-
function ebml(idBytes, payload) {
|
|
32
|
-
const size = Buffer.alloc(4);
|
|
33
|
-
// Four-byte size form: `0001xxxx` in the leading byte marks the width.
|
|
34
|
-
size.writeUInt32BE(payload.length);
|
|
35
|
-
size[0] |= 0x10;
|
|
36
|
-
return Buffer.concat([Buffer.from(idBytes), size, payload]);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** @param {number} value @param {number} bytes @returns {Buffer} */
|
|
40
|
-
function uint(value, bytes) {
|
|
41
|
-
const out = Buffer.alloc(bytes);
|
|
42
|
-
out.writeUIntBE(value, 0, bytes);
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** @param {number} value @returns {Buffer} */
|
|
47
|
-
function float64(value) {
|
|
48
|
-
const out = Buffer.alloc(8);
|
|
49
|
-
out.writeDoubleBE(value);
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const ID_EBML_HEADER = [0x1a, 0x45, 0xdf, 0xa3];
|
|
54
|
-
const ID_SEGMENT = [0x18, 0x53, 0x80, 0x67];
|
|
55
|
-
const ID_SEEK_HEAD = [0x11, 0x4d, 0x9b, 0x74];
|
|
56
|
-
const ID_SEEK = [0x4d, 0xbb];
|
|
57
|
-
const ID_SEEK_ID = [0x53, 0xab];
|
|
58
|
-
const ID_SEEK_POSITION = [0x53, 0xac];
|
|
59
|
-
const ID_INFO = [0x15, 0x49, 0xa9, 0x66];
|
|
60
|
-
const ID_TIMESTAMP_SCALE = [0x2a, 0xd7, 0xb1];
|
|
61
|
-
const ID_DURATION = [0x44, 0x89];
|
|
62
|
-
const ID_CLUSTER = [0x1f, 0x43, 0xb6, 0x75];
|
|
63
|
-
const ID_TIMESTAMP = [0xe7];
|
|
64
|
-
const ID_VOID = [0xec];
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* A reader over a buffer, in the shape the container layer takes.
|
|
68
|
-
*
|
|
69
|
-
* @param {Buffer} bytes
|
|
70
|
-
* @returns {(start: number, end: number) => Promise<Buffer>}
|
|
71
|
-
*/
|
|
72
|
-
function readerOver(bytes) {
|
|
73
|
-
return async (start, end) => bytes.subarray(start, Math.min(end + 1, bytes.length));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
test("a Matroska file states its duration in ticks and its start in the first cluster", async () => {
|
|
77
|
-
const info = ebml(ID_INFO, Buffer.concat([
|
|
78
|
-
ebml(ID_TIMESTAMP_SCALE, uint(1_000_000, 3)),
|
|
79
|
-
// 2131 ticks of a millisecond each.
|
|
80
|
-
ebml(ID_DURATION, float64(2131))
|
|
81
|
-
]));
|
|
82
|
-
const cluster = ebml(ID_CLUSTER, ebml(ID_TIMESTAMP, uint(130, 1)));
|
|
83
|
-
const file = Buffer.concat([
|
|
84
|
-
ebml(ID_EBML_HEADER, Buffer.alloc(4)),
|
|
85
|
-
ebml(ID_SEGMENT, Buffer.concat([info, cluster]))
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
const container = new MatroskaContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
89
|
-
const read = await container.readMediaInfo();
|
|
90
|
-
|
|
91
|
-
assert.equal(read.format, "matroska");
|
|
92
|
-
assert.ok(Math.abs(read.durationSeconds - 2.131) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
93
|
-
assert.ok(Math.abs(read.startTimeSeconds - 0.13) < 1e-9, `start was ${read.startTimeSeconds}`);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("a cluster past the head window is found through the SeekHead", async () => {
|
|
97
|
-
// Everything before the cluster is padded past the 64 KB the head read covers,
|
|
98
|
-
// which is the case this second path exists for: a file whose Tracks element
|
|
99
|
-
// is large enough to push the first cluster out of reach.
|
|
100
|
-
const info = ebml(ID_INFO, ebml(ID_TIMESTAMP_SCALE, uint(1_000_000, 3)));
|
|
101
|
-
const padding = ebml(ID_VOID, Buffer.alloc(70 * 1024));
|
|
102
|
-
const cluster = ebml(ID_CLUSTER, ebml(ID_TIMESTAMP, uint(2500, 2)));
|
|
103
|
-
// The SeekHead is written first, so its own length is known before the
|
|
104
|
-
// position it names can be computed — build it with a placeholder, measure,
|
|
105
|
-
// then write the real position.
|
|
106
|
-
const seekHeadFor = (position) => ebml(ID_SEEK_HEAD, ebml(ID_SEEK, Buffer.concat([
|
|
107
|
-
ebml(ID_SEEK_ID, Buffer.from(ID_CLUSTER)),
|
|
108
|
-
ebml(ID_SEEK_POSITION, uint(position, 4))
|
|
109
|
-
])));
|
|
110
|
-
const seekHeadLength = seekHeadFor(0).length;
|
|
111
|
-
const clusterPosition = seekHeadLength + info.length + padding.length;
|
|
112
|
-
const segmentPayload = Buffer.concat([seekHeadFor(clusterPosition), info, padding, cluster]);
|
|
113
|
-
const file = Buffer.concat([
|
|
114
|
-
ebml(ID_EBML_HEADER, Buffer.alloc(4)),
|
|
115
|
-
ebml(ID_SEGMENT, segmentPayload)
|
|
116
|
-
]);
|
|
117
|
-
|
|
118
|
-
const container = new MatroskaContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
119
|
-
const read = await container.readMediaInfo();
|
|
120
|
-
|
|
121
|
-
assert.ok(Math.abs(read.startTimeSeconds - 2.5) < 1e-9, `start was ${read.startTimeSeconds}`);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("a Matroska file that declares no duration says so, rather than saying zero", async () => {
|
|
125
|
-
const cluster = ebml(ID_CLUSTER, ebml(ID_TIMESTAMP, uint(0, 1)));
|
|
126
|
-
const file = Buffer.concat([
|
|
127
|
-
ebml(ID_EBML_HEADER, Buffer.alloc(4)),
|
|
128
|
-
ebml(ID_SEGMENT, cluster)
|
|
129
|
-
]);
|
|
130
|
-
|
|
131
|
-
const container = new MatroskaContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
132
|
-
const read = await container.readMediaInfo();
|
|
133
|
-
|
|
134
|
-
assert.equal(read.durationSeconds, null);
|
|
135
|
-
assert.equal(read.startTimeSeconds, 0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* An ISO/IEC 14496-12 box.
|
|
140
|
-
*
|
|
141
|
-
* @param {string} type
|
|
142
|
-
* @param {Buffer} payload
|
|
143
|
-
* @returns {Buffer}
|
|
144
|
-
*/
|
|
145
|
-
function box(type, payload) {
|
|
146
|
-
const header = Buffer.alloc(8);
|
|
147
|
-
header.writeUInt32BE(payload.length + 8);
|
|
148
|
-
header.write(type, 4, "latin1");
|
|
149
|
-
return Buffer.concat([header, payload]);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
test("an MP4 states its duration in mvhd and its start in an empty edit", async () => {
|
|
153
|
-
const mvhd = box("mvhd", Buffer.concat([
|
|
154
|
-
Buffer.alloc(4), // version 0 + flags
|
|
155
|
-
Buffer.alloc(8), // creation, modification
|
|
156
|
-
uint(1000, 4), // timescale: ticks per second
|
|
157
|
-
uint(2000, 4), // duration: two seconds
|
|
158
|
-
Buffer.alloc(80)
|
|
159
|
-
]));
|
|
160
|
-
const elst = box("elst", Buffer.concat([
|
|
161
|
-
Buffer.alloc(4), // version 0 + flags
|
|
162
|
-
uint(1, 4), // one entry
|
|
163
|
-
uint(130, 4), // segment_duration: 0.130 s at the movie timescale
|
|
164
|
-
Buffer.from([0xff, 0xff, 0xff, 0xff]), // media_time -1: an EMPTY edit
|
|
165
|
-
uint(0x00010000, 4) // media_rate 1.0
|
|
166
|
-
]));
|
|
167
|
-
const trak = box("trak", box("edts", elst));
|
|
168
|
-
const file = Buffer.concat([
|
|
169
|
-
box("ftyp", Buffer.from("isom", "latin1")),
|
|
170
|
-
box("moov", Buffer.concat([mvhd, trak]))
|
|
171
|
-
]);
|
|
172
|
-
|
|
173
|
-
const container = new Mp4Container({ readRange: readerOver(file), fileSize: file.length });
|
|
174
|
-
const read = await container.readMediaInfo();
|
|
175
|
-
|
|
176
|
-
assert.equal(read.format, "mp4");
|
|
177
|
-
assert.ok(Math.abs(read.durationSeconds - 2) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
178
|
-
assert.ok(Math.abs(read.startTimeSeconds - 0.13) < 1e-9, `start was ${read.startTimeSeconds}`);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("an MP4 with no edit list begins at zero, and that is an answer", async () => {
|
|
182
|
-
const mvhd = box("mvhd", Buffer.concat([
|
|
183
|
-
Buffer.alloc(4),
|
|
184
|
-
Buffer.alloc(8),
|
|
185
|
-
uint(600, 4),
|
|
186
|
-
uint(1200, 4),
|
|
187
|
-
Buffer.alloc(80)
|
|
188
|
-
]));
|
|
189
|
-
const file = Buffer.concat([
|
|
190
|
-
box("ftyp", Buffer.from("isom", "latin1")),
|
|
191
|
-
box("moov", mvhd)
|
|
192
|
-
]);
|
|
193
|
-
|
|
194
|
-
const container = new Mp4Container({ readRange: readerOver(file), fileSize: file.length });
|
|
195
|
-
const read = await container.readMediaInfo();
|
|
196
|
-
|
|
197
|
-
assert.ok(Math.abs(read.durationSeconds - 2) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
198
|
-
assert.equal(read.startTimeSeconds, 0);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("an AVI states its length as microseconds per frame times the frame count", async () => {
|
|
202
|
-
const avih = Buffer.concat([
|
|
203
|
-
Buffer.from("avih", "latin1"),
|
|
204
|
-
uint(56, 4),
|
|
205
|
-
Buffer.from(new Uint8Array(new Uint32Array([
|
|
206
|
-
40_000, // dwMicroSecPerFrame: 25 fps
|
|
207
|
-
0, 0, 0,
|
|
208
|
-
50 // dwTotalFrames: two seconds of them
|
|
209
|
-
]).buffer)),
|
|
210
|
-
Buffer.alloc(36)
|
|
211
|
-
]);
|
|
212
|
-
const file = Buffer.concat([
|
|
213
|
-
Buffer.from("RIFF", "latin1"),
|
|
214
|
-
uint(0, 4),
|
|
215
|
-
Buffer.from("AVI ", "latin1"),
|
|
216
|
-
Buffer.from("LIST", "latin1"),
|
|
217
|
-
uint(avih.length + 4, 4),
|
|
218
|
-
Buffer.from("hdrl", "latin1"),
|
|
219
|
-
avih
|
|
220
|
-
]);
|
|
221
|
-
|
|
222
|
-
const container = new AviContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
223
|
-
const read = await container.readMediaInfo();
|
|
224
|
-
|
|
225
|
-
assert.equal(read.format, "avi");
|
|
226
|
-
assert.ok(Math.abs(read.durationSeconds - 2) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
227
|
-
assert.equal(read.startTimeSeconds, 0);
|
|
228
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What a file declares about ITSELF — format, duration, and where its own
|
|
3
|
+
* timeline begins — read from its header by the container layer.
|
|
4
|
+
*
|
|
5
|
+
* These fixtures are built byte by byte rather than produced by ffmpeg, on
|
|
6
|
+
* purpose: a test that runs a real encoder measures the machine it runs on, and
|
|
7
|
+
* two such tests in this suite have failed four times in one day for exactly
|
|
8
|
+
* that reason (roadmap item 54). The numbers here are checked against ffmpeg
|
|
9
|
+
* ONCE, by hand, and the result is recorded rather than re-measured on every
|
|
10
|
+
* run — 2026-09-03, a Matroska file offset by 0.130435 s: ffmpeg reported
|
|
11
|
+
* `Duration: 00:00:02.13, start: 0.130000` and this reader answered
|
|
12
|
+
* `durationSeconds 2.131, startTimeSeconds 0.13`, which is the same number at
|
|
13
|
+
* the precision each prints. The same file as MP4: `start: 0.000000` from
|
|
14
|
+
* ffmpeg, 0 from this reader.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
|
|
20
|
+
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
21
|
+
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
22
|
+
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An EBML element: its id bytes, a four-byte size, then the payload.
|
|
26
|
+
*
|
|
27
|
+
* @param {number[]} idBytes
|
|
28
|
+
* @param {Buffer} payload
|
|
29
|
+
* @returns {Buffer}
|
|
30
|
+
*/
|
|
31
|
+
function ebml(idBytes, payload) {
|
|
32
|
+
const size = Buffer.alloc(4);
|
|
33
|
+
// Four-byte size form: `0001xxxx` in the leading byte marks the width.
|
|
34
|
+
size.writeUInt32BE(payload.length);
|
|
35
|
+
size[0] |= 0x10;
|
|
36
|
+
return Buffer.concat([Buffer.from(idBytes), size, payload]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {number} value @param {number} bytes @returns {Buffer} */
|
|
40
|
+
function uint(value, bytes) {
|
|
41
|
+
const out = Buffer.alloc(bytes);
|
|
42
|
+
out.writeUIntBE(value, 0, bytes);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {number} value @returns {Buffer} */
|
|
47
|
+
function float64(value) {
|
|
48
|
+
const out = Buffer.alloc(8);
|
|
49
|
+
out.writeDoubleBE(value);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ID_EBML_HEADER = [0x1a, 0x45, 0xdf, 0xa3];
|
|
54
|
+
const ID_SEGMENT = [0x18, 0x53, 0x80, 0x67];
|
|
55
|
+
const ID_SEEK_HEAD = [0x11, 0x4d, 0x9b, 0x74];
|
|
56
|
+
const ID_SEEK = [0x4d, 0xbb];
|
|
57
|
+
const ID_SEEK_ID = [0x53, 0xab];
|
|
58
|
+
const ID_SEEK_POSITION = [0x53, 0xac];
|
|
59
|
+
const ID_INFO = [0x15, 0x49, 0xa9, 0x66];
|
|
60
|
+
const ID_TIMESTAMP_SCALE = [0x2a, 0xd7, 0xb1];
|
|
61
|
+
const ID_DURATION = [0x44, 0x89];
|
|
62
|
+
const ID_CLUSTER = [0x1f, 0x43, 0xb6, 0x75];
|
|
63
|
+
const ID_TIMESTAMP = [0xe7];
|
|
64
|
+
const ID_VOID = [0xec];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A reader over a buffer, in the shape the container layer takes.
|
|
68
|
+
*
|
|
69
|
+
* @param {Buffer} bytes
|
|
70
|
+
* @returns {(start: number, end: number) => Promise<Buffer>}
|
|
71
|
+
*/
|
|
72
|
+
function readerOver(bytes) {
|
|
73
|
+
return async (start, end) => bytes.subarray(start, Math.min(end + 1, bytes.length));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test("a Matroska file states its duration in ticks and its start in the first cluster", async () => {
|
|
77
|
+
const info = ebml(ID_INFO, Buffer.concat([
|
|
78
|
+
ebml(ID_TIMESTAMP_SCALE, uint(1_000_000, 3)),
|
|
79
|
+
// 2131 ticks of a millisecond each.
|
|
80
|
+
ebml(ID_DURATION, float64(2131))
|
|
81
|
+
]));
|
|
82
|
+
const cluster = ebml(ID_CLUSTER, ebml(ID_TIMESTAMP, uint(130, 1)));
|
|
83
|
+
const file = Buffer.concat([
|
|
84
|
+
ebml(ID_EBML_HEADER, Buffer.alloc(4)),
|
|
85
|
+
ebml(ID_SEGMENT, Buffer.concat([info, cluster]))
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const container = new MatroskaContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
89
|
+
const read = await container.readMediaInfo();
|
|
90
|
+
|
|
91
|
+
assert.equal(read.format, "matroska");
|
|
92
|
+
assert.ok(Math.abs(read.durationSeconds - 2.131) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
93
|
+
assert.ok(Math.abs(read.startTimeSeconds - 0.13) < 1e-9, `start was ${read.startTimeSeconds}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a cluster past the head window is found through the SeekHead", async () => {
|
|
97
|
+
// Everything before the cluster is padded past the 64 KB the head read covers,
|
|
98
|
+
// which is the case this second path exists for: a file whose Tracks element
|
|
99
|
+
// is large enough to push the first cluster out of reach.
|
|
100
|
+
const info = ebml(ID_INFO, ebml(ID_TIMESTAMP_SCALE, uint(1_000_000, 3)));
|
|
101
|
+
const padding = ebml(ID_VOID, Buffer.alloc(70 * 1024));
|
|
102
|
+
const cluster = ebml(ID_CLUSTER, ebml(ID_TIMESTAMP, uint(2500, 2)));
|
|
103
|
+
// The SeekHead is written first, so its own length is known before the
|
|
104
|
+
// position it names can be computed — build it with a placeholder, measure,
|
|
105
|
+
// then write the real position.
|
|
106
|
+
const seekHeadFor = (position) => ebml(ID_SEEK_HEAD, ebml(ID_SEEK, Buffer.concat([
|
|
107
|
+
ebml(ID_SEEK_ID, Buffer.from(ID_CLUSTER)),
|
|
108
|
+
ebml(ID_SEEK_POSITION, uint(position, 4))
|
|
109
|
+
])));
|
|
110
|
+
const seekHeadLength = seekHeadFor(0).length;
|
|
111
|
+
const clusterPosition = seekHeadLength + info.length + padding.length;
|
|
112
|
+
const segmentPayload = Buffer.concat([seekHeadFor(clusterPosition), info, padding, cluster]);
|
|
113
|
+
const file = Buffer.concat([
|
|
114
|
+
ebml(ID_EBML_HEADER, Buffer.alloc(4)),
|
|
115
|
+
ebml(ID_SEGMENT, segmentPayload)
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const container = new MatroskaContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
119
|
+
const read = await container.readMediaInfo();
|
|
120
|
+
|
|
121
|
+
assert.ok(Math.abs(read.startTimeSeconds - 2.5) < 1e-9, `start was ${read.startTimeSeconds}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a Matroska file that declares no duration says so, rather than saying zero", async () => {
|
|
125
|
+
const cluster = ebml(ID_CLUSTER, ebml(ID_TIMESTAMP, uint(0, 1)));
|
|
126
|
+
const file = Buffer.concat([
|
|
127
|
+
ebml(ID_EBML_HEADER, Buffer.alloc(4)),
|
|
128
|
+
ebml(ID_SEGMENT, cluster)
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const container = new MatroskaContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
132
|
+
const read = await container.readMediaInfo();
|
|
133
|
+
|
|
134
|
+
assert.equal(read.durationSeconds, null);
|
|
135
|
+
assert.equal(read.startTimeSeconds, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An ISO/IEC 14496-12 box.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} type
|
|
142
|
+
* @param {Buffer} payload
|
|
143
|
+
* @returns {Buffer}
|
|
144
|
+
*/
|
|
145
|
+
function box(type, payload) {
|
|
146
|
+
const header = Buffer.alloc(8);
|
|
147
|
+
header.writeUInt32BE(payload.length + 8);
|
|
148
|
+
header.write(type, 4, "latin1");
|
|
149
|
+
return Buffer.concat([header, payload]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test("an MP4 states its duration in mvhd and its start in an empty edit", async () => {
|
|
153
|
+
const mvhd = box("mvhd", Buffer.concat([
|
|
154
|
+
Buffer.alloc(4), // version 0 + flags
|
|
155
|
+
Buffer.alloc(8), // creation, modification
|
|
156
|
+
uint(1000, 4), // timescale: ticks per second
|
|
157
|
+
uint(2000, 4), // duration: two seconds
|
|
158
|
+
Buffer.alloc(80)
|
|
159
|
+
]));
|
|
160
|
+
const elst = box("elst", Buffer.concat([
|
|
161
|
+
Buffer.alloc(4), // version 0 + flags
|
|
162
|
+
uint(1, 4), // one entry
|
|
163
|
+
uint(130, 4), // segment_duration: 0.130 s at the movie timescale
|
|
164
|
+
Buffer.from([0xff, 0xff, 0xff, 0xff]), // media_time -1: an EMPTY edit
|
|
165
|
+
uint(0x00010000, 4) // media_rate 1.0
|
|
166
|
+
]));
|
|
167
|
+
const trak = box("trak", box("edts", elst));
|
|
168
|
+
const file = Buffer.concat([
|
|
169
|
+
box("ftyp", Buffer.from("isom", "latin1")),
|
|
170
|
+
box("moov", Buffer.concat([mvhd, trak]))
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const container = new Mp4Container({ readRange: readerOver(file), fileSize: file.length });
|
|
174
|
+
const read = await container.readMediaInfo();
|
|
175
|
+
|
|
176
|
+
assert.equal(read.format, "mp4");
|
|
177
|
+
assert.ok(Math.abs(read.durationSeconds - 2) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
178
|
+
assert.ok(Math.abs(read.startTimeSeconds - 0.13) < 1e-9, `start was ${read.startTimeSeconds}`);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("an MP4 with no edit list begins at zero, and that is an answer", async () => {
|
|
182
|
+
const mvhd = box("mvhd", Buffer.concat([
|
|
183
|
+
Buffer.alloc(4),
|
|
184
|
+
Buffer.alloc(8),
|
|
185
|
+
uint(600, 4),
|
|
186
|
+
uint(1200, 4),
|
|
187
|
+
Buffer.alloc(80)
|
|
188
|
+
]));
|
|
189
|
+
const file = Buffer.concat([
|
|
190
|
+
box("ftyp", Buffer.from("isom", "latin1")),
|
|
191
|
+
box("moov", mvhd)
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const container = new Mp4Container({ readRange: readerOver(file), fileSize: file.length });
|
|
195
|
+
const read = await container.readMediaInfo();
|
|
196
|
+
|
|
197
|
+
assert.ok(Math.abs(read.durationSeconds - 2) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
198
|
+
assert.equal(read.startTimeSeconds, 0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("an AVI states its length as microseconds per frame times the frame count", async () => {
|
|
202
|
+
const avih = Buffer.concat([
|
|
203
|
+
Buffer.from("avih", "latin1"),
|
|
204
|
+
uint(56, 4),
|
|
205
|
+
Buffer.from(new Uint8Array(new Uint32Array([
|
|
206
|
+
40_000, // dwMicroSecPerFrame: 25 fps
|
|
207
|
+
0, 0, 0,
|
|
208
|
+
50 // dwTotalFrames: two seconds of them
|
|
209
|
+
]).buffer)),
|
|
210
|
+
Buffer.alloc(36)
|
|
211
|
+
]);
|
|
212
|
+
const file = Buffer.concat([
|
|
213
|
+
Buffer.from("RIFF", "latin1"),
|
|
214
|
+
uint(0, 4),
|
|
215
|
+
Buffer.from("AVI ", "latin1"),
|
|
216
|
+
Buffer.from("LIST", "latin1"),
|
|
217
|
+
uint(avih.length + 4, 4),
|
|
218
|
+
Buffer.from("hdrl", "latin1"),
|
|
219
|
+
avih
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const container = new AviContainer({ readRange: readerOver(file), fileSize: file.length });
|
|
223
|
+
const read = await container.readMediaInfo();
|
|
224
|
+
|
|
225
|
+
assert.equal(read.format, "avi");
|
|
226
|
+
assert.ok(Math.abs(read.durationSeconds - 2) < 1e-9, `duration was ${read.durationSeconds}`);
|
|
227
|
+
assert.equal(read.startTimeSeconds, 0);
|
|
228
|
+
});
|
package/test/contention.test.js
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The price of a second job, with the readings it was derived from.
|
|
3
|
-
*
|
|
4
|
-
* Measured on the addon host 2026-08-18: decoding the same clip ran at 2.20x
|
|
5
|
-
* alone, 0.85x with one encoder beside it and 0.60x with two. The budget has
|
|
6
|
-
* been adding independent prices, and these say two jobs that each fit alone do
|
|
7
|
-
* not fit together.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
|
|
13
|
-
import { contentionPenalty, costWithContention, penaltiesFrom } from "../services/encode/contention.js";
|
|
14
|
-
|
|
15
|
-
/** The addon host's own readings. */
|
|
16
|
-
const ALONE = 2.2;
|
|
17
|
-
const BESIDE = [
|
|
18
|
-
{ others: 1, speed: 0.85 },
|
|
19
|
-
{ others: 2, speed: 0.6 }
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
test("the penalties are the ratios of the measured speeds", () => {
|
|
23
|
-
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
24
|
-
|
|
25
|
-
assert.ok(penalties);
|
|
26
|
-
// 2.2 / 0.85 = 2.59: the same work costs two and a half times more with one
|
|
27
|
-
// encoder beside it.
|
|
28
|
-
assert.ok(Math.abs(penalties.get(1) - 2.588) < 0.01, `${penalties.get(1)}`);
|
|
29
|
-
assert.ok(Math.abs(penalties.get(2) - 3.667) < 0.01, `${penalties.get(2)}`);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("alone on the machine is what the benchmarks measure, so nothing is corrected", () => {
|
|
33
|
-
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
34
|
-
const answer = contentionPenalty(0, penalties);
|
|
35
|
-
assert.equal(answer.penalty, 1);
|
|
36
|
-
assert.equal(answer.measured, true);
|
|
37
|
-
assert.equal(costWithContention(0.45, 0, penalties), 0.45);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("a cost beside one other job is the measured multiple, not the sum of two prices", () => {
|
|
41
|
-
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
42
|
-
// Decoding priced at 0.45 s/s alone becomes 1.17 s/s beside an encoder —
|
|
43
|
-
// which is what the field measured (1.18), and what additivity cannot say.
|
|
44
|
-
const corrected = costWithContention(0.45, 1, penalties);
|
|
45
|
-
assert.ok(Math.abs(corrected - 1.165) < 0.01, `${corrected}`);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("beyond the readings it holds the largest instead of extrapolating", () => {
|
|
49
|
-
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
50
|
-
const four = contentionPenalty(4, penalties);
|
|
51
|
-
assert.equal(four.penalty, penalties.get(2), "two readings say nothing about a fourth job");
|
|
52
|
-
assert.equal(four.from, 2, "and the answer says which reading it came from");
|
|
53
|
-
assert.equal(four.measured, true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("with nothing measured the prediction is left alone, not guessed at", () => {
|
|
57
|
-
const answer = contentionPenalty(1, null);
|
|
58
|
-
assert.equal(answer.penalty, 1);
|
|
59
|
-
assert.equal(answer.measured, false, "the caller must be able to say the figure is uncorrected");
|
|
60
|
-
assert.equal(costWithContention(0.45, 1, null), 0.45);
|
|
61
|
-
assert.equal(contentionPenalty(1, new Map()).measured, false);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("a machine that got faster for being busier has no penalty to measure", () => {
|
|
65
|
-
// Noise on a fast host, not a discovery. Measured on the developer's desktop,
|
|
66
|
-
// the difference between one and two jobs is inside the scatter.
|
|
67
|
-
const penalties = penaltiesFrom(20, [{ others: 1, speed: 21 }]);
|
|
68
|
-
assert.equal(penalties.get(1), 1);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("readings that are not measurements are left out", () => {
|
|
72
|
-
assert.equal(penaltiesFrom(0, BESIDE), null, "every penalty is relative to the alone reading");
|
|
73
|
-
assert.equal(penaltiesFrom(ALONE, []), null);
|
|
74
|
-
assert.equal(penaltiesFrom(ALONE, [{ others: 0, speed: 2 }]), null, "zero others is not a penalty");
|
|
75
|
-
assert.equal(penaltiesFrom(ALONE, [{ others: 1, speed: Number.NaN }]), null);
|
|
76
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The price of a second job, with the readings it was derived from.
|
|
3
|
+
*
|
|
4
|
+
* Measured on the addon host 2026-08-18: decoding the same clip ran at 2.20x
|
|
5
|
+
* alone, 0.85x with one encoder beside it and 0.60x with two. The budget has
|
|
6
|
+
* been adding independent prices, and these say two jobs that each fit alone do
|
|
7
|
+
* not fit together.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
|
|
13
|
+
import { contentionPenalty, costWithContention, penaltiesFrom } from "../services/encode/contention.js";
|
|
14
|
+
|
|
15
|
+
/** The addon host's own readings. */
|
|
16
|
+
const ALONE = 2.2;
|
|
17
|
+
const BESIDE = [
|
|
18
|
+
{ others: 1, speed: 0.85 },
|
|
19
|
+
{ others: 2, speed: 0.6 }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
test("the penalties are the ratios of the measured speeds", () => {
|
|
23
|
+
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
24
|
+
|
|
25
|
+
assert.ok(penalties);
|
|
26
|
+
// 2.2 / 0.85 = 2.59: the same work costs two and a half times more with one
|
|
27
|
+
// encoder beside it.
|
|
28
|
+
assert.ok(Math.abs(penalties.get(1) - 2.588) < 0.01, `${penalties.get(1)}`);
|
|
29
|
+
assert.ok(Math.abs(penalties.get(2) - 3.667) < 0.01, `${penalties.get(2)}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("alone on the machine is what the benchmarks measure, so nothing is corrected", () => {
|
|
33
|
+
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
34
|
+
const answer = contentionPenalty(0, penalties);
|
|
35
|
+
assert.equal(answer.penalty, 1);
|
|
36
|
+
assert.equal(answer.measured, true);
|
|
37
|
+
assert.equal(costWithContention(0.45, 0, penalties), 0.45);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a cost beside one other job is the measured multiple, not the sum of two prices", () => {
|
|
41
|
+
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
42
|
+
// Decoding priced at 0.45 s/s alone becomes 1.17 s/s beside an encoder —
|
|
43
|
+
// which is what the field measured (1.18), and what additivity cannot say.
|
|
44
|
+
const corrected = costWithContention(0.45, 1, penalties);
|
|
45
|
+
assert.ok(Math.abs(corrected - 1.165) < 0.01, `${corrected}`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("beyond the readings it holds the largest instead of extrapolating", () => {
|
|
49
|
+
const penalties = penaltiesFrom(ALONE, BESIDE);
|
|
50
|
+
const four = contentionPenalty(4, penalties);
|
|
51
|
+
assert.equal(four.penalty, penalties.get(2), "two readings say nothing about a fourth job");
|
|
52
|
+
assert.equal(four.from, 2, "and the answer says which reading it came from");
|
|
53
|
+
assert.equal(four.measured, true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("with nothing measured the prediction is left alone, not guessed at", () => {
|
|
57
|
+
const answer = contentionPenalty(1, null);
|
|
58
|
+
assert.equal(answer.penalty, 1);
|
|
59
|
+
assert.equal(answer.measured, false, "the caller must be able to say the figure is uncorrected");
|
|
60
|
+
assert.equal(costWithContention(0.45, 1, null), 0.45);
|
|
61
|
+
assert.equal(contentionPenalty(1, new Map()).measured, false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a machine that got faster for being busier has no penalty to measure", () => {
|
|
65
|
+
// Noise on a fast host, not a discovery. Measured on the developer's desktop,
|
|
66
|
+
// the difference between one and two jobs is inside the scatter.
|
|
67
|
+
const penalties = penaltiesFrom(20, [{ others: 1, speed: 21 }]);
|
|
68
|
+
assert.equal(penalties.get(1), 1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("readings that are not measurements are left out", () => {
|
|
72
|
+
assert.equal(penaltiesFrom(0, BESIDE), null, "every penalty is relative to the alone reading");
|
|
73
|
+
assert.equal(penaltiesFrom(ALONE, []), null);
|
|
74
|
+
assert.equal(penaltiesFrom(ALONE, [{ others: 0, speed: 2 }]), null, "zero others is not a penalty");
|
|
75
|
+
assert.equal(penaltiesFrom(ALONE, [{ others: 1, speed: Number.NaN }]), null);
|
|
76
|
+
});
|