@torrent-tv/proxy 2.89.5 → 2.89.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +338 -338
- package/CHANGELOG.md +16 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -753
- 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 +475 -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 +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +68 -68
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/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 +240 -240
- package/test/helpers/manager.js +62 -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 +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +87 -87
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-completeness.test.js +37 -1
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +160 -160
- package/test/quality-variants.test.js +1270 -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 +64 -64
- package/test/same-height-switch.test.js +663 -663
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +222 -221
- package/test/segment-serve-wiring.test.js +9 -3
- 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 +111 -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 +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/supply-margin.test.js +190 -190
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +508 -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 +92 -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,355 +1,355 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Exact media time, and the rule by which a viewer's browser joins
|
|
3
|
-
* buffered media.
|
|
4
|
-
*
|
|
5
|
-
* A time is `{ ticks, timescale }`: `ticks / timescale` seconds, both `BigInt`.
|
|
6
|
-
* A file states its timestamps this way, and comparing two of them is integer
|
|
7
|
-
* arithmetic. Converting them to seconds first replaces an exact comparison
|
|
8
|
-
* with a rounded one — measured 2026-10-02 on a served film, seven joins whose
|
|
9
|
-
* ticks were equal read as `88.67299999999999` against `88.673` and were taken
|
|
10
|
-
* for holes.
|
|
11
|
-
*
|
|
12
|
-
* A value that arrives as a JavaScript number — a position or a buffered range
|
|
13
|
-
* from the page, a cut time from the playlist — is a binary fraction, and it is
|
|
14
|
-
* converted to the exact time it denotes ({@link fromSeconds}). No unit is
|
|
15
|
-
* chosen and nothing is rounded. Seconds are produced again only for a
|
|
16
|
-
* forecast, a page or a log ({@link toSeconds}).
|
|
17
|
-
*
|
|
18
|
-
* A range is `{ start, end, frame }`: the presentation interval `[start, end)`
|
|
19
|
-
* and `frame`, the coded-frame duration a browser is known to allow this range
|
|
20
|
-
* when it decides whether a neighbour joins it ({@link joins}). `frame` is zero
|
|
21
|
-
* for a range whose frames are not known, such as one the page reports holding.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/** @typedef {{ ticks: bigint, timescale: bigint }} MediaTime */
|
|
25
|
-
/** @typedef {{ start: MediaTime, end: MediaTime, frame: MediaTime }} MediaRange */
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The gap WebKit closes between buffered ranges: `PlatformTimeRanges::
|
|
29
|
-
* timeFudgeFactor()`, applied by `PlatformTimeRanges::add` with
|
|
30
|
-
* `EliminateSmallGaps` to every sample a `TrackBuffer` adds.
|
|
31
|
-
* https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/graphics/PlatformTimeRanges.cpp
|
|
32
|
-
*
|
|
33
|
-
* @type {MediaTime}
|
|
34
|
-
*/
|
|
35
|
-
const WEBKIT_TIME_FUDGE_FACTOR = Object.freeze({ ticks: 2002n, timescale: 24000n });
|
|
36
|
-
|
|
37
|
-
/** @type {MediaTime} */
|
|
38
|
-
export const ZERO = Object.freeze({ ticks: 0n, timescale: 1n });
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* How far a buffered range a page reports can lie from the exact time of the
|
|
42
|
-
* frames behind it. Chromium converts three values to whole microseconds before
|
|
43
|
-
* a buffered range exists — a sample's presentation time and its duration in
|
|
44
|
-
* `TimeDeltaFromRational` (media/formats/mp4/track_run_iterator.cc, rounding
|
|
45
|
-
* toward zero), and the page's `timestampOffset` — so each edge is off by less
|
|
46
|
-
* than one microsecond per conversion. Gecko and WebKit keep exact rationals
|
|
47
|
-
* until the value becomes a JavaScript number, which is far finer.
|
|
48
|
-
*
|
|
49
|
-
* @type {MediaTime}
|
|
50
|
-
*/
|
|
51
|
-
export const REPORTED_TIME_ERROR = Object.freeze({ ticks: 3n, timescale: 1_000_000n });
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {bigint} ticks
|
|
55
|
-
* @param {bigint} timescale
|
|
56
|
-
* @returns {MediaTime}
|
|
57
|
-
*/
|
|
58
|
-
export function mediaTime(ticks, timescale) {
|
|
59
|
-
if (!(timescale > 0n)) {
|
|
60
|
-
throw new Error(`A timescale must be positive, not ${timescale}.`);
|
|
61
|
-
}
|
|
62
|
-
return { ticks, timescale };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The exact time a finite number of seconds denotes. An IEEE 754 double is
|
|
67
|
-
* `mantissa * 2^exponent`, so it is a fraction whose denominator is a power of
|
|
68
|
-
* two; this returns that fraction.
|
|
69
|
-
*
|
|
70
|
-
* @param {number} seconds
|
|
71
|
-
* @returns {MediaTime}
|
|
72
|
-
*/
|
|
73
|
-
export function fromSeconds(seconds) {
|
|
74
|
-
if (!Number.isFinite(seconds)) {
|
|
75
|
-
throw new Error(`Seconds must be finite, not ${seconds}.`);
|
|
76
|
-
}
|
|
77
|
-
if (seconds === 0) {
|
|
78
|
-
return ZERO;
|
|
79
|
-
}
|
|
80
|
-
const view = new DataView(new ArrayBuffer(8));
|
|
81
|
-
view.setFloat64(0, seconds);
|
|
82
|
-
const bits = view.getBigUint64(0);
|
|
83
|
-
const sign = bits >> 63n === 1n ? -1n : 1n;
|
|
84
|
-
const biasedExponent = (bits >> 52n) & 0x7ffn;
|
|
85
|
-
let mantissa = bits & 0xfffffffffffffn;
|
|
86
|
-
let exponent;
|
|
87
|
-
if (biasedExponent === 0n) {
|
|
88
|
-
exponent = -1074n;
|
|
89
|
-
} else {
|
|
90
|
-
mantissa |= 0x10000000000000n;
|
|
91
|
-
exponent = biasedExponent - 1075n;
|
|
92
|
-
}
|
|
93
|
-
while (exponent < 0n && mantissa % 2n === 0n) {
|
|
94
|
-
mantissa /= 2n;
|
|
95
|
-
exponent += 1n;
|
|
96
|
-
}
|
|
97
|
-
return exponent >= 0n
|
|
98
|
-
? { ticks: sign * mantissa * (1n << exponent), timescale: 1n }
|
|
99
|
-
: { ticks: sign * mantissa, timescale: 1n << -exponent };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Seconds, for a forecast, a page or a log.
|
|
104
|
-
*
|
|
105
|
-
* @param {MediaTime} time
|
|
106
|
-
* @returns {number}
|
|
107
|
-
*/
|
|
108
|
-
export function toSeconds(time) {
|
|
109
|
-
return Number(time.ticks) / Number(time.timescale);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function gcd(left, right) {
|
|
113
|
-
let a = left < 0n ? -left : left;
|
|
114
|
-
let b = right;
|
|
115
|
-
while (b !== 0n) {
|
|
116
|
-
[a, b] = [b, a % b];
|
|
117
|
-
}
|
|
118
|
-
return a;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {MediaTime} left
|
|
123
|
-
* @param {MediaTime} right
|
|
124
|
-
* @returns {-1 | 0 | 1}
|
|
125
|
-
*/
|
|
126
|
-
export function compare(left, right) {
|
|
127
|
-
if (left.timescale === right.timescale) {
|
|
128
|
-
return left.ticks < right.ticks ? -1 : left.ticks > right.ticks ? 1 : 0;
|
|
129
|
-
}
|
|
130
|
-
const a = left.ticks * right.timescale;
|
|
131
|
-
const b = right.ticks * left.timescale;
|
|
132
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* @param {MediaTime} left
|
|
137
|
-
* @param {MediaTime} right
|
|
138
|
-
* @returns {MediaTime}
|
|
139
|
-
*/
|
|
140
|
-
export function add(left, right) {
|
|
141
|
-
if (left.timescale === right.timescale) {
|
|
142
|
-
return { ticks: left.ticks + right.ticks, timescale: left.timescale };
|
|
143
|
-
}
|
|
144
|
-
const common = left.timescale / gcd(left.timescale, right.timescale) * right.timescale;
|
|
145
|
-
return {
|
|
146
|
-
ticks: left.ticks * (common / left.timescale) + right.ticks * (common / right.timescale),
|
|
147
|
-
timescale: common
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @param {MediaTime} left
|
|
153
|
-
* @param {MediaTime} right
|
|
154
|
-
* @returns {MediaTime}
|
|
155
|
-
*/
|
|
156
|
-
export function subtract(left, right) {
|
|
157
|
-
return add(left, { ticks: -right.ticks, timescale: right.timescale });
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* @param {MediaTime} time
|
|
162
|
-
* @param {bigint} factor
|
|
163
|
-
* @returns {MediaTime}
|
|
164
|
-
*/
|
|
165
|
-
export function multiply(time, factor) {
|
|
166
|
-
return { ticks: time.ticks * factor, timescale: time.timescale };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* `time * numerator / denominator`, exactly.
|
|
171
|
-
*
|
|
172
|
-
* @param {MediaTime} time
|
|
173
|
-
* @param {MediaTime} numerator
|
|
174
|
-
* @param {MediaTime} denominator - Not zero.
|
|
175
|
-
* @returns {MediaTime}
|
|
176
|
-
*/
|
|
177
|
-
export function scale(time, numerator, denominator) {
|
|
178
|
-
const ticks = time.ticks * numerator.ticks * denominator.timescale;
|
|
179
|
-
const timescale = time.timescale * numerator.timescale * denominator.ticks;
|
|
180
|
-
return timescale < 0n ? { ticks: -ticks, timescale: -timescale } : { ticks, timescale };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* The least timescale in which every one of `times` is a whole number of ticks.
|
|
185
|
-
* Times expressed in it add and compare without a common denominator being
|
|
186
|
-
* sought each time.
|
|
187
|
-
*
|
|
188
|
-
* @param {Iterable<MediaTime>} times
|
|
189
|
-
* @returns {bigint}
|
|
190
|
-
*/
|
|
191
|
-
export function commonTimescale(times) {
|
|
192
|
-
const distinct = new Set();
|
|
193
|
-
for (const time of times) {
|
|
194
|
-
distinct.add(time.timescale);
|
|
195
|
-
}
|
|
196
|
-
let common = 1n;
|
|
197
|
-
for (const timescale of distinct) {
|
|
198
|
-
common = common / gcd(common, timescale) * timescale;
|
|
199
|
-
}
|
|
200
|
-
return common;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* The same time in `timescale`, which must hold it as a whole number of ticks
|
|
205
|
-
* ({@link commonTimescale}).
|
|
206
|
-
*
|
|
207
|
-
* @param {MediaTime} time
|
|
208
|
-
* @param {bigint} timescale
|
|
209
|
-
* @returns {MediaTime}
|
|
210
|
-
*/
|
|
211
|
-
export function rescale(time, timescale) {
|
|
212
|
-
if (time.timescale === timescale) {
|
|
213
|
-
return time;
|
|
214
|
-
}
|
|
215
|
-
if (timescale % time.timescale !== 0n) {
|
|
216
|
-
throw new Error(`${timescale} ticks a second cannot hold a time counted in ${time.timescale}.`);
|
|
217
|
-
}
|
|
218
|
-
return { ticks: time.ticks * (timescale / time.timescale), timescale };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* The tick of `timescale` nearest to `time`, halves rounded up. For a forecast
|
|
223
|
-
* only: a measured time is never rounded.
|
|
224
|
-
*
|
|
225
|
-
* @param {MediaTime} time
|
|
226
|
-
* @param {bigint} timescale
|
|
227
|
-
* @returns {MediaTime}
|
|
228
|
-
*/
|
|
229
|
-
export function nearest(time, timescale) {
|
|
230
|
-
const numerator = time.ticks * timescale;
|
|
231
|
-
const quotient = numerator / time.timescale;
|
|
232
|
-
const remainder = numerator - quotient * time.timescale;
|
|
233
|
-
const floor = remainder < 0n ? quotient - 1n : quotient;
|
|
234
|
-
const rest = numerator - floor * time.timescale;
|
|
235
|
-
return { ticks: 2n * rest >= time.timescale ? floor + 1n : floor, timescale };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** @param {...MediaTime} times */
|
|
239
|
-
export function earliest(...times) {
|
|
240
|
-
return times.reduce((low, time) => (compare(time, low) < 0 ? time : low));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** @param {...MediaTime} times */
|
|
244
|
-
export function latest(...times) {
|
|
245
|
-
return times.reduce((high, time) => (compare(time, high) > 0 ? time : high));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Whether a gap between two buffered presentation ranges is closed by EVERY
|
|
250
|
-
* Media Source Extensions implementation this player runs on.
|
|
251
|
-
*
|
|
252
|
-
* The specification leaves it open: "The threshold for determining
|
|
253
|
-
* disjointness of track buffer ranges is implementation-specific"
|
|
254
|
-
* (https://w3c.github.io/media-source/#track-buffer-ranges). So the condition
|
|
255
|
-
* is the conjunction of what the three engines do, each read from its source:
|
|
256
|
-
*
|
|
257
|
-
* 1. Gecko — `TrackBuffersManager::InsertFrames` gives each inserted interval a
|
|
258
|
-
* fuzz of half `mLongestFrameDuration`, the longest frame since the last
|
|
259
|
-
* keyframe, and `IntervalSet::Add` joins intervals whose distance is at most
|
|
260
|
-
* the sum of their fuzzes (`Interval::Touches`); a merged interval keeps the
|
|
261
|
-
* larger fuzz (`Interval::Span`). Hence `2 * gap <= leftFrame + rightFrame`.
|
|
262
|
-
* 2. WebKit — a fixed {@link WEBKIT_TIME_FUDGE_FACTOR}.
|
|
263
|
-
* 3. Chromium — `SourceBufferRange::IsNextInPresentationSequence` accepts a
|
|
264
|
-
* frame starting within twice `max_interbuffer_distance_` of the highest
|
|
265
|
-
* frame start; that distance is the largest frame duration or decode
|
|
266
|
-
* distance appended so far, updated before ranges merge
|
|
267
|
-
* (`SourceBufferStream::Append`). The frame whose end is the left range's
|
|
268
|
-
* end starts no earlier than the highest start, so the distance to check is
|
|
269
|
-
* at most the gap plus that frame's duration. Condition 1 bounds the gap by
|
|
270
|
-
* the larger of the two frames, so both terms are within that distance and
|
|
271
|
-
* nothing further is required.
|
|
272
|
-
*
|
|
273
|
-
* Old iOS without Media Source plays HLS in its own closed player. Nothing can
|
|
274
|
-
* be derived for it, and this rule is not a statement about it.
|
|
275
|
-
*
|
|
276
|
-
* @param {MediaTime} gap - Right start minus left end.
|
|
277
|
-
* @param {MediaTime} leftFrame
|
|
278
|
-
* @param {MediaTime} rightFrame
|
|
279
|
-
* @returns {boolean}
|
|
280
|
-
*/
|
|
281
|
-
export function joins(gap, leftFrame, rightFrame) {
|
|
282
|
-
if (gap.ticks <= 0n) {
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
return compare(multiply(gap, 2n), add(leftFrame, rightFrame)) <= 0 &&
|
|
286
|
-
compare(gap, WEBKIT_TIME_FUDGE_FACTOR) <= 0;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Insert a range into a sorted list of disjoint ranges, joining every
|
|
291
|
-
* neighbour {@link joins} admits. Mutates `ranges`.
|
|
292
|
-
*
|
|
293
|
-
* @param {MediaRange[]} ranges
|
|
294
|
-
* @param {{ start: MediaTime, end: MediaTime, frame?: MediaTime }} range
|
|
295
|
-
* @returns {void}
|
|
296
|
-
*/
|
|
297
|
-
export function addRange(ranges, range) {
|
|
298
|
-
let next = { start: range.start, end: range.end, frame: range.frame ?? ZERO };
|
|
299
|
-
if (compare(next.end, next.start) <= 0) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
const merged = [];
|
|
303
|
-
let inserted = false;
|
|
304
|
-
for (const held of ranges) {
|
|
305
|
-
if (inserted) {
|
|
306
|
-
merged.push(held);
|
|
307
|
-
} else if (compare(held.end, next.start) < 0 && !joins(subtract(next.start, held.end), held.frame, next.frame)) {
|
|
308
|
-
merged.push(held);
|
|
309
|
-
} else if (compare(next.end, held.start) < 0 && !joins(subtract(held.start, next.end), next.frame, held.frame)) {
|
|
310
|
-
merged.push(next, held);
|
|
311
|
-
inserted = true;
|
|
312
|
-
} else {
|
|
313
|
-
next = {
|
|
314
|
-
start: earliest(held.start, next.start),
|
|
315
|
-
end: latest(held.end, next.end),
|
|
316
|
-
frame: latest(held.frame, next.frame)
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (!inserted) {
|
|
321
|
-
merged.push(next);
|
|
322
|
-
}
|
|
323
|
-
ranges.splice(0, ranges.length, ...merged);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* The end of the range that holds `time`, or `time` itself when none does.
|
|
328
|
-
*
|
|
329
|
-
* @param {MediaRange[]} ranges - Sorted and disjoint.
|
|
330
|
-
* @param {MediaTime} time
|
|
331
|
-
* @returns {MediaTime}
|
|
332
|
-
*/
|
|
333
|
-
export function contiguousEnd(ranges, time) {
|
|
334
|
-
for (const range of ranges) {
|
|
335
|
-
if (compare(range.start, time) <= 0 && compare(time, range.end) <= 0) {
|
|
336
|
-
return range.end;
|
|
337
|
-
}
|
|
338
|
-
if (compare(range.start, time) > 0) {
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return time;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Whether `[start, end)` lies inside one range.
|
|
347
|
-
*
|
|
348
|
-
* @param {MediaRange[]} ranges
|
|
349
|
-
* @param {MediaTime} start
|
|
350
|
-
* @param {MediaTime} end
|
|
351
|
-
* @returns {boolean}
|
|
352
|
-
*/
|
|
353
|
-
export function contains(ranges, start, end) {
|
|
354
|
-
return ranges.some((range) => compare(range.start, start) <= 0 && compare(end, range.end) <= 0);
|
|
355
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file Exact media time, and the rule by which a viewer's browser joins
|
|
3
|
+
* buffered media.
|
|
4
|
+
*
|
|
5
|
+
* A time is `{ ticks, timescale }`: `ticks / timescale` seconds, both `BigInt`.
|
|
6
|
+
* A file states its timestamps this way, and comparing two of them is integer
|
|
7
|
+
* arithmetic. Converting them to seconds first replaces an exact comparison
|
|
8
|
+
* with a rounded one — measured 2026-10-02 on a served film, seven joins whose
|
|
9
|
+
* ticks were equal read as `88.67299999999999` against `88.673` and were taken
|
|
10
|
+
* for holes.
|
|
11
|
+
*
|
|
12
|
+
* A value that arrives as a JavaScript number — a position or a buffered range
|
|
13
|
+
* from the page, a cut time from the playlist — is a binary fraction, and it is
|
|
14
|
+
* converted to the exact time it denotes ({@link fromSeconds}). No unit is
|
|
15
|
+
* chosen and nothing is rounded. Seconds are produced again only for a
|
|
16
|
+
* forecast, a page or a log ({@link toSeconds}).
|
|
17
|
+
*
|
|
18
|
+
* A range is `{ start, end, frame }`: the presentation interval `[start, end)`
|
|
19
|
+
* and `frame`, the coded-frame duration a browser is known to allow this range
|
|
20
|
+
* when it decides whether a neighbour joins it ({@link joins}). `frame` is zero
|
|
21
|
+
* for a range whose frames are not known, such as one the page reports holding.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @typedef {{ ticks: bigint, timescale: bigint }} MediaTime */
|
|
25
|
+
/** @typedef {{ start: MediaTime, end: MediaTime, frame: MediaTime }} MediaRange */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The gap WebKit closes between buffered ranges: `PlatformTimeRanges::
|
|
29
|
+
* timeFudgeFactor()`, applied by `PlatformTimeRanges::add` with
|
|
30
|
+
* `EliminateSmallGaps` to every sample a `TrackBuffer` adds.
|
|
31
|
+
* https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/graphics/PlatformTimeRanges.cpp
|
|
32
|
+
*
|
|
33
|
+
* @type {MediaTime}
|
|
34
|
+
*/
|
|
35
|
+
const WEBKIT_TIME_FUDGE_FACTOR = Object.freeze({ ticks: 2002n, timescale: 24000n });
|
|
36
|
+
|
|
37
|
+
/** @type {MediaTime} */
|
|
38
|
+
export const ZERO = Object.freeze({ ticks: 0n, timescale: 1n });
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How far a buffered range a page reports can lie from the exact time of the
|
|
42
|
+
* frames behind it. Chromium converts three values to whole microseconds before
|
|
43
|
+
* a buffered range exists — a sample's presentation time and its duration in
|
|
44
|
+
* `TimeDeltaFromRational` (media/formats/mp4/track_run_iterator.cc, rounding
|
|
45
|
+
* toward zero), and the page's `timestampOffset` — so each edge is off by less
|
|
46
|
+
* than one microsecond per conversion. Gecko and WebKit keep exact rationals
|
|
47
|
+
* until the value becomes a JavaScript number, which is far finer.
|
|
48
|
+
*
|
|
49
|
+
* @type {MediaTime}
|
|
50
|
+
*/
|
|
51
|
+
export const REPORTED_TIME_ERROR = Object.freeze({ ticks: 3n, timescale: 1_000_000n });
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {bigint} ticks
|
|
55
|
+
* @param {bigint} timescale
|
|
56
|
+
* @returns {MediaTime}
|
|
57
|
+
*/
|
|
58
|
+
export function mediaTime(ticks, timescale) {
|
|
59
|
+
if (!(timescale > 0n)) {
|
|
60
|
+
throw new Error(`A timescale must be positive, not ${timescale}.`);
|
|
61
|
+
}
|
|
62
|
+
return { ticks, timescale };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The exact time a finite number of seconds denotes. An IEEE 754 double is
|
|
67
|
+
* `mantissa * 2^exponent`, so it is a fraction whose denominator is a power of
|
|
68
|
+
* two; this returns that fraction.
|
|
69
|
+
*
|
|
70
|
+
* @param {number} seconds
|
|
71
|
+
* @returns {MediaTime}
|
|
72
|
+
*/
|
|
73
|
+
export function fromSeconds(seconds) {
|
|
74
|
+
if (!Number.isFinite(seconds)) {
|
|
75
|
+
throw new Error(`Seconds must be finite, not ${seconds}.`);
|
|
76
|
+
}
|
|
77
|
+
if (seconds === 0) {
|
|
78
|
+
return ZERO;
|
|
79
|
+
}
|
|
80
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
81
|
+
view.setFloat64(0, seconds);
|
|
82
|
+
const bits = view.getBigUint64(0);
|
|
83
|
+
const sign = bits >> 63n === 1n ? -1n : 1n;
|
|
84
|
+
const biasedExponent = (bits >> 52n) & 0x7ffn;
|
|
85
|
+
let mantissa = bits & 0xfffffffffffffn;
|
|
86
|
+
let exponent;
|
|
87
|
+
if (biasedExponent === 0n) {
|
|
88
|
+
exponent = -1074n;
|
|
89
|
+
} else {
|
|
90
|
+
mantissa |= 0x10000000000000n;
|
|
91
|
+
exponent = biasedExponent - 1075n;
|
|
92
|
+
}
|
|
93
|
+
while (exponent < 0n && mantissa % 2n === 0n) {
|
|
94
|
+
mantissa /= 2n;
|
|
95
|
+
exponent += 1n;
|
|
96
|
+
}
|
|
97
|
+
return exponent >= 0n
|
|
98
|
+
? { ticks: sign * mantissa * (1n << exponent), timescale: 1n }
|
|
99
|
+
: { ticks: sign * mantissa, timescale: 1n << -exponent };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Seconds, for a forecast, a page or a log.
|
|
104
|
+
*
|
|
105
|
+
* @param {MediaTime} time
|
|
106
|
+
* @returns {number}
|
|
107
|
+
*/
|
|
108
|
+
export function toSeconds(time) {
|
|
109
|
+
return Number(time.ticks) / Number(time.timescale);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function gcd(left, right) {
|
|
113
|
+
let a = left < 0n ? -left : left;
|
|
114
|
+
let b = right;
|
|
115
|
+
while (b !== 0n) {
|
|
116
|
+
[a, b] = [b, a % b];
|
|
117
|
+
}
|
|
118
|
+
return a;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {MediaTime} left
|
|
123
|
+
* @param {MediaTime} right
|
|
124
|
+
* @returns {-1 | 0 | 1}
|
|
125
|
+
*/
|
|
126
|
+
export function compare(left, right) {
|
|
127
|
+
if (left.timescale === right.timescale) {
|
|
128
|
+
return left.ticks < right.ticks ? -1 : left.ticks > right.ticks ? 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
const a = left.ticks * right.timescale;
|
|
131
|
+
const b = right.ticks * left.timescale;
|
|
132
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {MediaTime} left
|
|
137
|
+
* @param {MediaTime} right
|
|
138
|
+
* @returns {MediaTime}
|
|
139
|
+
*/
|
|
140
|
+
export function add(left, right) {
|
|
141
|
+
if (left.timescale === right.timescale) {
|
|
142
|
+
return { ticks: left.ticks + right.ticks, timescale: left.timescale };
|
|
143
|
+
}
|
|
144
|
+
const common = left.timescale / gcd(left.timescale, right.timescale) * right.timescale;
|
|
145
|
+
return {
|
|
146
|
+
ticks: left.ticks * (common / left.timescale) + right.ticks * (common / right.timescale),
|
|
147
|
+
timescale: common
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {MediaTime} left
|
|
153
|
+
* @param {MediaTime} right
|
|
154
|
+
* @returns {MediaTime}
|
|
155
|
+
*/
|
|
156
|
+
export function subtract(left, right) {
|
|
157
|
+
return add(left, { ticks: -right.ticks, timescale: right.timescale });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {MediaTime} time
|
|
162
|
+
* @param {bigint} factor
|
|
163
|
+
* @returns {MediaTime}
|
|
164
|
+
*/
|
|
165
|
+
export function multiply(time, factor) {
|
|
166
|
+
return { ticks: time.ticks * factor, timescale: time.timescale };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `time * numerator / denominator`, exactly.
|
|
171
|
+
*
|
|
172
|
+
* @param {MediaTime} time
|
|
173
|
+
* @param {MediaTime} numerator
|
|
174
|
+
* @param {MediaTime} denominator - Not zero.
|
|
175
|
+
* @returns {MediaTime}
|
|
176
|
+
*/
|
|
177
|
+
export function scale(time, numerator, denominator) {
|
|
178
|
+
const ticks = time.ticks * numerator.ticks * denominator.timescale;
|
|
179
|
+
const timescale = time.timescale * numerator.timescale * denominator.ticks;
|
|
180
|
+
return timescale < 0n ? { ticks: -ticks, timescale: -timescale } : { ticks, timescale };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The least timescale in which every one of `times` is a whole number of ticks.
|
|
185
|
+
* Times expressed in it add and compare without a common denominator being
|
|
186
|
+
* sought each time.
|
|
187
|
+
*
|
|
188
|
+
* @param {Iterable<MediaTime>} times
|
|
189
|
+
* @returns {bigint}
|
|
190
|
+
*/
|
|
191
|
+
export function commonTimescale(times) {
|
|
192
|
+
const distinct = new Set();
|
|
193
|
+
for (const time of times) {
|
|
194
|
+
distinct.add(time.timescale);
|
|
195
|
+
}
|
|
196
|
+
let common = 1n;
|
|
197
|
+
for (const timescale of distinct) {
|
|
198
|
+
common = common / gcd(common, timescale) * timescale;
|
|
199
|
+
}
|
|
200
|
+
return common;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The same time in `timescale`, which must hold it as a whole number of ticks
|
|
205
|
+
* ({@link commonTimescale}).
|
|
206
|
+
*
|
|
207
|
+
* @param {MediaTime} time
|
|
208
|
+
* @param {bigint} timescale
|
|
209
|
+
* @returns {MediaTime}
|
|
210
|
+
*/
|
|
211
|
+
export function rescale(time, timescale) {
|
|
212
|
+
if (time.timescale === timescale) {
|
|
213
|
+
return time;
|
|
214
|
+
}
|
|
215
|
+
if (timescale % time.timescale !== 0n) {
|
|
216
|
+
throw new Error(`${timescale} ticks a second cannot hold a time counted in ${time.timescale}.`);
|
|
217
|
+
}
|
|
218
|
+
return { ticks: time.ticks * (timescale / time.timescale), timescale };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The tick of `timescale` nearest to `time`, halves rounded up. For a forecast
|
|
223
|
+
* only: a measured time is never rounded.
|
|
224
|
+
*
|
|
225
|
+
* @param {MediaTime} time
|
|
226
|
+
* @param {bigint} timescale
|
|
227
|
+
* @returns {MediaTime}
|
|
228
|
+
*/
|
|
229
|
+
export function nearest(time, timescale) {
|
|
230
|
+
const numerator = time.ticks * timescale;
|
|
231
|
+
const quotient = numerator / time.timescale;
|
|
232
|
+
const remainder = numerator - quotient * time.timescale;
|
|
233
|
+
const floor = remainder < 0n ? quotient - 1n : quotient;
|
|
234
|
+
const rest = numerator - floor * time.timescale;
|
|
235
|
+
return { ticks: 2n * rest >= time.timescale ? floor + 1n : floor, timescale };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @param {...MediaTime} times */
|
|
239
|
+
export function earliest(...times) {
|
|
240
|
+
return times.reduce((low, time) => (compare(time, low) < 0 ? time : low));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @param {...MediaTime} times */
|
|
244
|
+
export function latest(...times) {
|
|
245
|
+
return times.reduce((high, time) => (compare(time, high) > 0 ? time : high));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Whether a gap between two buffered presentation ranges is closed by EVERY
|
|
250
|
+
* Media Source Extensions implementation this player runs on.
|
|
251
|
+
*
|
|
252
|
+
* The specification leaves it open: "The threshold for determining
|
|
253
|
+
* disjointness of track buffer ranges is implementation-specific"
|
|
254
|
+
* (https://w3c.github.io/media-source/#track-buffer-ranges). So the condition
|
|
255
|
+
* is the conjunction of what the three engines do, each read from its source:
|
|
256
|
+
*
|
|
257
|
+
* 1. Gecko — `TrackBuffersManager::InsertFrames` gives each inserted interval a
|
|
258
|
+
* fuzz of half `mLongestFrameDuration`, the longest frame since the last
|
|
259
|
+
* keyframe, and `IntervalSet::Add` joins intervals whose distance is at most
|
|
260
|
+
* the sum of their fuzzes (`Interval::Touches`); a merged interval keeps the
|
|
261
|
+
* larger fuzz (`Interval::Span`). Hence `2 * gap <= leftFrame + rightFrame`.
|
|
262
|
+
* 2. WebKit — a fixed {@link WEBKIT_TIME_FUDGE_FACTOR}.
|
|
263
|
+
* 3. Chromium — `SourceBufferRange::IsNextInPresentationSequence` accepts a
|
|
264
|
+
* frame starting within twice `max_interbuffer_distance_` of the highest
|
|
265
|
+
* frame start; that distance is the largest frame duration or decode
|
|
266
|
+
* distance appended so far, updated before ranges merge
|
|
267
|
+
* (`SourceBufferStream::Append`). The frame whose end is the left range's
|
|
268
|
+
* end starts no earlier than the highest start, so the distance to check is
|
|
269
|
+
* at most the gap plus that frame's duration. Condition 1 bounds the gap by
|
|
270
|
+
* the larger of the two frames, so both terms are within that distance and
|
|
271
|
+
* nothing further is required.
|
|
272
|
+
*
|
|
273
|
+
* Old iOS without Media Source plays HLS in its own closed player. Nothing can
|
|
274
|
+
* be derived for it, and this rule is not a statement about it.
|
|
275
|
+
*
|
|
276
|
+
* @param {MediaTime} gap - Right start minus left end.
|
|
277
|
+
* @param {MediaTime} leftFrame
|
|
278
|
+
* @param {MediaTime} rightFrame
|
|
279
|
+
* @returns {boolean}
|
|
280
|
+
*/
|
|
281
|
+
export function joins(gap, leftFrame, rightFrame) {
|
|
282
|
+
if (gap.ticks <= 0n) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
return compare(multiply(gap, 2n), add(leftFrame, rightFrame)) <= 0 &&
|
|
286
|
+
compare(gap, WEBKIT_TIME_FUDGE_FACTOR) <= 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Insert a range into a sorted list of disjoint ranges, joining every
|
|
291
|
+
* neighbour {@link joins} admits. Mutates `ranges`.
|
|
292
|
+
*
|
|
293
|
+
* @param {MediaRange[]} ranges
|
|
294
|
+
* @param {{ start: MediaTime, end: MediaTime, frame?: MediaTime }} range
|
|
295
|
+
* @returns {void}
|
|
296
|
+
*/
|
|
297
|
+
export function addRange(ranges, range) {
|
|
298
|
+
let next = { start: range.start, end: range.end, frame: range.frame ?? ZERO };
|
|
299
|
+
if (compare(next.end, next.start) <= 0) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const merged = [];
|
|
303
|
+
let inserted = false;
|
|
304
|
+
for (const held of ranges) {
|
|
305
|
+
if (inserted) {
|
|
306
|
+
merged.push(held);
|
|
307
|
+
} else if (compare(held.end, next.start) < 0 && !joins(subtract(next.start, held.end), held.frame, next.frame)) {
|
|
308
|
+
merged.push(held);
|
|
309
|
+
} else if (compare(next.end, held.start) < 0 && !joins(subtract(held.start, next.end), next.frame, held.frame)) {
|
|
310
|
+
merged.push(next, held);
|
|
311
|
+
inserted = true;
|
|
312
|
+
} else {
|
|
313
|
+
next = {
|
|
314
|
+
start: earliest(held.start, next.start),
|
|
315
|
+
end: latest(held.end, next.end),
|
|
316
|
+
frame: latest(held.frame, next.frame)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (!inserted) {
|
|
321
|
+
merged.push(next);
|
|
322
|
+
}
|
|
323
|
+
ranges.splice(0, ranges.length, ...merged);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The end of the range that holds `time`, or `time` itself when none does.
|
|
328
|
+
*
|
|
329
|
+
* @param {MediaRange[]} ranges - Sorted and disjoint.
|
|
330
|
+
* @param {MediaTime} time
|
|
331
|
+
* @returns {MediaTime}
|
|
332
|
+
*/
|
|
333
|
+
export function contiguousEnd(ranges, time) {
|
|
334
|
+
for (const range of ranges) {
|
|
335
|
+
if (compare(range.start, time) <= 0 && compare(time, range.end) <= 0) {
|
|
336
|
+
return range.end;
|
|
337
|
+
}
|
|
338
|
+
if (compare(range.start, time) > 0) {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return time;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whether `[start, end)` lies inside one range.
|
|
347
|
+
*
|
|
348
|
+
* @param {MediaRange[]} ranges
|
|
349
|
+
* @param {MediaTime} start
|
|
350
|
+
* @param {MediaTime} end
|
|
351
|
+
* @returns {boolean}
|
|
352
|
+
*/
|
|
353
|
+
export function contains(ranges, start, end) {
|
|
354
|
+
return ranges.some((range) => compare(range.start, start) <= 0 && compare(end, range.end) <= 0);
|
|
355
|
+
}
|