@torrent-tv/proxy 2.89.1 → 2.89.3
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 +6 -1
- package/CHANGELOG.md +15 -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 +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +42 -8
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -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 +380 -380
- 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/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/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 +2282 -2282
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -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 +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/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/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -349
- 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-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +476 -458
- package/services/encode/segment-formats/index.js +103 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +604 -541
- 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 +1150 -1150
- package/services/server/ViewerRequests.js +790 -786
- 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 +1024 -1020
- 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/segment-store/SegmentStore.js +1255 -1255
- 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 +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +836 -836
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/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 -0
- package/services/{server → viewer}/playback-readiness.js +635 -516
- 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 -70
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/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/fmp4-presentation-clock.test.js +146 -162
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -112
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -0
- 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 +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -483
- package/test/playback-start-regressions.test.js +101 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -105
- 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 +209 -209
- 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 +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/whole-file-held-read.test.js +30 -30
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/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,162 +1,146 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
4
|
-
import {
|
|
5
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
9
|
-
import { Viewer } from "../services/viewer/Viewer.js";
|
|
10
|
-
|
|
11
|
-
function box(type, body) {
|
|
12
|
-
const header = Buffer.alloc(8);
|
|
13
|
-
header.writeUInt32BE(body.length + 8);
|
|
14
|
-
header.write(type, 4);
|
|
15
|
-
return Buffer.concat([header, body]);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function init(emptyMs, mediaTime, kind = "vide") {
|
|
19
|
-
const mvhd = Buffer.alloc(24);
|
|
20
|
-
mvhd.writeUInt32BE(1000, 12);
|
|
21
|
-
const tkhd = Buffer.alloc(20);
|
|
22
|
-
tkhd.writeUInt32BE(1, 12);
|
|
23
|
-
const mdhd = Buffer.alloc(24);
|
|
24
|
-
mdhd.writeUInt32BE(24000, 12);
|
|
25
|
-
const hdlr = Buffer.alloc(12);
|
|
26
|
-
hdlr.write(kind, 8);
|
|
27
|
-
const elst = Buffer.alloc(32);
|
|
28
|
-
elst.writeUInt32BE(2, 4);
|
|
29
|
-
elst.writeUInt32BE(emptyMs, 8);
|
|
30
|
-
elst.writeInt32BE(-1, 12);
|
|
31
|
-
elst.writeUInt32BE(1000, 20);
|
|
32
|
-
elst.writeInt32BE(mediaTime, 24);
|
|
33
|
-
return box("moov", Buffer.concat([box("mvhd", mvhd), box("trak", Buffer.concat([
|
|
34
|
-
box("tkhd", tkhd), box("edts", box("elst", elst)),
|
|
35
|
-
box("mdia", Buffer.concat([box("mdhd", mdhd), box("hdlr", hdlr)]))
|
|
36
|
-
]))]));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function fragment(decodeTime, composition) {
|
|
40
|
-
const tfhd = Buffer.alloc(12);
|
|
41
|
-
tfhd.writeUIntBE(8, 1, 3);
|
|
42
|
-
tfhd.writeUInt32BE(1, 4);
|
|
43
|
-
tfhd.writeUInt32BE(1000, 8);
|
|
44
|
-
const tfdt = Buffer.alloc(12);
|
|
45
|
-
tfdt[0] = 1;
|
|
46
|
-
tfdt.writeBigUInt64BE(BigInt(decodeTime), 4);
|
|
47
|
-
const trun = Buffer.alloc(12);
|
|
48
|
-
trun.writeUIntBE(0x800, 1, 3);
|
|
49
|
-
trun.writeUInt32BE(1, 4);
|
|
50
|
-
trun.writeUInt32BE(composition, 8);
|
|
51
|
-
return box("moof", box("traf", Buffer.concat([
|
|
52
|
-
box("tfhd", tfhd), box("tfdt", tfdt), box("trun", trun)
|
|
53
|
-
])));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
test("an interrupted frame cannot prove coverage of a full future cut", () => {
|
|
57
|
-
const ranges = readPresentationRanges(Buffer.concat([init(456000, 0), fragment(0, 0)]));
|
|
58
|
-
assert.ok(
|
|
59
|
-
assert.ok(
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("a shared init preserves presentation times without adding composition delay twice", () => {
|
|
63
|
-
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
64
|
-
const own = init(4125, 3000);
|
|
65
|
-
const media = Buffer.concat([fragment(0, 3000), fragment(48000, 3000)]);
|
|
66
|
-
const raw = Buffer.concat([own, media]);
|
|
67
|
-
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
68
|
-
startSeconds: 4.125, initBytes: shared, rawBytes: raw
|
|
69
|
-
});
|
|
70
|
-
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
71
|
-
const times = [];
|
|
72
|
-
walkBoxes(prepared, (type, start) => {
|
|
73
|
-
if (type === "tfdt") times.push(Number(prepared.readBigUInt64BE(start + 4)));
|
|
74
|
-
});
|
|
75
|
-
assert.deepEqual(times, [98000, 146000]);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
assert.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const media = Buffer.concat([fragment(0, 0), fragment(72000, 0)]);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
assert.equal(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const continuous = readPresentationRanges(Buffer.concat([init(0, 0), fragment(1000, 72000)]));
|
|
148
|
-
assert.equal(continuePresentationRanges(first, continuous)[0].end, continuous[0].start);
|
|
149
|
-
const missing = readPresentationRanges(Buffer.concat([init(0, 0), fragment(24000, 72000)]));
|
|
150
|
-
assert.deepEqual(continuePresentationRanges(first, missing), first);
|
|
151
|
-
const audioFirst = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(0, 0)]));
|
|
152
|
-
const audioNext = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(1000, 72000)]));
|
|
153
|
-
assert.deepEqual(continuePresentationRanges(audioFirst, audioNext), audioFirst);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test("a viewer retains only finite per-track player clock offsets", () => {
|
|
157
|
-
const viewer = new Viewer("clock-reading", 1);
|
|
158
|
-
viewer.report({ timestampOffsets: { video: -0.083, audio: 0, media: 400 } }, 2);
|
|
159
|
-
assert.deepEqual(viewer.linkReading().timestampOffsets, { video: -0.083, audio: 0 });
|
|
160
|
-
viewer.report({ timestampOffsets: { video: Number.POSITIVE_INFINITY } }, 3);
|
|
161
|
-
assert.deepEqual(viewer.linkReading().timestampOffsets, {});
|
|
162
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
4
|
+
import { producedThroughSeconds, readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
5
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
9
|
+
import { Viewer } from "../services/viewer/Viewer.js";
|
|
10
|
+
|
|
11
|
+
function box(type, body) {
|
|
12
|
+
const header = Buffer.alloc(8);
|
|
13
|
+
header.writeUInt32BE(body.length + 8);
|
|
14
|
+
header.write(type, 4);
|
|
15
|
+
return Buffer.concat([header, body]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function init(emptyMs, mediaTime, kind = "vide") {
|
|
19
|
+
const mvhd = Buffer.alloc(24);
|
|
20
|
+
mvhd.writeUInt32BE(1000, 12);
|
|
21
|
+
const tkhd = Buffer.alloc(20);
|
|
22
|
+
tkhd.writeUInt32BE(1, 12);
|
|
23
|
+
const mdhd = Buffer.alloc(24);
|
|
24
|
+
mdhd.writeUInt32BE(24000, 12);
|
|
25
|
+
const hdlr = Buffer.alloc(12);
|
|
26
|
+
hdlr.write(kind, 8);
|
|
27
|
+
const elst = Buffer.alloc(32);
|
|
28
|
+
elst.writeUInt32BE(2, 4);
|
|
29
|
+
elst.writeUInt32BE(emptyMs, 8);
|
|
30
|
+
elst.writeInt32BE(-1, 12);
|
|
31
|
+
elst.writeUInt32BE(1000, 20);
|
|
32
|
+
elst.writeInt32BE(mediaTime, 24);
|
|
33
|
+
return box("moov", Buffer.concat([box("mvhd", mvhd), box("trak", Buffer.concat([
|
|
34
|
+
box("tkhd", tkhd), box("edts", box("elst", elst)),
|
|
35
|
+
box("mdia", Buffer.concat([box("mdhd", mdhd), box("hdlr", hdlr)]))
|
|
36
|
+
]))]));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fragment(decodeTime, composition) {
|
|
40
|
+
const tfhd = Buffer.alloc(12);
|
|
41
|
+
tfhd.writeUIntBE(8, 1, 3);
|
|
42
|
+
tfhd.writeUInt32BE(1, 4);
|
|
43
|
+
tfhd.writeUInt32BE(1000, 8);
|
|
44
|
+
const tfdt = Buffer.alloc(12);
|
|
45
|
+
tfdt[0] = 1;
|
|
46
|
+
tfdt.writeBigUInt64BE(BigInt(decodeTime), 4);
|
|
47
|
+
const trun = Buffer.alloc(12);
|
|
48
|
+
trun.writeUIntBE(0x800, 1, 3);
|
|
49
|
+
trun.writeUInt32BE(1, 4);
|
|
50
|
+
trun.writeUInt32BE(composition, 8);
|
|
51
|
+
return box("moof", box("traf", Buffer.concat([
|
|
52
|
+
box("tfhd", tfhd), box("tfdt", tfdt), box("trun", trun)
|
|
53
|
+
])));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test("an interrupted frame cannot prove coverage of a full future cut", () => {
|
|
57
|
+
const ranges = readPresentationRanges(Buffer.concat([init(456000, 0), fragment(0, 0)]));
|
|
58
|
+
assert.ok(producedThroughSeconds(ranges) > 456);
|
|
59
|
+
assert.ok(producedThroughSeconds(ranges) < 456.1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a shared init preserves presentation times without adding composition delay twice", () => {
|
|
63
|
+
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
64
|
+
const own = init(4125, 3000);
|
|
65
|
+
const media = Buffer.concat([fragment(0, 3000), fragment(48000, 3000)]);
|
|
66
|
+
const raw = Buffer.concat([own, media]);
|
|
67
|
+
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
68
|
+
startSeconds: 4.125, initBytes: shared, rawBytes: raw
|
|
69
|
+
});
|
|
70
|
+
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
71
|
+
const times = [];
|
|
72
|
+
walkBoxes(prepared, (type, start) => {
|
|
73
|
+
if (type === "tfdt") times.push(Number(prepared.readBigUInt64BE(start + 4)));
|
|
74
|
+
});
|
|
75
|
+
assert.deepEqual(times, [98000, 146000]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a piece states each range in ticks with the frame duration a browser allows it", () => {
|
|
79
|
+
const coverage = readPresentationRanges(Buffer.concat([init(0, 0, "vide"), fragment(24000, 0)]));
|
|
80
|
+
assert.deepEqual(coverage.tracks[0].ranges, [{ start: 24000n, end: 25000n, frame: 1000n }]);
|
|
81
|
+
assert.equal(coverage.tracks[0].timescale, 24000n);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a shared init first read after a seek also supports returning to the beginning", () => {
|
|
85
|
+
const shared = fmp4Format.prepareSharedInit(init(304083, 2000));
|
|
86
|
+
const media = fragment(0, 2000);
|
|
87
|
+
const raw = Buffer.concat([init(83, 2000), media]);
|
|
88
|
+
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
89
|
+
startSeconds: 0.083, initBytes: shared, rawBytes: raw
|
|
90
|
+
});
|
|
91
|
+
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("HLS fragments use the separate init and the same timeline translation as serving", (t) => {
|
|
95
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "presentation-ranges-"));
|
|
96
|
+
const store = new SegmentStore({ root });
|
|
97
|
+
t.after(() => {
|
|
98
|
+
store.dropAll("the check is over");
|
|
99
|
+
rmSync(root, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
const key = "encoded-video";
|
|
102
|
+
store.useFormat(key, fmp4Format);
|
|
103
|
+
const dir = store.directoryFor(key);
|
|
104
|
+
const media = fragment(0, 2000);
|
|
105
|
+
writeFileSync(path.join(dir, fmp4Format.segmentFileName(1)), media);
|
|
106
|
+
assert.equal(store.mediaRangesOf(key, 1, { startSeconds: 4 }), undefined);
|
|
107
|
+
const header = init(0, 0);
|
|
108
|
+
writeFileSync(path.join(dir, fmp4Format.initFileName), header);
|
|
109
|
+
const served = fmp4Format.prepareSegmentBytes(media, { initBytes: header, startSeconds: 4 });
|
|
110
|
+
assert.deepEqual(store.mediaRangesOf(key, 1, { startSeconds: 4 }),
|
|
111
|
+
readPresentationRanges(Buffer.concat([header, served])));
|
|
112
|
+
assert.ok(store.mediaRangesOf(key, 1).tracks[0].ranges[0].start > 4n * 24000n);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a decode gap between fragments is two ranges, for video as for audio", () => {
|
|
116
|
+
// Joining across a gap is the browser's decision, made in the viewer
|
|
117
|
+
// component; the piece states what it holds.
|
|
118
|
+
const media = Buffer.concat([fragment(0, 0), fragment(72000, 0)]);
|
|
119
|
+
for (const kind of ["vide", "soun"]) {
|
|
120
|
+
const coverage = readPresentationRanges(Buffer.concat([init(0, 0, kind), media]));
|
|
121
|
+
assert.deepEqual(coverage.tracks[0].ranges.map(({ start, end }) => [start, end]),
|
|
122
|
+
[[0n, 1000n], [72000n, 73000n]]);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("served ranges move every track by the session init's position, in ticks", () => {
|
|
127
|
+
const audioHeader = init(0, 0, "soun");
|
|
128
|
+
const audio = readPresentationRanges(Buffer.concat([audioHeader, fragment(96000, 0)]));
|
|
129
|
+
assert.equal(fmp4Format.servedMediaRanges(audio, { initBytes: audioHeader }).ranges[0].start, 96000n);
|
|
130
|
+
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
131
|
+
const video = readPresentationRanges(Buffer.concat([init(83, 2000), fragment(0, 2000)]));
|
|
132
|
+
// The piece's own edit puts its first frame at round(83 ms * 24000) = 1992
|
|
133
|
+
// ticks; the served bytes carry the composition delay the shared init's
|
|
134
|
+
// edit removes, which is what a page's timestampOffset then takes back.
|
|
135
|
+
assert.equal(video.tracks[0].ranges[0].start, 1992n);
|
|
136
|
+
assert.equal(fmp4Format.servedMediaRanges(video, { initBytes: shared }).ranges[0].start, 1992n + 2000n);
|
|
137
|
+
assert.deepEqual(fmp4Format.servedMediaRanges(video, { initBytes: null }).ranges, video.tracks[0].ranges);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a viewer retains only finite per-track player clock offsets", () => {
|
|
141
|
+
const viewer = new Viewer("clock-reading", 1);
|
|
142
|
+
viewer.report({ timestampOffsets: { video: -0.083, audio: 0, media: 400 } }, 2);
|
|
143
|
+
assert.deepEqual(viewer.linkReading().timestampOffsets, { video: -0.083, audio: 0 });
|
|
144
|
+
viewer.report({ timestampOffsets: { video: Number.POSITIVE_INFINITY } }, 3);
|
|
145
|
+
assert.deepEqual(viewer.linkReading().timestampOffsets, {});
|
|
146
|
+
});
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Splitting a self-contained fMP4 piece into an init segment and a media
|
|
3
|
-
* segment.
|
|
4
|
-
*
|
|
5
|
-
* The muxer that takes explicit cut times writes every piece whole —
|
|
6
|
-
* `ftyp moov moof mdat … mfra` — but HLS wants one init named by `#EXT-X-MAP`
|
|
7
|
-
* and media segments carrying only fragments. Cutting at the wrong offset does
|
|
8
|
-
* not fail loudly; it produces something that parses as garbage, which is why
|
|
9
|
-
* the boundaries are pinned here rather than trusted.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A piece shaped like the muxer's output. Sizes are what matters, not contents.
|
|
18
|
-
*
|
|
19
|
-
* @param {{ withIndex?: boolean }} [options]
|
|
20
|
-
* @returns {{ piece: Buffer, initLength: number, fragmentsLength: number }}
|
|
21
|
-
*/
|
|
22
|
-
function makePiece({ withIndex = true } = {}) {
|
|
23
|
-
const box = (type, bodyLength) => {
|
|
24
|
-
const buffer = Buffer.alloc(8 + bodyLength);
|
|
25
|
-
buffer.writeUInt32BE(8 + bodyLength, 0);
|
|
26
|
-
buffer.write(type, 4, "latin1");
|
|
27
|
-
buffer.fill(0x5a, 8);
|
|
28
|
-
return buffer;
|
|
29
|
-
};
|
|
30
|
-
const ftyp = box("ftyp", 24);
|
|
31
|
-
const moov = box("moov", 400);
|
|
32
|
-
const moof = box("moof", 120);
|
|
33
|
-
const mdat = box("mdat", 900);
|
|
34
|
-
const mfra = box("mfra", 40);
|
|
35
|
-
const parts = withIndex ? [ftyp, moov, moof, mdat, mfra] : [ftyp, moov, moof, mdat];
|
|
36
|
-
return {
|
|
37
|
-
piece: Buffer.concat(parts),
|
|
38
|
-
initLength: ftyp.length + moov.length,
|
|
39
|
-
fragmentsLength: moof.length + mdat.length
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
test("the init is everything before the first fragment", () => {
|
|
44
|
-
const { piece, initLength } = makePiece();
|
|
45
|
-
const init = fmp4Format.extractInit(piece);
|
|
46
|
-
assert.ok(init, "no init found in a piece that has one");
|
|
47
|
-
assert.equal(init.length, initLength, "init must end exactly where the first fragment begins");
|
|
48
|
-
assert.equal(init.toString("latin1", 4, 8), "ftyp", "init must start at the file header");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("the media segment is the fragments alone, index dropped", () => {
|
|
52
|
-
const { piece, initLength, fragmentsLength } = makePiece();
|
|
53
|
-
const media = fmp4Format.stripInit(piece);
|
|
54
|
-
assert.equal(media.toString("latin1", 4, 8), "moof", "a media segment must begin at a fragment");
|
|
55
|
-
assert.equal(
|
|
56
|
-
media.length,
|
|
57
|
-
fragmentsLength,
|
|
58
|
-
"the trailing random-access index must be dropped: its offsets describe a file that no longer exists"
|
|
59
|
-
);
|
|
60
|
-
assert.equal(piece.length, initLength + fragmentsLength + 48, "test fixture sanity");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("a piece without a trailing index keeps everything to the end", () => {
|
|
64
|
-
const { piece, fragmentsLength } = makePiece({ withIndex: false });
|
|
65
|
-
assert.equal(fmp4Format.stripInit(piece).length, fragmentsLength);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("a segment that is already fragments-only is left alone", () => {
|
|
69
|
-
const alreadyMedia = fmp4Format.stripInit(makePiece().piece);
|
|
70
|
-
assert.deepEqual(
|
|
71
|
-
fmp4Format.stripInit(alreadyMedia),
|
|
72
|
-
alreadyMedia,
|
|
73
|
-
"stripping twice must not eat the first fragment"
|
|
74
|
-
);
|
|
75
|
-
assert.equal(
|
|
76
|
-
fmp4Format.extractInit(alreadyMedia),
|
|
77
|
-
null,
|
|
78
|
-
"there is no init to lift out of a media segment, and inventing one would corrupt playback"
|
|
79
|
-
);
|
|
80
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Splitting a self-contained fMP4 piece into an init segment and a media
|
|
3
|
+
* segment.
|
|
4
|
+
*
|
|
5
|
+
* The muxer that takes explicit cut times writes every piece whole —
|
|
6
|
+
* `ftyp moov moof mdat … mfra` — but HLS wants one init named by `#EXT-X-MAP`
|
|
7
|
+
* and media segments carrying only fragments. Cutting at the wrong offset does
|
|
8
|
+
* not fail loudly; it produces something that parses as garbage, which is why
|
|
9
|
+
* the boundaries are pinned here rather than trusted.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A piece shaped like the muxer's output. Sizes are what matters, not contents.
|
|
18
|
+
*
|
|
19
|
+
* @param {{ withIndex?: boolean }} [options]
|
|
20
|
+
* @returns {{ piece: Buffer, initLength: number, fragmentsLength: number }}
|
|
21
|
+
*/
|
|
22
|
+
function makePiece({ withIndex = true } = {}) {
|
|
23
|
+
const box = (type, bodyLength) => {
|
|
24
|
+
const buffer = Buffer.alloc(8 + bodyLength);
|
|
25
|
+
buffer.writeUInt32BE(8 + bodyLength, 0);
|
|
26
|
+
buffer.write(type, 4, "latin1");
|
|
27
|
+
buffer.fill(0x5a, 8);
|
|
28
|
+
return buffer;
|
|
29
|
+
};
|
|
30
|
+
const ftyp = box("ftyp", 24);
|
|
31
|
+
const moov = box("moov", 400);
|
|
32
|
+
const moof = box("moof", 120);
|
|
33
|
+
const mdat = box("mdat", 900);
|
|
34
|
+
const mfra = box("mfra", 40);
|
|
35
|
+
const parts = withIndex ? [ftyp, moov, moof, mdat, mfra] : [ftyp, moov, moof, mdat];
|
|
36
|
+
return {
|
|
37
|
+
piece: Buffer.concat(parts),
|
|
38
|
+
initLength: ftyp.length + moov.length,
|
|
39
|
+
fragmentsLength: moof.length + mdat.length
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("the init is everything before the first fragment", () => {
|
|
44
|
+
const { piece, initLength } = makePiece();
|
|
45
|
+
const init = fmp4Format.extractInit(piece);
|
|
46
|
+
assert.ok(init, "no init found in a piece that has one");
|
|
47
|
+
assert.equal(init.length, initLength, "init must end exactly where the first fragment begins");
|
|
48
|
+
assert.equal(init.toString("latin1", 4, 8), "ftyp", "init must start at the file header");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("the media segment is the fragments alone, index dropped", () => {
|
|
52
|
+
const { piece, initLength, fragmentsLength } = makePiece();
|
|
53
|
+
const media = fmp4Format.stripInit(piece);
|
|
54
|
+
assert.equal(media.toString("latin1", 4, 8), "moof", "a media segment must begin at a fragment");
|
|
55
|
+
assert.equal(
|
|
56
|
+
media.length,
|
|
57
|
+
fragmentsLength,
|
|
58
|
+
"the trailing random-access index must be dropped: its offsets describe a file that no longer exists"
|
|
59
|
+
);
|
|
60
|
+
assert.equal(piece.length, initLength + fragmentsLength + 48, "test fixture sanity");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a piece without a trailing index keeps everything to the end", () => {
|
|
64
|
+
const { piece, fragmentsLength } = makePiece({ withIndex: false });
|
|
65
|
+
assert.equal(fmp4Format.stripInit(piece).length, fragmentsLength);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a segment that is already fragments-only is left alone", () => {
|
|
69
|
+
const alreadyMedia = fmp4Format.stripInit(makePiece().piece);
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
fmp4Format.stripInit(alreadyMedia),
|
|
72
|
+
alreadyMedia,
|
|
73
|
+
"stripping twice must not eat the first fragment"
|
|
74
|
+
);
|
|
75
|
+
assert.equal(
|
|
76
|
+
fmp4Format.extractInit(alreadyMedia),
|
|
77
|
+
null,
|
|
78
|
+
"there is no init to lift out of a media segment, and inventing one would corrupt playback"
|
|
79
|
+
);
|
|
80
|
+
});
|