@torrent-tv/proxy 2.88.2 → 2.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2259 -2246
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +411 -411
- package/docs/browser-proxy-contract.md +23 -4
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -173
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -84
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +725 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1235 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -758
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2184
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -416
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1150 -1141
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -254
- package/services/encode/quality/output-format.js +319 -315
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +456 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +21 -6
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +298 -245
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -603
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1244
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -131
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -0
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +760 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +13 -8
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +995 -992
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1931 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1240 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/episode-naming.js +182 -182
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2675 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +70 -0
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/episode-naming.test.js +131 -131
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/audio-frame-boundary.json +1 -0
- package/test/fmp4-presentation-clock.test.js +147 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +270 -204
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +453 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1229
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +323 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -102
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -0
- package/test/source-file.test.js +133 -133
- package/test/source-files-route.test.js +113 -113
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -407
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,147 +1,147 @@
|
|
|
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 { continuePresentationRanges, 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("a shared init preserves presentation times without adding composition delay twice", () => {
|
|
57
|
-
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
58
|
-
const own = init(4125, 3000);
|
|
59
|
-
const media = Buffer.concat([fragment(0, 3000), fragment(48000, 3000)]);
|
|
60
|
-
const raw = Buffer.concat([own, media]);
|
|
61
|
-
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
62
|
-
startSeconds: 4.125, initBytes: shared, rawBytes: raw
|
|
63
|
-
});
|
|
64
|
-
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
65
|
-
const times = [];
|
|
66
|
-
walkBoxes(prepared, (type, start) => {
|
|
67
|
-
if (type === "tfdt") times.push(Number(prepared.readBigUInt64BE(start + 4)));
|
|
68
|
-
});
|
|
69
|
-
assert.deepEqual(times, [98000, 146000]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("a shared init first read after a seek also supports returning to the beginning", () => {
|
|
73
|
-
const shared = fmp4Format.prepareSharedInit(init(304083, 2000));
|
|
74
|
-
const media = fragment(0, 2000);
|
|
75
|
-
const raw = Buffer.concat([init(83, 2000), media]);
|
|
76
|
-
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
77
|
-
startSeconds: 0.083, initBytes: shared, rawBytes: raw
|
|
78
|
-
});
|
|
79
|
-
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("HLS fragments use the separate init and the same timeline translation as serving", (t) => {
|
|
83
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "presentation-ranges-"));
|
|
84
|
-
const store = new SegmentStore({ root });
|
|
85
|
-
t.after(() => {
|
|
86
|
-
store.dropAll("the check is over");
|
|
87
|
-
rmSync(root, { recursive: true, force: true });
|
|
88
|
-
});
|
|
89
|
-
const key = "encoded-video";
|
|
90
|
-
store.useFormat(key, fmp4Format);
|
|
91
|
-
const dir = store.directoryFor(key);
|
|
92
|
-
const media = fragment(0, 2000);
|
|
93
|
-
writeFileSync(path.join(dir, fmp4Format.segmentFileName(1)), media);
|
|
94
|
-
assert.equal(store.mediaRangesOf(key, 1, { startSeconds: 4 }), undefined);
|
|
95
|
-
const header = init(0, 0);
|
|
96
|
-
writeFileSync(path.join(dir, fmp4Format.initFileName), header);
|
|
97
|
-
const served = fmp4Format.prepareSegmentBytes(media, { initBytes: header, startSeconds: 4 });
|
|
98
|
-
assert.deepEqual(store.mediaRangesOf(key, 1, { startSeconds: 4 }),
|
|
99
|
-
readPresentationRanges(Buffer.concat([header, served])));
|
|
100
|
-
assert.ok(store.mediaRangesOf(key, 1)[0].start > 4);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("variable-rate video retains its frame until the next presentation sample", () => {
|
|
104
|
-
const media = Buffer.concat([fragment(0, 0), fragment(72000, 0)]);
|
|
105
|
-
const video = readPresentationRanges(Buffer.concat([init(0, 0), media]));
|
|
106
|
-
assert.equal(video.length, 1);
|
|
107
|
-
assert.equal(video[0].start, 0);
|
|
108
|
-
assert.ok(video[0].end > 3);
|
|
109
|
-
const audio = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), media]));
|
|
110
|
-
assert.equal(audio.length, 2);
|
|
111
|
-
assert.ok(audio[0].end < 0.05);
|
|
112
|
-
assert.equal(audio[1].start, 3);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("client clock projection accounts for shared edits and audio initPTS", () => {
|
|
116
|
-
const audioHeader = init(0, 0, "soun");
|
|
117
|
-
const audio = readPresentationRanges(Buffer.concat([audioHeader, fragment(96000, 0)]));
|
|
118
|
-
const shifted = fmp4Format.clientMediaRanges(audio, {
|
|
119
|
-
initBytes: audioHeader, timestampOffsetSeconds: -0.083
|
|
120
|
-
});
|
|
121
|
-
assert.equal(shifted[0].start, 4 - 0.083);
|
|
122
|
-
const videoHeader = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
123
|
-
const video = readPresentationRanges(Buffer.concat([init(83, 2000), fragment(0, 2000)]));
|
|
124
|
-
const projected = fmp4Format.clientMediaRanges(video, {
|
|
125
|
-
initBytes: videoHeader, timestampOffsetSeconds: -2000 / 24000
|
|
126
|
-
});
|
|
127
|
-
assert.deepEqual(projected, video);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("a variable-rate frame crosses a cut only with continuous decode samples", () => {
|
|
131
|
-
const first = readPresentationRanges(Buffer.concat([init(0, 0), fragment(0, 0)]));
|
|
132
|
-
const continuous = readPresentationRanges(Buffer.concat([init(0, 0), fragment(1000, 72000)]));
|
|
133
|
-
assert.equal(continuePresentationRanges(first, continuous)[0].end, continuous[0].start);
|
|
134
|
-
const missing = readPresentationRanges(Buffer.concat([init(0, 0), fragment(24000, 72000)]));
|
|
135
|
-
assert.deepEqual(continuePresentationRanges(first, missing), first);
|
|
136
|
-
const audioFirst = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(0, 0)]));
|
|
137
|
-
const audioNext = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(1000, 72000)]));
|
|
138
|
-
assert.deepEqual(continuePresentationRanges(audioFirst, audioNext), audioFirst);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("a viewer retains only finite per-track player clock offsets", () => {
|
|
142
|
-
const viewer = new Viewer("clock-reading", 1);
|
|
143
|
-
viewer.report({ timestampOffsets: { video: -0.083, audio: 0, media: 400 } }, 2);
|
|
144
|
-
assert.deepEqual(viewer.linkReading().timestampOffsets, { video: -0.083, audio: 0 });
|
|
145
|
-
viewer.report({ timestampOffsets: { video: Number.POSITIVE_INFINITY } }, 3);
|
|
146
|
-
assert.deepEqual(viewer.linkReading().timestampOffsets, {});
|
|
147
|
-
});
|
|
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 { continuePresentationRanges, 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("a shared init preserves presentation times without adding composition delay twice", () => {
|
|
57
|
+
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
58
|
+
const own = init(4125, 3000);
|
|
59
|
+
const media = Buffer.concat([fragment(0, 3000), fragment(48000, 3000)]);
|
|
60
|
+
const raw = Buffer.concat([own, media]);
|
|
61
|
+
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
62
|
+
startSeconds: 4.125, initBytes: shared, rawBytes: raw
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
65
|
+
const times = [];
|
|
66
|
+
walkBoxes(prepared, (type, start) => {
|
|
67
|
+
if (type === "tfdt") times.push(Number(prepared.readBigUInt64BE(start + 4)));
|
|
68
|
+
});
|
|
69
|
+
assert.deepEqual(times, [98000, 146000]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a shared init first read after a seek also supports returning to the beginning", () => {
|
|
73
|
+
const shared = fmp4Format.prepareSharedInit(init(304083, 2000));
|
|
74
|
+
const media = fragment(0, 2000);
|
|
75
|
+
const raw = Buffer.concat([init(83, 2000), media]);
|
|
76
|
+
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
77
|
+
startSeconds: 0.083, initBytes: shared, rawBytes: raw
|
|
78
|
+
});
|
|
79
|
+
assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("HLS fragments use the separate init and the same timeline translation as serving", (t) => {
|
|
83
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "presentation-ranges-"));
|
|
84
|
+
const store = new SegmentStore({ root });
|
|
85
|
+
t.after(() => {
|
|
86
|
+
store.dropAll("the check is over");
|
|
87
|
+
rmSync(root, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
const key = "encoded-video";
|
|
90
|
+
store.useFormat(key, fmp4Format);
|
|
91
|
+
const dir = store.directoryFor(key);
|
|
92
|
+
const media = fragment(0, 2000);
|
|
93
|
+
writeFileSync(path.join(dir, fmp4Format.segmentFileName(1)), media);
|
|
94
|
+
assert.equal(store.mediaRangesOf(key, 1, { startSeconds: 4 }), undefined);
|
|
95
|
+
const header = init(0, 0);
|
|
96
|
+
writeFileSync(path.join(dir, fmp4Format.initFileName), header);
|
|
97
|
+
const served = fmp4Format.prepareSegmentBytes(media, { initBytes: header, startSeconds: 4 });
|
|
98
|
+
assert.deepEqual(store.mediaRangesOf(key, 1, { startSeconds: 4 }),
|
|
99
|
+
readPresentationRanges(Buffer.concat([header, served])));
|
|
100
|
+
assert.ok(store.mediaRangesOf(key, 1)[0].start > 4);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("variable-rate video retains its frame until the next presentation sample", () => {
|
|
104
|
+
const media = Buffer.concat([fragment(0, 0), fragment(72000, 0)]);
|
|
105
|
+
const video = readPresentationRanges(Buffer.concat([init(0, 0), media]));
|
|
106
|
+
assert.equal(video.length, 1);
|
|
107
|
+
assert.equal(video[0].start, 0);
|
|
108
|
+
assert.ok(video[0].end > 3);
|
|
109
|
+
const audio = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), media]));
|
|
110
|
+
assert.equal(audio.length, 2);
|
|
111
|
+
assert.ok(audio[0].end < 0.05);
|
|
112
|
+
assert.equal(audio[1].start, 3);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("client clock projection accounts for shared edits and audio initPTS", () => {
|
|
116
|
+
const audioHeader = init(0, 0, "soun");
|
|
117
|
+
const audio = readPresentationRanges(Buffer.concat([audioHeader, fragment(96000, 0)]));
|
|
118
|
+
const shifted = fmp4Format.clientMediaRanges(audio, {
|
|
119
|
+
initBytes: audioHeader, timestampOffsetSeconds: -0.083
|
|
120
|
+
});
|
|
121
|
+
assert.equal(shifted[0].start, 4 - 0.083);
|
|
122
|
+
const videoHeader = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
123
|
+
const video = readPresentationRanges(Buffer.concat([init(83, 2000), fragment(0, 2000)]));
|
|
124
|
+
const projected = fmp4Format.clientMediaRanges(video, {
|
|
125
|
+
initBytes: videoHeader, timestampOffsetSeconds: -2000 / 24000
|
|
126
|
+
});
|
|
127
|
+
assert.deepEqual(projected, video);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("a variable-rate frame crosses a cut only with continuous decode samples", () => {
|
|
131
|
+
const first = readPresentationRanges(Buffer.concat([init(0, 0), fragment(0, 0)]));
|
|
132
|
+
const continuous = readPresentationRanges(Buffer.concat([init(0, 0), fragment(1000, 72000)]));
|
|
133
|
+
assert.equal(continuePresentationRanges(first, continuous)[0].end, continuous[0].start);
|
|
134
|
+
const missing = readPresentationRanges(Buffer.concat([init(0, 0), fragment(24000, 72000)]));
|
|
135
|
+
assert.deepEqual(continuePresentationRanges(first, missing), first);
|
|
136
|
+
const audioFirst = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(0, 0)]));
|
|
137
|
+
const audioNext = readPresentationRanges(Buffer.concat([init(0, 0, "soun"), fragment(1000, 72000)]));
|
|
138
|
+
assert.deepEqual(continuePresentationRanges(audioFirst, audioNext), audioFirst);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a viewer retains only finite per-track player clock offsets", () => {
|
|
142
|
+
const viewer = new Viewer("clock-reading", 1);
|
|
143
|
+
viewer.report({ timestampOffsets: { video: -0.083, audio: 0, media: 400 } }, 2);
|
|
144
|
+
assert.deepEqual(viewer.linkReading().timestampOffsets, { video: -0.083, audio: 0 });
|
|
145
|
+
viewer.report({ timestampOffsets: { video: Number.POSITIVE_INFINITY } }, 3);
|
|
146
|
+
assert.deepEqual(viewer.linkReading().timestampOffsets, {});
|
|
147
|
+
});
|
|
@@ -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
|
+
});
|