@torrent-tv/proxy 2.87.7 → 2.87.8
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 +4 -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/biome.json +411 -411
- 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/files/get.js +100 -100
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- 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 +129 -129
- package/routes/api/transcode-sessions/post.js +160 -160
- 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 +361 -361
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +84 -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/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +332 -332
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +776 -776
- package/services/encode/EncodeRuns.js +1229 -1229
- 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 +755 -755
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2162 -2162
- 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 +416 -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 +1083 -1083
- 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 +254 -254
- package/services/encode/quality/output-format.js +266 -266
- 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 +747 -747
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +437 -437
- package/services/encode/segment-formats/index.js +77 -77
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +311 -311
- 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/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +603 -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 +1244 -1244
- package/services/media/container/SubtitleFileContainer.js +322 -322
- 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/tracks/AudioTrack.js +131 -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/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 +1146 -1146
- package/services/server/ViewerRequests.js +522 -522
- package/services/server/WaitLedger.js +142 -142
- 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 +983 -983
- 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 +516 -516
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1880 -1880
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1196 -1196
- package/services/storage/wire.js +175 -175
- package/services/torrent/Contents.js +324 -324
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/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/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/data-channel-handler.js +21 -8
- 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 +657 -657
- 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-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 -0
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +35 -35
- 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 +161 -161
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-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 +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 +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 +204 -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 +381 -381
- 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 +560 -560
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +318 -318
- 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/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 +1229 -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 +102 -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/source-file.test.js +133 -133
- package/test/source-files-route.test.js +103 -103
- 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 +182 -182
- 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 +407 -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,107 +1,107 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Fetching the region under a viewer's resume position.
|
|
3
|
-
*
|
|
4
|
-
* All that is left of a file that used to read containers here as well. The
|
|
5
|
-
* parsing moved to the main thread on 2026-09-15: a container is built from one
|
|
6
|
-
* function, `readRange(start, end)`, the pieces behind it live in shared memory,
|
|
7
|
-
* and the reason it had been put here — "the main thread cannot open a read
|
|
8
|
-
* stream on one of its files" — was true of WebTorrent's own API and not of the
|
|
9
|
-
* bytes. Three commands, a second `ContainerOrchestrator` in this thread, and
|
|
10
|
-
* every answer carried back over the channel went with it.
|
|
11
|
-
*
|
|
12
|
-
* What stays is the part that really is the torrent's: turning a position in
|
|
13
|
-
* seconds into a byte offset and pulling that region off the swarm. How long
|
|
14
|
-
* the file runs is TOLD to it, because that is what the file states about
|
|
15
|
-
* itself and this thread no longer reads such things.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { logger } from "../../../utils/logger.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* How much of the file to pull in under the viewer's resume position.
|
|
31
|
-
*
|
|
32
|
-
* One piece of a video torrent is 4-16 MB and a resume lands anywhere inside
|
|
33
|
-
* one, so anything smaller would still leave the encoder waiting for the piece
|
|
34
|
-
* it starts in. Eight megabytes covers that piece and usually the next.
|
|
35
|
-
*/
|
|
36
|
-
const RESUME_REGION_BYTES = 8 * 1024 * 1024;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Where a position in seconds falls in a file, in bytes.
|
|
40
|
-
*
|
|
41
|
-
* Proportional, and therefore approximate on a variable bitrate — which is what
|
|
42
|
-
* it is for: a prefetch that puts the swarm to work on roughly the right place
|
|
43
|
-
* while the plan and the session are still being built. The encoder's own read
|
|
44
|
-
* asks for the exact bytes a moment later and corrects it.
|
|
45
|
-
*
|
|
46
|
-
* A position past the end is clamped to the end rather than refused: a resume
|
|
47
|
-
* position can outlive the file it was recorded against, and reading the last
|
|
48
|
-
* bytes is harmless where reading past them is an error.
|
|
49
|
-
*
|
|
50
|
-
* @param {number} fileLength
|
|
51
|
-
* @param {number} durationSeconds
|
|
52
|
-
* @param {number} positionSeconds
|
|
53
|
-
* @returns {number}
|
|
54
|
-
*/
|
|
55
|
-
export function resumeByteOffset(fileLength, durationSeconds, positionSeconds) {
|
|
56
|
-
if (!(fileLength > 0) || !(durationSeconds > 0) || !(positionSeconds > 0)) {
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
59
|
-
const within = Math.min(positionSeconds, durationSeconds);
|
|
60
|
-
return Math.min(fileLength - 1, Math.floor((fileLength * within) / durationSeconds));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Start fetching the region a viewer is about to resume at.
|
|
65
|
-
*
|
|
66
|
-
* Where that region IS can only be worked out from two numbers the file itself
|
|
67
|
-
* holds — its length and its duration — so this belongs beside the container
|
|
68
|
-
* read rather than in the route: the route knows a position in seconds and
|
|
69
|
-
* nothing else. The conversion is proportional and therefore approximate on a
|
|
70
|
-
* variable bitrate; it is a prefetch, and the encoder's own read corrects it.
|
|
71
|
-
*
|
|
72
|
-
* @param {object} torrent
|
|
73
|
-
* @param {number} fileIndex
|
|
74
|
-
* @param {string} sourceKey
|
|
75
|
-
* @param {number} positionSeconds
|
|
76
|
-
* @param {{ prefetchEdges?: () => Promise<unknown>, fetchRegion?: (start: number, bytes: number) => Promise<unknown> }} options
|
|
77
|
-
* @returns {Promise<boolean>} Whether a region was asked for.
|
|
78
|
-
*/
|
|
79
|
-
export async function warmResumePosition(torrent, fileIndex, sourceKey, positionSeconds, options = {}) {
|
|
80
|
-
const file = torrent?.files?.[fileIndex];
|
|
81
|
-
if (!file || !(positionSeconds > 0) || typeof options.fetchRegion !== "function") {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
// TOLD, not worked out here. How long a file runs is what the file states
|
|
85
|
-
// about itself, and that is read on the main thread now; this one turns a
|
|
86
|
-
// position into a byte offset and fetches the region under it, which is all
|
|
87
|
-
// of the job that is actually the torrent's.
|
|
88
|
-
const duration = Number(options.durationSeconds);
|
|
89
|
-
if (!Number.isFinite(duration) || duration <= 0) {
|
|
90
|
-
logger.info(
|
|
91
|
-
`warm ${sourceKey.slice(0, 8)}: "${String(file.name).slice(0, 40)}" does not declare its ` +
|
|
92
|
-
"duration, so where the viewer's position falls in it cannot be worked out — " +
|
|
93
|
-
"the region under it is left to the encoder's own read"
|
|
94
|
-
);
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
const at = resumeByteOffset(file.length, duration, positionSeconds);
|
|
98
|
-
logger.info(
|
|
99
|
-
`warm ${sourceKey.slice(0, 8)}: fetching ${(RESUME_REGION_BYTES / (1024 * 1024)).toFixed(0)}MB under the ` +
|
|
100
|
-
`viewer's position ${positionSeconds.toFixed(1)}s of ${duration.toFixed(1)}s, which is ` +
|
|
101
|
-
`${(at / (1024 * 1024)).toFixed(1)}MB into "${String(file.name).slice(0, 40)}"`
|
|
102
|
-
);
|
|
103
|
-
await options.fetchRegion(at, RESUME_REGION_BYTES);
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Fetching the region under a viewer's resume position.
|
|
3
|
+
*
|
|
4
|
+
* All that is left of a file that used to read containers here as well. The
|
|
5
|
+
* parsing moved to the main thread on 2026-09-15: a container is built from one
|
|
6
|
+
* function, `readRange(start, end)`, the pieces behind it live in shared memory,
|
|
7
|
+
* and the reason it had been put here — "the main thread cannot open a read
|
|
8
|
+
* stream on one of its files" — was true of WebTorrent's own API and not of the
|
|
9
|
+
* bytes. Three commands, a second `ContainerOrchestrator` in this thread, and
|
|
10
|
+
* every answer carried back over the channel went with it.
|
|
11
|
+
*
|
|
12
|
+
* What stays is the part that really is the torrent's: turning a position in
|
|
13
|
+
* seconds into a byte offset and pulling that region off the swarm. How long
|
|
14
|
+
* the file runs is TOLD to it, because that is what the file states about
|
|
15
|
+
* itself and this thread no longer reads such things.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { logger } from "../../../utils/logger.js";
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How much of the file to pull in under the viewer's resume position.
|
|
31
|
+
*
|
|
32
|
+
* One piece of a video torrent is 4-16 MB and a resume lands anywhere inside
|
|
33
|
+
* one, so anything smaller would still leave the encoder waiting for the piece
|
|
34
|
+
* it starts in. Eight megabytes covers that piece and usually the next.
|
|
35
|
+
*/
|
|
36
|
+
const RESUME_REGION_BYTES = 8 * 1024 * 1024;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Where a position in seconds falls in a file, in bytes.
|
|
40
|
+
*
|
|
41
|
+
* Proportional, and therefore approximate on a variable bitrate — which is what
|
|
42
|
+
* it is for: a prefetch that puts the swarm to work on roughly the right place
|
|
43
|
+
* while the plan and the session are still being built. The encoder's own read
|
|
44
|
+
* asks for the exact bytes a moment later and corrects it.
|
|
45
|
+
*
|
|
46
|
+
* A position past the end is clamped to the end rather than refused: a resume
|
|
47
|
+
* position can outlive the file it was recorded against, and reading the last
|
|
48
|
+
* bytes is harmless where reading past them is an error.
|
|
49
|
+
*
|
|
50
|
+
* @param {number} fileLength
|
|
51
|
+
* @param {number} durationSeconds
|
|
52
|
+
* @param {number} positionSeconds
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function resumeByteOffset(fileLength, durationSeconds, positionSeconds) {
|
|
56
|
+
if (!(fileLength > 0) || !(durationSeconds > 0) || !(positionSeconds > 0)) {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
const within = Math.min(positionSeconds, durationSeconds);
|
|
60
|
+
return Math.min(fileLength - 1, Math.floor((fileLength * within) / durationSeconds));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Start fetching the region a viewer is about to resume at.
|
|
65
|
+
*
|
|
66
|
+
* Where that region IS can only be worked out from two numbers the file itself
|
|
67
|
+
* holds — its length and its duration — so this belongs beside the container
|
|
68
|
+
* read rather than in the route: the route knows a position in seconds and
|
|
69
|
+
* nothing else. The conversion is proportional and therefore approximate on a
|
|
70
|
+
* variable bitrate; it is a prefetch, and the encoder's own read corrects it.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} torrent
|
|
73
|
+
* @param {number} fileIndex
|
|
74
|
+
* @param {string} sourceKey
|
|
75
|
+
* @param {number} positionSeconds
|
|
76
|
+
* @param {{ prefetchEdges?: () => Promise<unknown>, fetchRegion?: (start: number, bytes: number) => Promise<unknown> }} options
|
|
77
|
+
* @returns {Promise<boolean>} Whether a region was asked for.
|
|
78
|
+
*/
|
|
79
|
+
export async function warmResumePosition(torrent, fileIndex, sourceKey, positionSeconds, options = {}) {
|
|
80
|
+
const file = torrent?.files?.[fileIndex];
|
|
81
|
+
if (!file || !(positionSeconds > 0) || typeof options.fetchRegion !== "function") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// TOLD, not worked out here. How long a file runs is what the file states
|
|
85
|
+
// about itself, and that is read on the main thread now; this one turns a
|
|
86
|
+
// position into a byte offset and fetches the region under it, which is all
|
|
87
|
+
// of the job that is actually the torrent's.
|
|
88
|
+
const duration = Number(options.durationSeconds);
|
|
89
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
90
|
+
logger.info(
|
|
91
|
+
`warm ${sourceKey.slice(0, 8)}: "${String(file.name).slice(0, 40)}" does not declare its ` +
|
|
92
|
+
"duration, so where the viewer's position falls in it cannot be worked out — " +
|
|
93
|
+
"the region under it is left to the encoder's own read"
|
|
94
|
+
);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const at = resumeByteOffset(file.length, duration, positionSeconds);
|
|
98
|
+
logger.info(
|
|
99
|
+
`warm ${sourceKey.slice(0, 8)}: fetching ${(RESUME_REGION_BYTES / (1024 * 1024)).toFixed(0)}MB under the ` +
|
|
100
|
+
`viewer's position ${positionSeconds.toFixed(1)}s of ${duration.toFixed(1)}s, which is ` +
|
|
101
|
+
`${(at / (1024 * 1024)).toFixed(1)}MB into "${String(file.name).slice(0, 40)}"`
|
|
102
|
+
);
|
|
103
|
+
await options.fetchRegion(at, RESUME_REGION_BYTES);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file `webrtc-polyfill`'s interface, served by a pure-JavaScript WebRTC stack.
|
|
3
|
-
*
|
|
4
|
-
* **Why this exists.** node-datachannel is native, and its native side is not
|
|
5
|
-
* safe to use from two V8 isolates at once: the process aborts with
|
|
6
|
-
* `HandleScope: Entering the V8 API without proper locking in place`. Measured
|
|
7
|
-
* on win32/x64 and linux/arm64 alike — one isolate is fine (either the main
|
|
8
|
-
* thread or a worker), two at the same time is fatal, and preloading in both
|
|
9
|
-
* does not help. The upstream issue about workers (#129) was closed in 0.4.0
|
|
10
|
-
* but only covers use from a worker ALONE, which does work.
|
|
11
|
-
*
|
|
12
|
-
* We need it twice: `transport/webrtc-manager.js` runs the video channel to the browser on
|
|
13
|
-
* the main thread, and the torrent client — which is now on its own thread —
|
|
14
|
-
* creates peer connections of its own to announce on `wss://` trackers. Before
|
|
15
|
-
* the thread split both lived in one isolate and nothing was wrong; afterwards
|
|
16
|
-
* any torrent carrying a wss tracker took the whole proxy down.
|
|
17
|
-
*
|
|
18
|
-
* So the native stack stays where it earns its keep — the main thread, carrying
|
|
19
|
-
* video — and the torrent's trackers get a JavaScript implementation, where the
|
|
20
|
-
* traffic is a handful of signalling messages. `services/torrent/worker/worker.js`
|
|
21
|
-
* installs a module resolution hook that points `webrtc-polyfill` here; nothing
|
|
22
|
-
* outside the worker thread is affected, and no dependency is patched (the addon
|
|
23
|
-
* installs with `--ignore-scripts`, so a postinstall patch would never run).
|
|
24
|
-
*
|
|
25
|
-
* **What the shim has to fix.** werift's peer connection matches the interface
|
|
26
|
-
* `simple-peer` expects, but its data channel differs in two ways that matter:
|
|
27
|
-
*
|
|
28
|
-
* - it has no `binaryType`, and hands `onmessage` a `Buffer`. `simple-peer`
|
|
29
|
-
* only recognises `ArrayBuffer`; anything else goes through `text2arr`,
|
|
30
|
-
* which would corrupt every byte of torrent payload.
|
|
31
|
-
* - it has no `onbufferedamountlow`. `simple-peer` builds its backpressure on
|
|
32
|
-
* that event, so without it a send that hits the high-water mark never
|
|
33
|
-
* resumes.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
RTCPeerConnection as WeriftPeerConnection,
|
|
38
|
-
RTCIceCandidate
|
|
39
|
-
} from "werift";
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* A session description built the way browsers build it — from one object.
|
|
43
|
-
*
|
|
44
|
-
* werift's own class takes two positional arguments, `(sdp, type)`. Callers
|
|
45
|
-
* written against the browser pass `{ type, sdp }`, so with werift's class the
|
|
46
|
-
* type lands in the sdp slot and the description is rejected: field 2026-08-03,
|
|
47
|
-
* wss announces succeeded but every peer connection died with "Connection
|
|
48
|
-
* error: invalid sessionDescription".
|
|
49
|
-
*
|
|
50
|
-
* `RTCIceCandidate` needs no such treatment — werift already takes an object.
|
|
51
|
-
*/
|
|
52
|
-
class ShimSessionDescription {
|
|
53
|
-
/**
|
|
54
|
-
* @param {{ type?: string, sdp?: string } | string} init
|
|
55
|
-
* @param {string} [type] - Tolerated for callers using werift's own order.
|
|
56
|
-
*/
|
|
57
|
-
constructor(init, type) {
|
|
58
|
-
if (typeof init === "string") {
|
|
59
|
-
this.sdp = init;
|
|
60
|
-
this.type = type;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
this.type = init?.type;
|
|
64
|
-
this.sdp = init?.sdp;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
toJSON() {
|
|
68
|
-
return { type: this.type, sdp: this.sdp };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* A werift data channel wearing the browser's interface.
|
|
74
|
-
*
|
|
75
|
-
* Only what `simple-peer` touches is implemented — promising more would be
|
|
76
|
-
* pretending, since anything else has no caller and would never be exercised.
|
|
77
|
-
*/
|
|
78
|
-
class ShimDataChannel {
|
|
79
|
-
#channel;
|
|
80
|
-
/** @type {"blob" | "arraybuffer"} */
|
|
81
|
-
binaryType = "blob";
|
|
82
|
-
/** @type {((event: { data: ArrayBuffer | Buffer }) => void) | null} */
|
|
83
|
-
onmessage = null;
|
|
84
|
-
/** @type {(() => void) | null} */
|
|
85
|
-
onopen = null;
|
|
86
|
-
/** @type {(() => void) | null} */
|
|
87
|
-
onclose = null;
|
|
88
|
-
/** @type {((event: { error?: Error }) => void) | null} */
|
|
89
|
-
onerror = null;
|
|
90
|
-
/** @type {(() => void) | null} */
|
|
91
|
-
onbufferedamountlow = null;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {object} channel - werift's `RTCDataChannel`.
|
|
95
|
-
*/
|
|
96
|
-
constructor(channel) {
|
|
97
|
-
this.#channel = channel;
|
|
98
|
-
|
|
99
|
-
channel.onmessage = (event) => {
|
|
100
|
-
const data = event?.data ?? event;
|
|
101
|
-
this.onmessage?.({ data: this.#toWireFormat(data) });
|
|
102
|
-
};
|
|
103
|
-
channel.onopen = () => this.onopen?.();
|
|
104
|
-
channel.onclose = () => this.onclose?.();
|
|
105
|
-
channel.onerror = (event) => this.onerror?.(event ?? {});
|
|
106
|
-
|
|
107
|
-
// werift reports this as an observable rather than a handler property.
|
|
108
|
-
channel.bufferedAmountLow?.subscribe?.(() => this.onbufferedamountlow?.());
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Match what a browser would deliver for the requested `binaryType`.
|
|
113
|
-
*
|
|
114
|
-
* `simple-peer` checks `instanceof ArrayBuffer` and sends everything else
|
|
115
|
-
* through a text decoder, so a `Buffer` handed over unchanged arrives
|
|
116
|
-
* mangled.
|
|
117
|
-
*
|
|
118
|
-
* @param {unknown} data
|
|
119
|
-
* @returns {ArrayBuffer | unknown}
|
|
120
|
-
*/
|
|
121
|
-
#toWireFormat(data) {
|
|
122
|
-
if (this.binaryType !== "arraybuffer" || typeof data === "string") {
|
|
123
|
-
return data;
|
|
124
|
-
}
|
|
125
|
-
if (ArrayBuffer.isView(data)) {
|
|
126
|
-
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
127
|
-
}
|
|
128
|
-
return data;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
get label() {
|
|
132
|
-
return this.#channel.label;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
get readyState() {
|
|
136
|
-
return this.#channel.readyState;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
get bufferedAmount() {
|
|
140
|
-
return this.#channel.bufferedAmount;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
get bufferedAmountLowThreshold() {
|
|
144
|
-
return this.#channel.bufferedAmountLowThreshold;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
set bufferedAmountLowThreshold(value) {
|
|
148
|
-
this.#channel.bufferedAmountLowThreshold = value;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @param {string | ArrayBuffer | ArrayBufferView} data
|
|
153
|
-
* @returns {void}
|
|
154
|
-
*/
|
|
155
|
-
send(data) {
|
|
156
|
-
if (typeof data === "string") {
|
|
157
|
-
this.#channel.send(data);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
this.#channel.send(ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
close() {
|
|
164
|
-
this.#channel.close();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* werift's peer connection, handing out data channels that carry the browser's
|
|
170
|
-
* interface. Everything else is inherited unchanged — the peer connection side
|
|
171
|
-
* already matches.
|
|
172
|
-
*/
|
|
173
|
-
class ShimPeerConnection extends WeriftPeerConnection {
|
|
174
|
-
/**
|
|
175
|
-
* @param {...unknown} args - Passed through to werift.
|
|
176
|
-
*/
|
|
177
|
-
constructor(...args) {
|
|
178
|
-
super(...args);
|
|
179
|
-
|
|
180
|
-
// `ondatachannel` has to be redefined on the instance, not declared as an
|
|
181
|
-
// accessor on this class: werift assigns it as an own field in its own
|
|
182
|
-
// constructor, and an own property shadows a prototype accessor — so a
|
|
183
|
-
// subclass setter is simply never called, and the caller receives werift's
|
|
184
|
-
// bare channel instead of the wrapped one.
|
|
185
|
-
let handler = null;
|
|
186
|
-
const deliver = (event) => {
|
|
187
|
-
handler?.({ ...event, channel: new ShimDataChannel(event?.channel ?? event) });
|
|
188
|
-
};
|
|
189
|
-
Object.defineProperty(this, "ondatachannel", {
|
|
190
|
-
configurable: true,
|
|
191
|
-
// werift reads this property to dispatch, so it must hand back the
|
|
192
|
-
// wrapper rather than what the caller assigned.
|
|
193
|
-
get: () => (handler ? deliver : null),
|
|
194
|
-
set: (value) => {
|
|
195
|
-
handler = value;
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* @param {string} label
|
|
202
|
-
* @param {object} [options]
|
|
203
|
-
* @returns {ShimDataChannel}
|
|
204
|
-
*/
|
|
205
|
-
createDataChannel(label, options) {
|
|
206
|
-
return new ShimDataChannel(super.createDataChannel(label, options));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export {
|
|
211
|
-
ShimPeerConnection as RTCPeerConnection,
|
|
212
|
-
ShimSessionDescription as RTCSessionDescription,
|
|
213
|
-
RTCIceCandidate
|
|
214
|
-
};
|
|
215
|
-
export default {
|
|
216
|
-
RTCPeerConnection: ShimPeerConnection,
|
|
217
|
-
RTCSessionDescription: ShimSessionDescription,
|
|
218
|
-
RTCIceCandidate
|
|
219
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file `webrtc-polyfill`'s interface, served by a pure-JavaScript WebRTC stack.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists.** node-datachannel is native, and its native side is not
|
|
5
|
+
* safe to use from two V8 isolates at once: the process aborts with
|
|
6
|
+
* `HandleScope: Entering the V8 API without proper locking in place`. Measured
|
|
7
|
+
* on win32/x64 and linux/arm64 alike — one isolate is fine (either the main
|
|
8
|
+
* thread or a worker), two at the same time is fatal, and preloading in both
|
|
9
|
+
* does not help. The upstream issue about workers (#129) was closed in 0.4.0
|
|
10
|
+
* but only covers use from a worker ALONE, which does work.
|
|
11
|
+
*
|
|
12
|
+
* We need it twice: `transport/webrtc-manager.js` runs the video channel to the browser on
|
|
13
|
+
* the main thread, and the torrent client — which is now on its own thread —
|
|
14
|
+
* creates peer connections of its own to announce on `wss://` trackers. Before
|
|
15
|
+
* the thread split both lived in one isolate and nothing was wrong; afterwards
|
|
16
|
+
* any torrent carrying a wss tracker took the whole proxy down.
|
|
17
|
+
*
|
|
18
|
+
* So the native stack stays where it earns its keep — the main thread, carrying
|
|
19
|
+
* video — and the torrent's trackers get a JavaScript implementation, where the
|
|
20
|
+
* traffic is a handful of signalling messages. `services/torrent/worker/worker.js`
|
|
21
|
+
* installs a module resolution hook that points `webrtc-polyfill` here; nothing
|
|
22
|
+
* outside the worker thread is affected, and no dependency is patched (the addon
|
|
23
|
+
* installs with `--ignore-scripts`, so a postinstall patch would never run).
|
|
24
|
+
*
|
|
25
|
+
* **What the shim has to fix.** werift's peer connection matches the interface
|
|
26
|
+
* `simple-peer` expects, but its data channel differs in two ways that matter:
|
|
27
|
+
*
|
|
28
|
+
* - it has no `binaryType`, and hands `onmessage` a `Buffer`. `simple-peer`
|
|
29
|
+
* only recognises `ArrayBuffer`; anything else goes through `text2arr`,
|
|
30
|
+
* which would corrupt every byte of torrent payload.
|
|
31
|
+
* - it has no `onbufferedamountlow`. `simple-peer` builds its backpressure on
|
|
32
|
+
* that event, so without it a send that hits the high-water mark never
|
|
33
|
+
* resumes.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
RTCPeerConnection as WeriftPeerConnection,
|
|
38
|
+
RTCIceCandidate
|
|
39
|
+
} from "werift";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A session description built the way browsers build it — from one object.
|
|
43
|
+
*
|
|
44
|
+
* werift's own class takes two positional arguments, `(sdp, type)`. Callers
|
|
45
|
+
* written against the browser pass `{ type, sdp }`, so with werift's class the
|
|
46
|
+
* type lands in the sdp slot and the description is rejected: field 2026-08-03,
|
|
47
|
+
* wss announces succeeded but every peer connection died with "Connection
|
|
48
|
+
* error: invalid sessionDescription".
|
|
49
|
+
*
|
|
50
|
+
* `RTCIceCandidate` needs no such treatment — werift already takes an object.
|
|
51
|
+
*/
|
|
52
|
+
class ShimSessionDescription {
|
|
53
|
+
/**
|
|
54
|
+
* @param {{ type?: string, sdp?: string } | string} init
|
|
55
|
+
* @param {string} [type] - Tolerated for callers using werift's own order.
|
|
56
|
+
*/
|
|
57
|
+
constructor(init, type) {
|
|
58
|
+
if (typeof init === "string") {
|
|
59
|
+
this.sdp = init;
|
|
60
|
+
this.type = type;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.type = init?.type;
|
|
64
|
+
this.sdp = init?.sdp;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
toJSON() {
|
|
68
|
+
return { type: this.type, sdp: this.sdp };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A werift data channel wearing the browser's interface.
|
|
74
|
+
*
|
|
75
|
+
* Only what `simple-peer` touches is implemented — promising more would be
|
|
76
|
+
* pretending, since anything else has no caller and would never be exercised.
|
|
77
|
+
*/
|
|
78
|
+
class ShimDataChannel {
|
|
79
|
+
#channel;
|
|
80
|
+
/** @type {"blob" | "arraybuffer"} */
|
|
81
|
+
binaryType = "blob";
|
|
82
|
+
/** @type {((event: { data: ArrayBuffer | Buffer }) => void) | null} */
|
|
83
|
+
onmessage = null;
|
|
84
|
+
/** @type {(() => void) | null} */
|
|
85
|
+
onopen = null;
|
|
86
|
+
/** @type {(() => void) | null} */
|
|
87
|
+
onclose = null;
|
|
88
|
+
/** @type {((event: { error?: Error }) => void) | null} */
|
|
89
|
+
onerror = null;
|
|
90
|
+
/** @type {(() => void) | null} */
|
|
91
|
+
onbufferedamountlow = null;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {object} channel - werift's `RTCDataChannel`.
|
|
95
|
+
*/
|
|
96
|
+
constructor(channel) {
|
|
97
|
+
this.#channel = channel;
|
|
98
|
+
|
|
99
|
+
channel.onmessage = (event) => {
|
|
100
|
+
const data = event?.data ?? event;
|
|
101
|
+
this.onmessage?.({ data: this.#toWireFormat(data) });
|
|
102
|
+
};
|
|
103
|
+
channel.onopen = () => this.onopen?.();
|
|
104
|
+
channel.onclose = () => this.onclose?.();
|
|
105
|
+
channel.onerror = (event) => this.onerror?.(event ?? {});
|
|
106
|
+
|
|
107
|
+
// werift reports this as an observable rather than a handler property.
|
|
108
|
+
channel.bufferedAmountLow?.subscribe?.(() => this.onbufferedamountlow?.());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Match what a browser would deliver for the requested `binaryType`.
|
|
113
|
+
*
|
|
114
|
+
* `simple-peer` checks `instanceof ArrayBuffer` and sends everything else
|
|
115
|
+
* through a text decoder, so a `Buffer` handed over unchanged arrives
|
|
116
|
+
* mangled.
|
|
117
|
+
*
|
|
118
|
+
* @param {unknown} data
|
|
119
|
+
* @returns {ArrayBuffer | unknown}
|
|
120
|
+
*/
|
|
121
|
+
#toWireFormat(data) {
|
|
122
|
+
if (this.binaryType !== "arraybuffer" || typeof data === "string") {
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
125
|
+
if (ArrayBuffer.isView(data)) {
|
|
126
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
127
|
+
}
|
|
128
|
+
return data;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get label() {
|
|
132
|
+
return this.#channel.label;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get readyState() {
|
|
136
|
+
return this.#channel.readyState;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get bufferedAmount() {
|
|
140
|
+
return this.#channel.bufferedAmount;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get bufferedAmountLowThreshold() {
|
|
144
|
+
return this.#channel.bufferedAmountLowThreshold;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
set bufferedAmountLowThreshold(value) {
|
|
148
|
+
this.#channel.bufferedAmountLowThreshold = value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string | ArrayBuffer | ArrayBufferView} data
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
155
|
+
send(data) {
|
|
156
|
+
if (typeof data === "string") {
|
|
157
|
+
this.#channel.send(data);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#channel.send(ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
close() {
|
|
164
|
+
this.#channel.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* werift's peer connection, handing out data channels that carry the browser's
|
|
170
|
+
* interface. Everything else is inherited unchanged — the peer connection side
|
|
171
|
+
* already matches.
|
|
172
|
+
*/
|
|
173
|
+
class ShimPeerConnection extends WeriftPeerConnection {
|
|
174
|
+
/**
|
|
175
|
+
* @param {...unknown} args - Passed through to werift.
|
|
176
|
+
*/
|
|
177
|
+
constructor(...args) {
|
|
178
|
+
super(...args);
|
|
179
|
+
|
|
180
|
+
// `ondatachannel` has to be redefined on the instance, not declared as an
|
|
181
|
+
// accessor on this class: werift assigns it as an own field in its own
|
|
182
|
+
// constructor, and an own property shadows a prototype accessor — so a
|
|
183
|
+
// subclass setter is simply never called, and the caller receives werift's
|
|
184
|
+
// bare channel instead of the wrapped one.
|
|
185
|
+
let handler = null;
|
|
186
|
+
const deliver = (event) => {
|
|
187
|
+
handler?.({ ...event, channel: new ShimDataChannel(event?.channel ?? event) });
|
|
188
|
+
};
|
|
189
|
+
Object.defineProperty(this, "ondatachannel", {
|
|
190
|
+
configurable: true,
|
|
191
|
+
// werift reads this property to dispatch, so it must hand back the
|
|
192
|
+
// wrapper rather than what the caller assigned.
|
|
193
|
+
get: () => (handler ? deliver : null),
|
|
194
|
+
set: (value) => {
|
|
195
|
+
handler = value;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string} label
|
|
202
|
+
* @param {object} [options]
|
|
203
|
+
* @returns {ShimDataChannel}
|
|
204
|
+
*/
|
|
205
|
+
createDataChannel(label, options) {
|
|
206
|
+
return new ShimDataChannel(super.createDataChannel(label, options));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
ShimPeerConnection as RTCPeerConnection,
|
|
212
|
+
ShimSessionDescription as RTCSessionDescription,
|
|
213
|
+
RTCIceCandidate
|
|
214
|
+
};
|
|
215
|
+
export default {
|
|
216
|
+
RTCPeerConnection: ShimPeerConnection,
|
|
217
|
+
RTCSessionDescription: ShimSessionDescription,
|
|
218
|
+
RTCIceCandidate
|
|
219
|
+
};
|