@torrent-tv/proxy 2.86.0 → 2.87.1
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/CHANGELOG.md +2142 -1915
- package/CLAUDE.md +322 -197
- package/assets/calibration/NOTICE.md +2 -2
- package/bin/cli.js +684 -616
- package/biome.json +409 -233
- package/docs/browser-proxy-contract.md +94 -0
- package/docs/container-architecture.md +218 -213
- package/docs/download-architecture.md +7 -7
- package/docs/encode-architecture.md +287 -11
- package/docs/encode-run-state.md +2 -2
- package/docs/logs.md +97 -97
- package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
- package/openspec/changes/adaptive-bitrate/design.md +2 -2
- package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
- package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
- package/openspec/changes/chunked-request-bodies/design.md +1 -1
- package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
- package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
- package/openspec/changes/cold-start/design.md +1 -1
- package/openspec/changes/disk-cap/proposal.md +1 -1
- package/openspec/changes/proxy-observability/proposal.md +2 -2
- package/openspec/changes/track-selection/proposal.md +1 -1
- package/openspec/changes/transcode-quality/proposal.md +1 -1
- package/package.json +2 -2
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/routes/api/playback-plan/post.js +2 -2
- package/routes/api/sources/files/get.js +1 -1
- package/routes/api/sources/stats/get.js +1 -1
- package/routes/api/sources/warm/post.js +10 -3
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
- package/routes/api/transcode-sessions/net-report/post.js +129 -94
- package/routes/api/transcode-sessions/post.js +160 -139
- package/routes/api/transcode-sessions/progress/get.js +3 -3
- package/routes/api/transcode-sessions/release/post.js +3 -3
- package/routes/api/transcode-sessions/seek/post.js +12 -3
- package/routes/stream/get.js +2 -2
- package/routes/transcode/audio-file/get.js +15 -6
- package/routes/transcode/audio-warm/get.js +9 -6
- package/routes/transcode/session-file/get.js +52 -268
- package/routes/transcode/variant-file/get.js +46 -7
- package/routes/transcode/variant-warm/get.js +19 -6
- package/scripts/render-run-graph.js +1 -1
- package/server.js +720 -465
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -0
- package/services/encode/EncodeAdmission.js +332 -0
- package/services/{orchestrators → encode}/EncodeOrchestrator.js +307 -29
- package/services/encode/EncodeRun.js +16 -9
- package/services/encode/EncodeRuns.js +1229 -0
- package/services/encode/Encoder.js +18 -2
- package/services/encode/EncoderSelection.js +38 -0
- package/services/encode/LocalObservations.js +359 -0
- package/services/encode/NvencEncoder.js +5 -0
- package/services/encode/OutputOpening.js +755 -0
- package/services/encode/OutputTimes.js +579 -0
- package/services/encode/Renditions.js +2162 -0
- package/services/encode/RunProgress.js +84 -0
- package/services/encode/SoftwareEncoder.js +12 -7
- package/services/encode/args.js +402 -186
- package/services/encode/calibration.js +361 -0
- package/services/encode/contention.js +128 -128
- package/services/encode/encode-run-state.js +16 -20
- package/services/encode/fingerprint.js +322 -0
- package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
- package/services/{learned-median.js → encode/learned-median.js} +11 -0
- package/services/encode/output/EncodedOutput.js +57 -0
- package/services/encode/output/Output.js +131 -0
- package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
- package/services/encode/output/OutputSpec.js +390 -0
- package/services/{output → encode/output}/Timeline.js +12 -37
- package/services/encode/output/index.js +20 -0
- package/services/encode/output/ladder.js +32 -0
- package/services/{output → encode/output}/playlists.js +3 -0
- package/services/encode/output-key-format.js +21 -0
- package/services/encode/quality/EncodeCost.js +1083 -0
- package/services/encode/quality/HostLoad.js +525 -0
- package/services/encode/quality/HostTimings.js +249 -0
- package/services/encode/quality/OutputQualityState.js +28 -0
- package/services/encode/quality/QualityController.js +935 -0
- package/services/encode/quality/QualityOffer.js +416 -0
- package/services/encode/quality/drain-threat.js +47 -0
- package/services/encode/quality/link-budget.js +254 -0
- package/services/encode/quality/output-format.js +266 -0
- package/services/encode/quality/serving-output.js +216 -0
- package/services/encode/quality/visible-rung.js +57 -0
- package/services/encode/run-command.js +23 -20
- package/services/encode/run-inputs.js +42 -0
- package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
- package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
- package/services/encode/segment-formats/init-compat.js +324 -0
- package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
- package/services/encode/throughput.js +97 -0
- package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
- package/services/media/KeyframeTables.js +316 -0
- package/services/{source → media}/SourceFile.js +7 -55
- package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
- package/services/media/SubtitleOrchestrator.js +167 -0
- package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
- package/services/{container → media/container}/AviContainer.js +266 -266
- package/services/{container → media/container}/Container.js +596 -601
- package/services/media/container/ContainerFactory.js +56 -0
- package/services/media/container/KeyframeTable.js +272 -0
- package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
- package/services/{container → media/container}/Mp4Container.js +1244 -1244
- package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
- package/services/media/keyframe-probe.js +157 -0
- package/services/media/media-info-probe.js +93 -0
- package/services/{playback-planner.js → media/playback-planner.js} +82 -39
- package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
- package/services/{tracks → media/tracks}/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -0
- package/services/server/SegmentServing.js +1146 -0
- package/services/server/ViewerRequests.js +522 -0
- package/services/server/controllers/PlaybackController.js +36 -0
- package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
- package/services/server/http-responses.js +60 -0
- package/services/server/transcode-session-files.js +325 -0
- package/services/server/wire-outputs.js +983 -0
- package/services/storage/Diagnostics.js +202 -0
- package/services/storage/MachineBudget.js +235 -0
- package/services/{piece-store → storage}/allowance.js +107 -107
- package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
- package/services/{files → storage/files}/CompletedFiles.js +116 -1
- package/services/{disk → storage}/free.js +33 -0
- package/services/storage/machine-memory.js +56 -0
- package/services/{memory-report.js → storage/memory-report.js} +575 -596
- package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
- package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
- package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
- package/services/storage/wire.js +175 -0
- package/services/{download → torrent/download}/SwarmSelection.js +2 -2
- package/services/{download → torrent/download}/registry.js +2 -2
- package/services/torrent/files.js +1 -1
- package/services/torrent/piece-store-of.js +29 -0
- package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
- package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
- package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
- package/services/{torrent-worker → torrent/worker}/client.js +77 -58
- package/services/torrent/worker/held-bytes.js +155 -0
- package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
- package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
- package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
- package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
- package/services/torrent/worker/resume-warm.js +107 -0
- package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
- package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
- package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
- package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
- package/services/{health-collector.js → transport/health-collector.js} +6 -35
- package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
- package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
- package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
- package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
- package/services/viewer/Assignments.js +340 -0
- package/services/{priority → viewer}/PriorityMap.js +390 -390
- package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
- package/services/viewer/Viewer.js +657 -483
- package/services/viewer/Viewers.js +368 -257
- package/services/viewer/active-output.js +37 -54
- package/services/viewer/audio-start.js +4 -3
- package/services/viewer/buffer-trend.js +81 -0
- package/services/viewer/choices.js +647 -0
- package/services/viewer/positions.js +126 -120
- package/services/viewer/report-intake.js +46 -45
- package/test/asset-paths.test.js +28 -0
- package/test/audio-inventory.test.js +3 -3
- package/test/audio-track-bitrate.test.js +40 -0
- package/test/auto-quality-step.test.js +707 -515
- package/test/available-share.test.js +1 -1
- package/test/background-fill.test.js +1 -1
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +1 -1
- package/test/concurrent-cost.test.js +13 -3
- package/test/container-media-info.test.js +3 -3
- package/test/core-dumps.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +46 -9
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
- package/test/cut-times-timeline.test.js +2 -2
- package/test/cuts-follow-published-grid.test.js +9 -8
- package/test/data-channel-frame.test.js +1 -1
- package/test/dead-torrent-handle.test.js +1 -1
- package/test/declared-rates.test.js +2 -2
- package/test/declared-tracks.test.js +2 -2
- package/test/decode-cost-fit.test.js +1 -1
- package/test/decode-cost.test.js +34 -25
- package/test/delivery-probe.test.js +1 -1
- package/test/demand-register.test.js +4 -4
- package/test/dht-bootstrap.test.js +1 -1
- package/test/drain-threat.test.js +65 -0
- package/test/duplicate-add.test.js +1 -1
- package/test/encode-admission.test.js +146 -0
- package/test/encode-cost.test.js +199 -10
- package/test/encode-orchestrator.test.js +39 -1
- package/test/encode-plan-viewers.test.js +2 -2
- package/test/encode-run.test.js +2 -2
- package/test/encode-slope.test.js +1 -1
- package/test/encoded-output.test.js +39 -0
- package/test/encoder-readings.test.js +1 -1
- package/test/every-claimant-is-told-its-share.test.js +152 -0
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
- package/test/failed-start-breaker.test.js +81 -30
- package/test/fastest-wires.test.js +1 -1
- package/test/ffmpeg-stream-counts.test.js +1 -1
- package/test/file-edges.test.js +3 -3
- package/test/fmp4-self-contained.test.js +1 -1
- package/test/fmp4-timestamps.test.js +1 -1
- package/test/fmp4-track-completeness.test.js +1 -1
- package/test/format-keyed-outputs.test.js +78 -0
- package/test/health-metrics.test.js +3 -2
- package/test/held-request-width.test.js +22 -23
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -0
- package/test/helpers/manager.js +57 -0
- package/test/helpers/output-spec.js +63 -0
- package/test/host-load.test.js +1 -1
- package/test/index-accuracy-is-about-the-file.test.js +128 -0
- package/test/init-compat.test.js +219 -0
- package/test/input-lost-quiets-the-plan.test.js +1 -1
- package/test/input-unavailable.test.js +1 -1
- package/test/keeping-period.test.js +4 -4
- package/test/keyframe-index-accuracy.test.js +97 -45
- package/test/keyframe-table-budget.test.js +28 -33
- package/test/keyframes-belong-to-the-file.test.js +174 -57
- package/test/learned-median.test.js +1 -1
- package/test/letting-go-asks-who-is-watching.test.js +191 -0
- package/test/limit-row-by-area.test.js +60 -0
- package/test/link-budget.test.js +111 -0
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -0
- package/test/look-ahead-coverage.test.js +1 -1
- package/test/manager-surface.test.js +294 -0
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +1 -1
- package/test/memory-budget.test.js +2 -2
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +1 -1
- package/test/one-authority.test.js +45 -41
- package/test/one-budget.test.js +149 -0
- package/test/one-output-one-session.test.js +204 -0
- package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
- package/test/one-serving-output-per-height.test.js +458 -0
- package/test/orchestrator-wired.test.js +46 -45
- package/test/output-catalog.test.js +39 -0
- package/test/output-shape.test.js +4 -51
- package/test/output-spec.test.js +251 -157
- package/test/output-speed.test.js +17 -15
- package/test/outputs-being-prepared.test.js +40 -0
- package/test/packet-witness-ring.test.js +2 -2
- package/test/packet-witness.test.js +1 -1
- package/test/piece-disk-store.test.js +2 -2
- package/test/piece-from-whole-file.test.js +1 -1
- package/test/piece-lru.test.js +1 -1
- package/test/piece-reader.test.js +2 -2
- package/test/piece-store-eviction.test.js +1 -1
- package/test/piece-store-never-refuses.test.js +1 -1
- package/test/piece-store-reservations.test.js +1 -1
- package/test/piece-store-slow-disk.test.js +1 -1
- package/test/piece-tail.test.js +1 -1
- package/test/plan-host-timings.test.js +1 -1
- package/test/plan-is-steady.test.js +225 -225
- package/test/priority-map-download.test.js +4 -4
- package/test/priority-map-emptied.test.js +204 -205
- package/test/priority-map-per-output.test.js +215 -209
- package/test/priority-map.test.js +1 -1
- package/test/probe-wedge-certainty.test.js +1 -1
- package/test/produced-copy-choice.test.js +15 -60
- package/test/quality-offer.test.js +162 -0
- package/test/quality-variants.test.js +1229 -1167
- package/test/rate-control-in-the-output.test.js +180 -0
- package/test/read-survives-withdrawal.test.js +2 -2
- package/test/read-window.test.js +10 -3
- package/test/resume-warm.test.js +1 -1
- package/test/run-inputs.test.js +80 -0
- package/test/run-position-follows-published-grid.test.js +13 -11
- package/test/run-progress.test.js +65 -0
- package/test/same-height-switch.test.js +664 -0
- package/test/seek-frees-the-viewers-requests.test.js +217 -0
- package/test/seek-landing.test.js +13 -13
- package/test/seek-target-not-superseded.test.js +28 -29
- package/test/segment-closed-on-both-branches.test.js +96 -0
- package/test/segment-cut-times.test.js +1 -1
- package/test/segment-hold-supersede.test.js +28 -35
- package/test/segment-judged-by-its-own-header.test.js +221 -0
- package/test/segment-serve-wiring.test.js +130 -39
- package/test/segment-store-eviction.test.js +22 -7
- package/test/segment-store.test.js +98 -14
- package/test/segment-true-start.test.js +1 -1
- package/test/segments-are-shared.test.js +17 -16
- package/test/send-chunk-bytes.test.js +1 -1
- package/test/serving-existing-output.test.js +102 -0
- package/test/serving-output.test.js +102 -0
- package/test/shared-piece-store.test.js +1 -1
- package/test/source-file.test.js +1 -1
- package/test/stale-request-after-seek.test.js +17 -21
- package/test/startup-readings.test.js +10 -5
- package/test/subtitle-cue-framing.test.js +5 -5
- package/test/subtitle-cue-source.test.js +36 -38
- package/test/subtitle-cue-walk.test.js +11 -10
- package/test/subtitle-defaults.test.js +1 -1
- package/test/subtitle-language.test.js +4 -4
- package/test/subtitle-track-numbering.test.js +5 -4
- package/test/supply-margin.test.js +1 -1
- package/test/swarm-follows-readers.test.js +3 -3
- package/test/swarm-reach.test.js +1 -1
- package/test/swarm-selection.test.js +3 -3
- package/test/tail-duplication.test.js +1 -1
- package/test/the-channel-has-both-ends.test.js +91 -0
- package/test/the-torrent-thread-serves-bytes.test.js +107 -0
- package/test/thread-pool.test.js +3 -3
- package/test/timeline.test.js +1 -12
- package/test/torrent-cost.test.js +1 -1
- package/test/tracks-begin-together.test.js +38 -24
- package/test/tunnel-renewal.test.js +1 -1
- package/test/two-viewers-one-picture.test.js +120 -87
- package/test/upload-hurry.test.js +3 -3
- package/test/usrsctp-state.test.js +1 -1
- package/test/video-facts.test.js +1 -1
- package/test/viewer-assignments.test.js +143 -0
- package/test/viewer-outputs.test.js +60 -52
- package/test/viewer-presence.test.js +22 -25
- package/test/viewer-requests-admission.test.js +89 -0
- package/test/viewer-states.test.js +189 -202
- package/test/viewer-subtitle-subscription.test.js +1 -1
- package/test/viewer.test.js +139 -136
- package/test/viewing-generations.test.js +390 -0
- package/test/visible-rung.test.js +37 -0
- package/test/wait-ledger.test.js +1 -1
- package/test/webrtc-shim.test.js +3 -3
- package/test/wedge-certainty.test.js +2 -2
- package/test/withdraw-piece-claim.test.js +2 -2
- package/test/worker-channel.test.js +1 -31
- package/utils/client-log-file.js +151 -151
- package/scripts/read-container-index.mjs +0 -108
- package/services/container/ContainerFactory.js +0 -122
- package/services/controllers/PlaybackController.js +0 -33
- package/services/disk/DiskSpace.js +0 -150
- package/services/disk/wire.js +0 -61
- package/services/hls-session-manager.js +0 -9608
- package/services/orchestrators/SubtitleOrchestrator.js +0 -160
- package/services/output/Output.js +0 -94
- package/services/output/OutputSpec.js +0 -202
- package/services/output/index.js +0 -1
- package/services/output/ladder.js +0 -26
- package/services/produced-index.js +0 -222
- package/services/quality/EncodeCost.js +0 -555
- package/services/torrent-worker/container-tracks.js +0 -414
- package/services/viewer/link-readings.js +0 -56
- package/test/produced-index.test.js +0 -142
- /package/{services → bin}/thread-pool.js +0 -0
- /package/services/{available-share.js → encode/available-share.js} +0 -0
- /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
- /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
- /package/services/{host-load.js → encode/host-load.js} +0 -0
- /package/services/{output → encode/output}/cut-grid.js +0 -0
- /package/services/{output → encode/output}/rates.js +0 -0
- /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
- /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
- /package/services/{container → media/container}/ebml-reader.js +0 -0
- /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
- /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
- /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
- /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
- /package/services/{tracks → media/tracks}/index.js +0 -0
- /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
- /package/services/{disk → storage}/keep.js +0 -0
- /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
- /package/services/{disk → storage}/returns.js +0 -0
- /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
- /package/services/{demand → torrent/demand}/Urgency.js +0 -0
- /package/services/{demand → torrent/demand}/Window.js +0 -0
- /package/services/{demand → torrent/demand}/index.js +0 -0
- /package/services/{demand → torrent/demand}/pieces.js +0 -0
- /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
- /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
- /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
- /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
- /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
- /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
- /package/services/{registry-api.js → transport/registry-api.js} +0 -0
- /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
- /package/services/{priority → viewer}/WaitLedger.js +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
|
|
12
|
-
import { availableShareFrom, correctForAvailability } from "../services/available-share.js";
|
|
12
|
+
import { availableShareFrom, correctForAvailability } from "../services/encode/available-share.js";
|
|
13
13
|
|
|
14
14
|
test("only the work nobody has been charged for is subtracted", () => {
|
|
15
15
|
// The field reading: the box is saturated, our encoder has more than half of
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
|
-
import { fillFileInBackground, fillIsRunning } from "../services/torrent
|
|
14
|
+
import { fillFileInBackground, fillIsRunning } from "../services/torrent/worker/background-fill.js";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* A file whose reads resolve when the test says so, recording what was asked
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The browser's log, written beside the proxy's.
|
|
3
|
-
*
|
|
4
|
-
* What these hold is the naming, because the name is the whole point: the two
|
|
5
|
-
* halves of a session have to join without guessing, and a torrent name is
|
|
6
|
-
* arbitrary bytes chosen by a stranger that is about to become a path.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
|
|
15
|
-
import { createClientLogFiles } from "../utils/client-log-file.js";
|
|
16
|
-
|
|
17
|
-
/** A directory with a proxy log in it, which is what the sink is given. */
|
|
18
|
-
function aDirectory() {
|
|
19
|
-
const dir = mkdtempSync(join(tmpdir(), "ttv-client-log-"));
|
|
20
|
-
return { dir, proxyLog: join(dir, "proxy.log") };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const aSession = (over = {}) => ({
|
|
24
|
-
sessionId: "abcd1234",
|
|
25
|
-
startedAt: "2026-09-13T16:05:24.123Z",
|
|
26
|
-
torrentName: "Reacher.S04E07.1080p.rus.LostFilm.TV.mkv",
|
|
27
|
-
infoHash: "94dda59fdf252447d6ca5a8775dab2d11a832da5",
|
|
28
|
-
...over
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("the file lands beside the proxy log, named by when it started and what is watched", async () => {
|
|
32
|
-
const { dir, proxyLog } = aDirectory();
|
|
33
|
-
const logs = createClientLogFiles(proxyLog);
|
|
34
|
-
logs.write(aSession(), ["first line"]);
|
|
35
|
-
await logs.close();
|
|
36
|
-
|
|
37
|
-
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
38
|
-
assert.ok(name, "a client log file was written");
|
|
39
|
-
// The stamp is UTC and sorts; the session id joins it to the proxy's lines;
|
|
40
|
-
// the film says which one it is without opening the file.
|
|
41
|
-
assert.match(name, /^client-20260913-160524-abcd1234-Reacher\.S04E07\.1080p\.rus\.LostFilm\.TV\.mkv-94dda59f\.log$/);
|
|
42
|
-
assert.equal(readFileSync(join(dir, name), "utf8"), "first line\n");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("lines from one session go on appending to the one file", async () => {
|
|
46
|
-
const { dir, proxyLog } = aDirectory();
|
|
47
|
-
const logs = createClientLogFiles(proxyLog);
|
|
48
|
-
logs.write(aSession(), ["one", "two"]);
|
|
49
|
-
logs.write(aSession(), ["three"]);
|
|
50
|
-
await logs.close();
|
|
51
|
-
|
|
52
|
-
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
53
|
-
assert.equal(names.length, 1);
|
|
54
|
-
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "one\ntwo\nthree\n");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("a session that has not chosen a film yet still gets its own file", async () => {
|
|
58
|
-
// The page opening, a proxy being chosen, a connection failing — all before
|
|
59
|
-
// any torrent exists, and all worth keeping.
|
|
60
|
-
const { dir, proxyLog } = aDirectory();
|
|
61
|
-
const logs = createClientLogFiles(proxyLog);
|
|
62
|
-
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before any film"]);
|
|
63
|
-
await logs.close();
|
|
64
|
-
|
|
65
|
-
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
66
|
-
assert.match(name, /-no-torrent-yet\.log$/);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("the file keeps its name when the film arrives mid-session", async () => {
|
|
70
|
-
// Keyed by the session, not by the name: the earlier lines belong to the same
|
|
71
|
-
// viewing and must not start a second file.
|
|
72
|
-
const { dir, proxyLog } = aDirectory();
|
|
73
|
-
const logs = createClientLogFiles(proxyLog);
|
|
74
|
-
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before"]);
|
|
75
|
-
logs.write(aSession(), ["after"]);
|
|
76
|
-
await logs.close();
|
|
77
|
-
|
|
78
|
-
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
79
|
-
assert.equal(names.length, 1);
|
|
80
|
-
assert.match(names[0], /-no-torrent-yet\.log$/);
|
|
81
|
-
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "before\nafter\n");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("a hostile torrent name cannot escape the directory", async () => {
|
|
85
|
-
const { dir, proxyLog } = aDirectory();
|
|
86
|
-
const logs = createClientLogFiles(proxyLog);
|
|
87
|
-
logs.write(aSession({ torrentName: "../../etc/passwd" }), ["x"]);
|
|
88
|
-
await logs.close();
|
|
89
|
-
|
|
90
|
-
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
91
|
-
assert.equal(names.length, 1);
|
|
92
|
-
assert.ok(!names[0].includes(".."), `no traversal in ${names[0]}`);
|
|
93
|
-
assert.ok(!names[0].includes("/"), `no separator in ${names[0]}`);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("two sessions of one film are two files", async () => {
|
|
97
|
-
const { dir, proxyLog } = aDirectory();
|
|
98
|
-
const logs = createClientLogFiles(proxyLog);
|
|
99
|
-
logs.write(aSession({ sessionId: "aaaa1111" }), ["one viewer"]);
|
|
100
|
-
logs.write(aSession({ sessionId: "bbbb2222", startedAt: "2026-09-13T16:07:11.000Z" }), ["another"]);
|
|
101
|
-
await logs.close();
|
|
102
|
-
|
|
103
|
-
assert.equal(readdirSync(dir).filter((f) => f.startsWith("client-")).length, 2);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("no proxy log path means nothing is written and nothing throws", async () => {
|
|
107
|
-
const logs = createClientLogFiles("");
|
|
108
|
-
logs.write(aSession(), ["dropped"]);
|
|
109
|
-
await logs.close();
|
|
110
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The browser's log, written beside the proxy's.
|
|
3
|
+
*
|
|
4
|
+
* What these hold is the naming, because the name is the whole point: the two
|
|
5
|
+
* halves of a session have to join without guessing, and a torrent name is
|
|
6
|
+
* arbitrary bytes chosen by a stranger that is about to become a path.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { createClientLogFiles } from "../utils/client-log-file.js";
|
|
16
|
+
|
|
17
|
+
/** A directory with a proxy log in it, which is what the sink is given. */
|
|
18
|
+
function aDirectory() {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), "ttv-client-log-"));
|
|
20
|
+
return { dir, proxyLog: join(dir, "proxy.log") };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const aSession = (over = {}) => ({
|
|
24
|
+
sessionId: "abcd1234",
|
|
25
|
+
startedAt: "2026-09-13T16:05:24.123Z",
|
|
26
|
+
torrentName: "Reacher.S04E07.1080p.rus.LostFilm.TV.mkv",
|
|
27
|
+
infoHash: "94dda59fdf252447d6ca5a8775dab2d11a832da5",
|
|
28
|
+
...over
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("the file lands beside the proxy log, named by when it started and what is watched", async () => {
|
|
32
|
+
const { dir, proxyLog } = aDirectory();
|
|
33
|
+
const logs = createClientLogFiles(proxyLog);
|
|
34
|
+
logs.write(aSession(), ["first line"]);
|
|
35
|
+
await logs.close();
|
|
36
|
+
|
|
37
|
+
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
38
|
+
assert.ok(name, "a client log file was written");
|
|
39
|
+
// The stamp is UTC and sorts; the session id joins it to the proxy's lines;
|
|
40
|
+
// the film says which one it is without opening the file.
|
|
41
|
+
assert.match(name, /^client-20260913-160524-abcd1234-Reacher\.S04E07\.1080p\.rus\.LostFilm\.TV\.mkv-94dda59f\.log$/);
|
|
42
|
+
assert.equal(readFileSync(join(dir, name), "utf8"), "first line\n");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("lines from one session go on appending to the one file", async () => {
|
|
46
|
+
const { dir, proxyLog } = aDirectory();
|
|
47
|
+
const logs = createClientLogFiles(proxyLog);
|
|
48
|
+
logs.write(aSession(), ["one", "two"]);
|
|
49
|
+
logs.write(aSession(), ["three"]);
|
|
50
|
+
await logs.close();
|
|
51
|
+
|
|
52
|
+
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
53
|
+
assert.equal(names.length, 1);
|
|
54
|
+
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "one\ntwo\nthree\n");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a session that has not chosen a film yet still gets its own file", async () => {
|
|
58
|
+
// The page opening, a proxy being chosen, a connection failing — all before
|
|
59
|
+
// any torrent exists, and all worth keeping.
|
|
60
|
+
const { dir, proxyLog } = aDirectory();
|
|
61
|
+
const logs = createClientLogFiles(proxyLog);
|
|
62
|
+
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before any film"]);
|
|
63
|
+
await logs.close();
|
|
64
|
+
|
|
65
|
+
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
66
|
+
assert.match(name, /-no-torrent-yet\.log$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("the file keeps its name when the film arrives mid-session", async () => {
|
|
70
|
+
// Keyed by the session, not by the name: the earlier lines belong to the same
|
|
71
|
+
// viewing and must not start a second file.
|
|
72
|
+
const { dir, proxyLog } = aDirectory();
|
|
73
|
+
const logs = createClientLogFiles(proxyLog);
|
|
74
|
+
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before"]);
|
|
75
|
+
logs.write(aSession(), ["after"]);
|
|
76
|
+
await logs.close();
|
|
77
|
+
|
|
78
|
+
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
79
|
+
assert.equal(names.length, 1);
|
|
80
|
+
assert.match(names[0], /-no-torrent-yet\.log$/);
|
|
81
|
+
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "before\nafter\n");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a hostile torrent name cannot escape the directory", async () => {
|
|
85
|
+
const { dir, proxyLog } = aDirectory();
|
|
86
|
+
const logs = createClientLogFiles(proxyLog);
|
|
87
|
+
logs.write(aSession({ torrentName: "../../etc/passwd" }), ["x"]);
|
|
88
|
+
await logs.close();
|
|
89
|
+
|
|
90
|
+
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
91
|
+
assert.equal(names.length, 1);
|
|
92
|
+
assert.ok(!names[0].includes(".."), `no traversal in ${names[0]}`);
|
|
93
|
+
assert.ok(!names[0].includes("/"), `no separator in ${names[0]}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("two sessions of one film are two files", async () => {
|
|
97
|
+
const { dir, proxyLog } = aDirectory();
|
|
98
|
+
const logs = createClientLogFiles(proxyLog);
|
|
99
|
+
logs.write(aSession({ sessionId: "aaaa1111" }), ["one viewer"]);
|
|
100
|
+
logs.write(aSession({ sessionId: "bbbb2222", startedAt: "2026-09-13T16:07:11.000Z" }), ["another"]);
|
|
101
|
+
await logs.close();
|
|
102
|
+
|
|
103
|
+
assert.equal(readdirSync(dir).filter((f) => f.startsWith("client-")).length, 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("no proxy log path means nothing is written and nothing throws", async () => {
|
|
107
|
+
const logs = createClientLogFiles("");
|
|
108
|
+
logs.write(aSession(), ["dropped"]);
|
|
109
|
+
await logs.close();
|
|
110
|
+
});
|
|
@@ -13,7 +13,7 @@ import fs from "node:fs/promises";
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { Readable } from "node:stream";
|
|
16
|
-
import { CompletedFiles } from "../services/files/CompletedFiles.js";
|
|
16
|
+
import { CompletedFiles } from "../services/storage/files/CompletedFiles.js";
|
|
17
17
|
|
|
18
18
|
const INFO_HASH = "abcdef0123456789abcdef0123456789abcdef01";
|
|
19
19
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
|
|
14
|
-
import { canSustainOutput } from "../services/hwaccel.js";
|
|
14
|
+
import { canSustainOutput } from "../services/encode/hwaccel.js";
|
|
15
15
|
|
|
16
16
|
// A host that encodes 640x360 at 24 fps about eleven times over, and decodes
|
|
17
17
|
// 1080p24 at 2.6x — the addon host's own figures.
|
|
@@ -59,9 +59,9 @@ test("nothing else running leaves the answer exactly as it was", () => {
|
|
|
59
59
|
assert.equal(withZero.speed, without.speed);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
test("a
|
|
62
|
+
test("a wiring made without the startup calibration judges nothing", () => {
|
|
63
63
|
const answer = canSustainOutput({
|
|
64
|
-
benchmark:
|
|
64
|
+
benchmark: null,
|
|
65
65
|
outputPixelsPerSec: RUNG_240P,
|
|
66
66
|
concurrentCostSec: 0.125
|
|
67
67
|
});
|
|
@@ -69,6 +69,16 @@ test("a host with nothing measured still refuses nothing", () => {
|
|
|
69
69
|
assert.equal(answer.speed, null);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
test("a host calibrated with no mode qualified refuses a re-encode (roadmap item 97, step 14)", () => {
|
|
73
|
+
const answer = canSustainOutput({
|
|
74
|
+
benchmark: [],
|
|
75
|
+
outputPixelsPerSec: RUNG_240P,
|
|
76
|
+
concurrentCostSec: 0.125
|
|
77
|
+
});
|
|
78
|
+
assert.equal(answer.sustainable, false, "unknown capacity is not capacity");
|
|
79
|
+
assert.equal(answer.speed, null);
|
|
80
|
+
});
|
|
81
|
+
|
|
72
82
|
test("a cost heavy enough to fill the machine puts a rung below realtime", () => {
|
|
73
83
|
const answer = canSustainOutput({
|
|
74
84
|
benchmark: BENCHMARK,
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
import assert from "node:assert/strict";
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
|
|
20
|
-
import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
|
|
21
|
-
import { Mp4Container } from "../services/container/Mp4Container.js";
|
|
22
|
-
import { AviContainer } from "../services/container/AviContainer.js";
|
|
20
|
+
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
21
|
+
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
22
|
+
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* An EBML element: its id bytes, a four-byte size, then the payload.
|
package/test/core-dumps.test.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
|
|
12
|
-
import { dumpsToRemove, isCoreDump } from "../services/core-dumps.js";
|
|
12
|
+
import { dumpsToRemove, isCoreDump } from "../services/storage/core-dumps.js";
|
|
13
13
|
|
|
14
14
|
const dumps = [
|
|
15
15
|
{ name: "core.WorkerThread.81.1787176646", writtenAt: 1787176646000 },
|
|
@@ -29,10 +29,10 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
29
29
|
import os from "node:os";
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
32
|
-
import { SegmentStore } from "../services/
|
|
32
|
+
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
33
33
|
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
34
|
-
import { EncodeOrchestrator } from "../services/
|
|
35
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
34
|
+
import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
|
|
35
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
36
36
|
|
|
37
37
|
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
38
38
|
const SEGMENTS = 482;
|
|
@@ -70,7 +70,12 @@ function orchestratorWithAStore() {
|
|
|
70
70
|
firstByteWaitSec: 0.12,
|
|
71
71
|
refetchSecPerFilmSecond: () => 0.25,
|
|
72
72
|
startingSpeedFor: () => 2,
|
|
73
|
-
|
|
73
|
+
segmentCoverage: {
|
|
74
|
+
provenNumbers: (address) => store.provenNumbers(address),
|
|
75
|
+
announce: (address, index) => store.announce(address, index),
|
|
76
|
+
filesHeld: (address) => store.filesHeld(address),
|
|
77
|
+
clearUpAfter: (address, startedAt) => store.clearUpAfter(address, startedAt)
|
|
78
|
+
},
|
|
74
79
|
now: () => 1000,
|
|
75
80
|
logger,
|
|
76
81
|
makeRun: ({ address, from, to }) => new EncodeRun({
|
|
@@ -114,7 +119,15 @@ function theWholeFilmIsOnDisk(store, dir) {
|
|
|
114
119
|
|
|
115
120
|
test("a film that is entirely made needs no encoder", (t) => {
|
|
116
121
|
const { made, store, dir, root } = orchestratorWithAStore();
|
|
117
|
-
t.after(() =>
|
|
122
|
+
t.after(() => {
|
|
123
|
+
// THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
|
|
124
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
125
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
126
|
+
// open watch on it cannot be removed on Windows: `rmSync` retries until it
|
|
127
|
+
// gives up, and the check reads as a hang.
|
|
128
|
+
store.dropAll("the check is over");
|
|
129
|
+
rmSync(root, { recursive: true, force: true });
|
|
130
|
+
});
|
|
118
131
|
|
|
119
132
|
theWholeFilmIsOnDisk(store, dir);
|
|
120
133
|
aViewerAtTheStart(made);
|
|
@@ -128,7 +141,15 @@ test("a film that is entirely made needs no encoder", (t) => {
|
|
|
128
141
|
|
|
129
142
|
test("segments the store has lost stop being ready, and an encoder is placed again", (t) => {
|
|
130
143
|
const { made, store, dir, root } = orchestratorWithAStore();
|
|
131
|
-
t.after(() =>
|
|
144
|
+
t.after(() => {
|
|
145
|
+
// THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
|
|
146
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
147
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
148
|
+
// open watch on it cannot be removed on Windows: `rmSync` retries until it
|
|
149
|
+
// gives up, and the check reads as a hang.
|
|
150
|
+
store.dropAll("the check is over");
|
|
151
|
+
rmSync(root, { recursive: true, force: true });
|
|
152
|
+
});
|
|
132
153
|
|
|
133
154
|
theWholeFilmIsOnDisk(store, dir);
|
|
134
155
|
aViewerAtTheStart(made);
|
|
@@ -151,7 +172,15 @@ test("segments the store has lost stop being ready, and an encoder is placed aga
|
|
|
151
172
|
|
|
152
173
|
test("a piece discarded with the run that had it open is not ready either", (t) => {
|
|
153
174
|
const { made, store, dir, root } = orchestratorWithAStore();
|
|
154
|
-
t.after(() =>
|
|
175
|
+
t.after(() => {
|
|
176
|
+
// THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
|
|
177
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
178
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
179
|
+
// open watch on it cannot be removed on Windows: `rmSync` retries until it
|
|
180
|
+
// gives up, and the check reads as a hang.
|
|
181
|
+
store.dropAll("the check is over");
|
|
182
|
+
rmSync(root, { recursive: true, force: true });
|
|
183
|
+
});
|
|
155
184
|
|
|
156
185
|
theWholeFilmIsOnDisk(store, dir);
|
|
157
186
|
aViewerAtTheStart(made);
|
|
@@ -169,8 +198,16 @@ test("a piece discarded with the run that had it open is not ready either", (t)
|
|
|
169
198
|
});
|
|
170
199
|
|
|
171
200
|
test("nobody making what somebody waits for is said out loud", (t) => {
|
|
172
|
-
const { made, root, lines } = orchestratorWithAStore();
|
|
173
|
-
t.after(() =>
|
|
201
|
+
const { made, store, root, lines } = orchestratorWithAStore();
|
|
202
|
+
t.after(() => {
|
|
203
|
+
// THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
|
|
204
|
+
// holds a watch on each of them — that is how a piece closed by a muxer
|
|
205
|
+
// with no channel of its own becomes an event — and a directory with an
|
|
206
|
+
// open watch on it cannot be removed on Windows: `rmSync` retries until it
|
|
207
|
+
// gives up, and the check reads as a hang.
|
|
208
|
+
store.dropAll("the check is over");
|
|
209
|
+
rmSync(root, { recursive: true, force: true });
|
|
210
|
+
});
|
|
174
211
|
|
|
175
212
|
// A HOST THAT CANNOT BUILD AN ENCODER. Whatever the reason — and there are
|
|
176
213
|
// several — the state it leaves is the one that cost two sessions on
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What the subtitle walk found reaches the viewer whole.
|
|
3
|
+
*
|
|
4
|
+
* It did not. The walk has produced `detectedLanguage` on every push since
|
|
5
|
+
* 2.69.0 — the language read from the CUES rather than from a container that
|
|
6
|
+
* often states none — and the whole of the "label moves when the answer
|
|
7
|
+
* arrives" behaviour rests on it. It never arrived: the fact was copied by hand
|
|
8
|
+
* twice on its way out, and each copy listed the fields it knew about. The
|
|
9
|
+
* worker posted `trackIndex, cues, language, cursor`; the main thread's client
|
|
10
|
+
* rebuilt the same four from the message. `detectedLanguage` was in neither
|
|
11
|
+
* list, so the browser was told nothing and the label it shows for an untagged
|
|
12
|
+
* track has never moved.
|
|
13
|
+
*
|
|
14
|
+
* It is not a typo to fix in two places — it is the shape. A fact copied field
|
|
15
|
+
* by field loses whatever the copier did not know about, and says nothing when
|
|
16
|
+
* it does. The walk's own result travels now, so a field added to it arrives
|
|
17
|
+
* without anybody being told to forward it.
|
|
18
|
+
*
|
|
19
|
+
* (Both copies are gone anyway: the walk runs on the thread that pushes.)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import test from "node:test";
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} relative
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function source(relative) {
|
|
35
|
+
return readFileSync(path.join(ROOT, relative), "utf8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test("the walk states the language it read from the cues", () => {
|
|
39
|
+
// The producer. Without this the rest of the check is about nothing.
|
|
40
|
+
assert.match(source("services/media/SubtitleCues.js"), /detectedLanguage: detectLanguage\(/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the consumer asks for it", () => {
|
|
44
|
+
// The other end has always destructured it, which is what made the loss
|
|
45
|
+
// silent: a field nobody forwards is `undefined` and reads as "not detected".
|
|
46
|
+
assert.match(
|
|
47
|
+
source("services/transport/data-channel-handler.js"),
|
|
48
|
+
/function publishSubtitleCues\(\{[^}]*detectedLanguage[^}]*\}\)/
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the push forwards the whole of what the walk found, rather than a list of fields", () => {
|
|
53
|
+
// The one line that closes it. A list of names here is how the fact was lost
|
|
54
|
+
// twice, and a list cannot say that it is incomplete.
|
|
55
|
+
const wiring = source("server.js");
|
|
56
|
+
|
|
57
|
+
assert.match(wiring, /onSubtitleCues\?\.\(\{ sourceKey, fileIndex, \.\.\.entry \}\)/);
|
|
58
|
+
});
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import assert from "node:assert/strict";
|
|
16
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
16
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
17
17
|
import test from "node:test";
|
|
18
18
|
|
|
19
|
-
import { onKeyframeGridFor } from "../services/
|
|
19
|
+
import { onKeyframeGridFor } from "../services/encode/run-command.js";
|
|
20
20
|
|
|
21
21
|
test("a copied picture works on the source's timeline", () => {
|
|
22
22
|
assert.equal(
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import assert from "node:assert/strict";
|
|
18
|
-
import { computeCutGrid } from "../services/output/cut-grid.js";
|
|
19
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
18
|
+
import { computeCutGrid } from "../services/encode/output/cut-grid.js";
|
|
19
|
+
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
20
20
|
import test from "node:test";
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
23
|
+
import { segmentCutTimesFrom } from "../services/encode/run-command.js";
|
|
23
24
|
|
|
24
25
|
/** What the playlist in the player's hands says. */
|
|
25
26
|
const PUBLISHED = [0, 4.004, 8.008, 12.012, 16.016, 20.02];
|
|
@@ -29,10 +30,10 @@ const CORRECTED = [0, 4.004, 6.006, 12.012, 14.014, 20.02];
|
|
|
29
30
|
/**
|
|
30
31
|
* A session that has published one grid and since corrected another.
|
|
31
32
|
*
|
|
32
|
-
* @returns {{ manager:
|
|
33
|
+
* @returns {{ manager: object, session: object }}
|
|
33
34
|
*/
|
|
34
35
|
function sessionWithDriftedGrid() {
|
|
35
|
-
const manager =
|
|
36
|
+
const manager = wireOutputs({
|
|
36
37
|
enabled: true,
|
|
37
38
|
ffmpegBin: "ffmpeg",
|
|
38
39
|
localBindHost: "127.0.0.1",
|
|
@@ -47,7 +48,7 @@ function sessionWithDriftedGrid() {
|
|
|
47
48
|
|
|
48
49
|
test("the cut list is the one the playlist was written from", () => {
|
|
49
50
|
const { manager, session } = sessionWithDriftedGrid();
|
|
50
|
-
const grid = manager.publishedGridFor(session);
|
|
51
|
+
const grid = manager.outputTimes.publishedGridFor(session);
|
|
51
52
|
assert.deepEqual(grid, PUBLISHED);
|
|
52
53
|
// Interior cuts of a run starting at #1, rebased on the run's own start
|
|
53
54
|
// (4.004 s), and stopping before the last entry, which is the file's end.
|
|
@@ -60,7 +61,7 @@ test("the cut list is the one the playlist was written from", () => {
|
|
|
60
61
|
test("a corrected grid does not move the cuts of a session already being read", () => {
|
|
61
62
|
const { manager, session } = sessionWithDriftedGrid();
|
|
62
63
|
const fromCorrected = segmentCutTimesFrom(session.timeline.boundaries, 1);
|
|
63
|
-
const fromPublished = segmentCutTimesFrom(manager.publishedGridFor(session), 1);
|
|
64
|
+
const fromPublished = segmentCutTimesFrom(manager.outputTimes.publishedGridFor(session), 1);
|
|
64
65
|
assert.notDeepEqual(fromCorrected, fromPublished);
|
|
65
66
|
// The gap the field measured: the corrected table would have cut #2 two
|
|
66
67
|
// seconds early, which is four times what a player bridges.
|
|
@@ -70,7 +71,7 @@ test("a corrected grid does not move the cuts of a session already being read",
|
|
|
70
71
|
test("a session that published no grid falls back to the live one", () => {
|
|
71
72
|
const { manager } = sessionWithDriftedGrid();
|
|
72
73
|
const session = { id: "no-playlist", timeline: new Timeline({ boundaries: [...CORRECTED], published: [], cutGrid: "uniform" }) };
|
|
73
|
-
assert.deepEqual(manager.publishedGridFor(session), CORRECTED);
|
|
74
|
+
assert.deepEqual(manager.outputTimes.publishedGridFor(session), CORRECTED);
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
test("no segment is left shorter than a segment at the end of the film", () => {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import test from "node:test";
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
|
-
import { encodeFrame } from "../services/data-channel-handler.js";
|
|
13
|
+
import { encodeFrame } from "../services/transport/data-channel-handler.js";
|
|
14
14
|
|
|
15
15
|
const requestId = Buffer.from("abc123", "ascii");
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import test from "node:test";
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
|
-
import { isUsableTorrentHandle } from "../services/torrent
|
|
22
|
+
import { isUsableTorrentHandle } from "../services/torrent/worker/handle-state.js";
|
|
23
23
|
|
|
24
24
|
test("a live torrent is usable", () => {
|
|
25
25
|
assert.equal(isUsableTorrentHandle({ destroyed: false, files: [{ name: "a.mkv" }] }), true);
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import test from "node:test";
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
|
-
import { declaredRates } from "../services/output/rates.js";
|
|
14
|
-
import { bitrateFor } from "../services/output/playlists.js";
|
|
13
|
+
import { declaredRates } from "../services/encode/output/rates.js";
|
|
14
|
+
import { bitrateFor } from "../services/encode/output/playlists.js";
|
|
15
15
|
|
|
16
16
|
// The field file: 2 806 246 976 bytes over 20:18, cut into 291 pieces.
|
|
17
17
|
const FIELD = { fileLength: 2_806_246_976, durationSeconds: 1218.73 };
|
|
@@ -16,8 +16,8 @@ import test from "node:test";
|
|
|
16
16
|
import assert from "node:assert/strict";
|
|
17
17
|
import { readFileSync } from "node:fs";
|
|
18
18
|
|
|
19
|
-
const planner = readFileSync(new URL("../services/playback-planner.js", import.meta.url), "utf8");
|
|
20
|
-
const sessions = readFileSync(new URL("../services/
|
|
19
|
+
const planner = readFileSync(new URL("../services/media/playback-planner.js", import.meta.url), "utf8");
|
|
20
|
+
const sessions = readFileSync(new URL("../services/encode/Renditions.js", import.meta.url), "utf8");
|
|
21
21
|
|
|
22
22
|
test("every field the declaration reads is a field the cache stores", () => {
|
|
23
23
|
// What `declaredTracks` reads off the cached media info.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
|
|
14
|
-
import { decodeCostOf, fitDecodeCost } from "../services/decode-cost-fit.js";
|
|
14
|
+
import { decodeCostOf, fitDecodeCost } from "../services/encode/decode-cost-fit.js";
|
|
15
15
|
|
|
16
16
|
const FPS = 24;
|
|
17
17
|
|