@torrent-tv/proxy 2.89.17 → 2.89.19
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/README.md +14 -3
- package/package.json +10 -2
- package/server.js +0 -11
- package/utils/client-log-file.js +147 -42
- package/.claude/commands/opsx/apply.md +0 -155
- package/.claude/commands/opsx/archive.md +0 -160
- package/.claude/commands/opsx/explore.md +0 -174
- package/.claude/commands/opsx/propose.md +0 -109
- package/.claude/commands/opsx/sync.md +0 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
- package/.claude/skills/openspec-explore/SKILL.md +0 -289
- package/.claude/skills/openspec-propose/SKILL.md +0 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.dockerignore +0 -5
- package/AGENTS.md +0 -363
- package/CHANGELOG.md +0 -2377
- package/Dockerfile +0 -30
- package/biome.json +0 -412
- package/corpus.tmp.json +0 -1
- package/docs/browser-proxy-contract.md +0 -155
- package/docs/container-architecture.md +0 -273
- package/docs/download-architecture.md +0 -261
- package/docs/encode-architecture.md +0 -765
- package/docs/encode-run-state.md +0 -96
- package/docs/logs.md +0 -151
- package/docs/playback-start-forecast.md +0 -113
- package/docs/socks5-egress-research.md +0 -114
- package/docs/storage-architecture.md +0 -273
- package/knip.json +0 -14
- package/main2.cpuprofile +0 -1
- package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
- package/openspec/changes/adaptive-bitrate/design.md +0 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
- package/openspec/changes/binary-distribution/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/design.md +0 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
- package/openspec/changes/cold-start/design.md +0 -98
- package/openspec/changes/cold-start/proposal.md +0 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
- package/openspec/changes/cold-start/tasks.md +0 -48
- package/openspec/changes/disk-cap/.openspec.yaml +0 -2
- package/openspec/changes/disk-cap/proposal.md +0 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
- package/openspec/changes/disk-cap/tasks.md +0 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
- package/openspec/changes/proxy-observability/design.md +0 -38
- package/openspec/changes/proxy-observability/proposal.md +0 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
- package/openspec/changes/proxy-observability/tasks.md +0 -21
- package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
- package/openspec/changes/seek-debounce/design.md +0 -118
- package/openspec/changes/seek-debounce/proposal.md +0 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
- package/openspec/changes/seek-debounce/tasks.md +0 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
- package/openspec/changes/subtitle-language/proposal.md +0 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
- package/openspec/changes/subtitle-language/tasks.md +0 -26
- package/openspec/changes/track-selection/.openspec.yaml +0 -2
- package/openspec/changes/track-selection/design.md +0 -47
- package/openspec/changes/track-selection/proposal.md +0 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
- package/openspec/changes/track-selection/tasks.md +0 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
- package/openspec/changes/transcode-quality/proposal.md +0 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
- package/openspec/changes/transcode-quality/tasks.md +0 -63
- package/openspec/config.yaml +0 -38
- package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
- package/research/double-spawn-2026-09-10.md +0 -171
- package/research/handover-2026-09-13.md +0 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
- package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
- package/scripts/build-audio-samples.mjs +0 -31
- package/scripts/render-run-graph.js +0 -173
- package/test/asset-paths.test.js +0 -28
- package/test/audio-buffer-continuity.test.js +0 -77
- package/test/audio-calibration.test.js +0 -23
- package/test/audio-inventory.test.js +0 -212
- package/test/audio-track-bitrate.test.js +0 -40
- package/test/audio-warm-recorded.test.js +0 -63
- package/test/auto-quality-step.test.js +0 -762
- package/test/available-share.test.js +0 -76
- package/test/background-fill.test.js +0 -190
- package/test/client-log-file.test.js +0 -126
- package/test/client-logs-route.test.js +0 -34
- package/test/coalesce.test.js +0 -39
- package/test/completed-files.test.js +0 -115
- package/test/concurrent-cost.test.js +0 -148
- package/test/container-media-info.test.js +0 -228
- package/test/contention.test.js +0 -76
- package/test/core-dumps.test.js +0 -57
- package/test/coverage-follows-the-disk.test.js +0 -233
- package/test/coverage-map.test.js +0 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
- package/test/cut-times-timeline.test.js +0 -65
- package/test/cuts-follow-published-grid.test.js +0 -109
- package/test/data-channel-frame.test.js +0 -63
- package/test/data-channel-viewer-lifecycle.test.js +0 -193
- package/test/dead-torrent-handle.test.js +0 -52
- package/test/declared-rates.test.js +0 -116
- package/test/declared-tracks.test.js +0 -43
- package/test/decode-cost-fit.test.js +0 -202
- package/test/decode-cost.test.js +0 -475
- package/test/delivery-probe-field.test.js +0 -173
- package/test/delivery-probe.test.js +0 -382
- package/test/demand-register.test.js +0 -206
- package/test/dht-bootstrap.test.js +0 -72
- package/test/drain-threat.test.js +0 -65
- package/test/duplicate-add.test.js +0 -48
- package/test/encode-admission.test.js +0 -170
- package/test/encode-cost.test.js +0 -275
- package/test/encode-exit.test.js +0 -111
- package/test/encode-orchestrator.test.js +0 -426
- package/test/encode-plan-viewers.test.js +0 -725
- package/test/encode-plan.test.js +0 -545
- package/test/encode-run-state.test.js +0 -314
- package/test/encode-run.test.js +0 -248
- package/test/encode-slope.test.js +0 -46
- package/test/encoded-output.test.js +0 -39
- package/test/encoder-kinds.test.js +0 -122
- package/test/encoder-readings.test.js +0 -68
- package/test/episode-naming.test.js +0 -131
- package/test/every-claimant-is-told-its-share.test.js +0 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
- package/test/failed-start-breaker.test.js +0 -202
- package/test/fastest-wires.test.js +0 -143
- package/test/ffmpeg-stream-counts.test.js +0 -101
- package/test/file-edges.test.js +0 -191
- package/test/files-in-use.test.js +0 -34
- package/test/fixtures/audio-frame-boundary.json +0 -1
- package/test/fixtures/refused-audio-20261002.json +0 -10
- package/test/fmp4-presentation-clock.test.js +0 -148
- package/test/fmp4-self-contained.test.js +0 -80
- package/test/fmp4-timestamps.test.js +0 -140
- package/test/fmp4-track-completeness.test.js +0 -41
- package/test/format-keyed-outputs.test.js +0 -78
- package/test/health-metrics.test.js +0 -36
- package/test/held-read-ownership.test.js +0 -77
- package/test/held-request-width.test.js +0 -157
- package/test/helpers/encode-run.js +0 -248
- package/test/helpers/held-file.js +0 -77
- package/test/helpers/manager.js +0 -62
- package/test/helpers/matroska-file.js +0 -296
- package/test/helpers/output-spec.js +0 -63
- package/test/host-load.test.js +0 -68
- package/test/index-accuracy-is-about-the-file.test.js +0 -128
- package/test/init-compat.test.js +0 -219
- package/test/input-lost-quiets-the-plan.test.js +0 -261
- package/test/input-unavailable.test.js +0 -48
- package/test/installation-id.test.js +0 -39
- package/test/keeping-period.test.js +0 -83
- package/test/keyframe-index-accuracy.test.js +0 -188
- package/test/keyframe-table-budget.test.js +0 -123
- package/test/keyframes-belong-to-the-file.test.js +0 -249
- package/test/learned-median.test.js +0 -37
- package/test/letting-go-asks-who-is-watching.test.js +0 -191
- package/test/limit-row-by-area.test.js +0 -60
- package/test/link-budget.test.js +0 -111
- package/test/link-probe.test.js +0 -56
- package/test/local-encoding-data.test.js +0 -273
- package/test/logger-repeats.test.js +0 -134
- package/test/look-ahead-coverage.test.js +0 -56
- package/test/manager-surface.test.js +0 -291
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cluster-discovery.test.js +0 -323
- package/test/matroska-cues-track.test.js +0 -192
- package/test/media-time.test.js +0 -62
- package/test/memory-budget.test.js +0 -390
- package/test/move-cost.test.js +0 -216
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +0 -173
- package/test/one-authority.test.js +0 -285
- package/test/one-budget.test.js +0 -149
- package/test/one-output-one-session.test.js +0 -273
- package/test/one-resource-divided.test.js +0 -185
- package/test/one-serving-output-per-height.test.js +0 -458
- package/test/orchestrator-wired.test.js +0 -207
- package/test/output-catalog.test.js +0 -39
- package/test/output-shape.test.js +0 -21
- package/test/output-spec.test.js +0 -251
- package/test/output-speed.test.js +0 -92
- package/test/outputs-being-prepared.test.js +0 -40
- package/test/packet-witness-ring.test.js +0 -236
- package/test/packet-witness.test.js +0 -148
- package/test/piece-completeness.test.js +0 -78
- package/test/piece-disk-store.test.js +0 -401
- package/test/piece-from-whole-file.test.js +0 -129
- package/test/piece-lru.test.js +0 -319
- package/test/piece-reader.test.js +0 -162
- package/test/piece-selection-offset.test.js +0 -85
- package/test/piece-store-eviction.test.js +0 -595
- package/test/piece-store-never-refuses.test.js +0 -153
- package/test/piece-store-reservations.test.js +0 -403
- package/test/piece-store-slow-disk.test.js +0 -159
- package/test/piece-tail.test.js +0 -97
- package/test/plan-host-timings.test.js +0 -68
- package/test/plan-is-steady.test.js +0 -225
- package/test/playback-readiness.test.js +0 -518
- package/test/playback-start-regressions.test.js +0 -101
- package/test/priority-map-download.test.js +0 -174
- package/test/priority-map-emptied.test.js +0 -204
- package/test/priority-map-per-output.test.js +0 -215
- package/test/priority-map.test.js +0 -163
- package/test/probe-wedge-certainty.test.js +0 -60
- package/test/produced-copy-choice.test.js +0 -213
- package/test/published-timeline.test.js +0 -83
- package/test/quality-offer.test.js +0 -160
- package/test/quality-variants.test.js +0 -1274
- package/test/rate-control-in-the-output.test.js +0 -181
- package/test/read-survives-withdrawal.test.js +0 -164
- package/test/read-window.test.js +0 -365
- package/test/reads-not-yet-arrived.test.js +0 -218
- package/test/resume-warm.test.js +0 -39
- package/test/run-graph-drift.test.js +0 -37
- package/test/run-inputs.test.js +0 -80
- package/test/run-intervals.test.js +0 -100
- package/test/run-position-follows-published-grid.test.js +0 -89
- package/test/run-progress.test.js +0 -64
- package/test/same-height-switch.test.js +0 -663
- package/test/seek-frees-the-viewers-requests.test.js +0 -217
- package/test/seek-landing.test.js +0 -109
- package/test/seek-target-not-superseded.test.js +0 -195
- package/test/segment-closed-on-both-branches.test.js +0 -114
- package/test/segment-cut-times.test.js +0 -63
- package/test/segment-demand.test.js +0 -80
- package/test/segment-hold-supersede.test.js +0 -91
- package/test/segment-judged-by-its-own-header.test.js +0 -222
- package/test/segment-serve-wiring.test.js +0 -481
- package/test/segment-store-eviction.test.js +0 -247
- package/test/segment-store.test.js +0 -352
- package/test/segment-true-start.test.js +0 -86
- package/test/segments-are-shared.test.js +0 -176
- package/test/send-chunk-bytes.test.js +0 -86
- package/test/serving-existing-output.test.js +0 -111
- package/test/serving-output.test.js +0 -102
- package/test/shared-piece-store.test.js +0 -310
- package/test/sidecar-naming.test.js +0 -142
- package/test/soundtrack-rate-bound.test.js +0 -289
- package/test/source-file.test.js +0 -133
- package/test/source-files-route.test.js +0 -113
- package/test/source-registry.test.js +0 -18
- package/test/split-a-stretch.test.js +0 -139
- package/test/stale-request-after-seek.test.js +0 -158
- package/test/startup-readings.test.js +0 -124
- package/test/stream-route.test.js +0 -237
- package/test/subtitle-cue-framing.test.js +0 -200
- package/test/subtitle-cue-source.test.js +0 -102
- package/test/subtitle-cue-walk.test.js +0 -385
- package/test/subtitle-cursor.test.js +0 -63
- package/test/subtitle-defaults.test.js +0 -118
- package/test/subtitle-language.test.js +0 -252
- package/test/subtitle-metadata.test.js +0 -15
- package/test/subtitle-plan-waits.test.js +0 -125
- package/test/subtitle-track-numbering.test.js +0 -391
- package/test/supply-margin.test.js +0 -190
- package/test/swarm-follows-readers.test.js +0 -174
- package/test/swarm-reach.test.js +0 -130
- package/test/swarm-selection.test.js +0 -220
- package/test/tail-duplication.test.js +0 -167
- package/test/the-channel-has-both-ends.test.js +0 -91
- package/test/the-torrent-thread-serves-bytes.test.js +0 -107
- package/test/thread-pool.test.js +0 -53
- package/test/timeline.test.js +0 -84
- package/test/torrent-contents.test.js +0 -235
- package/test/torrent-cost.test.js +0 -71
- package/test/torrent-files.test.js +0 -221
- package/test/torrent-naming.test.js +0 -255
- package/test/tracks-begin-together.test.js +0 -209
- package/test/tunnel-renewal.test.js +0 -167
- package/test/two-viewers-one-picture.test.js +0 -508
- package/test/upload-hurry.test.js +0 -186
- package/test/usrsctp-state.test.js +0 -131
- package/test/video-facts.test.js +0 -102
- package/test/viewer-assignments.test.js +0 -143
- package/test/viewer-outputs.test.js +0 -285
- package/test/viewer-presence.test.js +0 -116
- package/test/viewer-requests-admission.test.js +0 -92
- package/test/viewer-starts-on-map.test.js +0 -27
- package/test/viewer-states.test.js +0 -192
- package/test/viewer-subtitle-subscription.test.js +0 -96
- package/test/viewer.test.js +0 -139
- package/test/viewing-generations.test.js +0 -390
- package/test/visible-rung.test.js +0 -37
- package/test/wait-ledger.test.js +0 -100
- package/test/webrtc-shim.test.js +0 -149
- package/test/wedge-certainty.test.js +0 -131
- package/test/whole-file-held-read.test.js +0 -30
- package/test/withdraw-piece-claim.test.js +0 -212
- package/test/worker-channel.test.js +0 -158
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
4
|
-
import { producedThroughSeconds, readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
5
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
9
|
-
import { Viewer } from "../services/viewer/Viewer.js";
|
|
10
|
-
|
|
11
|
-
function box(type, body) {
|
|
12
|
-
const header = Buffer.alloc(8);
|
|
13
|
-
header.writeUInt32BE(body.length + 8);
|
|
14
|
-
header.write(type, 4);
|
|
15
|
-
return Buffer.concat([header, body]);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function init(emptyMs, mediaTime, kind = "vide") {
|
|
19
|
-
const mvhd = Buffer.alloc(24);
|
|
20
|
-
mvhd.writeUInt32BE(1000, 12);
|
|
21
|
-
const tkhd = Buffer.alloc(20);
|
|
22
|
-
tkhd.writeUInt32BE(1, 12);
|
|
23
|
-
const mdhd = Buffer.alloc(24);
|
|
24
|
-
mdhd.writeUInt32BE(24000, 12);
|
|
25
|
-
const hdlr = Buffer.alloc(12);
|
|
26
|
-
hdlr.write(kind, 8);
|
|
27
|
-
const elst = Buffer.alloc(32);
|
|
28
|
-
elst.writeUInt32BE(2, 4);
|
|
29
|
-
elst.writeUInt32BE(emptyMs, 8);
|
|
30
|
-
elst.writeInt32BE(-1, 12);
|
|
31
|
-
elst.writeUInt32BE(1000, 20);
|
|
32
|
-
elst.writeInt32BE(mediaTime, 24);
|
|
33
|
-
return box("moov", Buffer.concat([box("mvhd", mvhd), box("trak", Buffer.concat([
|
|
34
|
-
box("tkhd", tkhd), box("edts", box("elst", elst)),
|
|
35
|
-
box("mdia", Buffer.concat([box("mdhd", mdhd), box("hdlr", hdlr)]))
|
|
36
|
-
]))]));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function fragment(decodeTime, composition) {
|
|
40
|
-
const tfhd = Buffer.alloc(12);
|
|
41
|
-
tfhd.writeUIntBE(8, 1, 3);
|
|
42
|
-
tfhd.writeUInt32BE(1, 4);
|
|
43
|
-
tfhd.writeUInt32BE(1000, 8);
|
|
44
|
-
const tfdt = Buffer.alloc(12);
|
|
45
|
-
tfdt[0] = 1;
|
|
46
|
-
tfdt.writeBigUInt64BE(BigInt(decodeTime), 4);
|
|
47
|
-
const trun = Buffer.alloc(12);
|
|
48
|
-
trun.writeUIntBE(0x800, 1, 3);
|
|
49
|
-
trun.writeUInt32BE(1, 4);
|
|
50
|
-
trun.writeUInt32BE(composition, 8);
|
|
51
|
-
return box("moof", box("traf", Buffer.concat([
|
|
52
|
-
box("tfhd", tfhd), box("tfdt", tfdt), box("trun", trun)
|
|
53
|
-
])));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
test("an interrupted frame cannot prove coverage of a full future cut", () => {
|
|
57
|
-
const ranges = readPresentationRanges(Buffer.concat([init(456000, 0), fragment(0, 0)]));
|
|
58
|
-
assert.ok(producedThroughSeconds(ranges) > 456);
|
|
59
|
-
assert.ok(producedThroughSeconds(ranges) < 456.1);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("a shared init preserves presentation times without adding composition delay twice", () => {
|
|
63
|
-
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
64
|
-
const own = init(4125, 3000);
|
|
65
|
-
const media = Buffer.concat([fragment(0, 3000), fragment(48000, 3000)]);
|
|
66
|
-
const raw = Buffer.concat([own, media]);
|
|
67
|
-
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
68
|
-
startSeconds: 4.125, initBytes: shared, rawBytes: raw
|
|
69
|
-
});
|
|
70
|
-
assert.deepEqual(fmp4Format.servedMediaRanges(readPresentationRanges(Buffer.concat([shared, prepared])), { initBytes: null }),
|
|
71
|
-
fmp4Format.servedMediaRanges(readPresentationRanges(raw), { initBytes: null }));
|
|
72
|
-
const times = [];
|
|
73
|
-
walkBoxes(prepared, (type, start) => {
|
|
74
|
-
if (type === "tfdt") times.push(Number(prepared.readBigUInt64BE(start + 4)));
|
|
75
|
-
});
|
|
76
|
-
assert.deepEqual(times, [98000, 146000]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("a piece states each range in ticks with the frame duration a browser allows it", () => {
|
|
80
|
-
const coverage = readPresentationRanges(Buffer.concat([init(0, 0, "vide"), fragment(24000, 0)]));
|
|
81
|
-
assert.deepEqual(coverage.tracks[0].ranges, [{ start: 24000n, end: 25000n, frame: 1000n }]);
|
|
82
|
-
assert.equal(coverage.tracks[0].timescale, 24000n);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("a shared init first read after a seek also supports returning to the beginning", () => {
|
|
86
|
-
const shared = fmp4Format.prepareSharedInit(init(304083, 2000));
|
|
87
|
-
const media = fragment(0, 2000);
|
|
88
|
-
const raw = Buffer.concat([init(83, 2000), media]);
|
|
89
|
-
const prepared = fmp4Format.prepareSegmentBytes(media, {
|
|
90
|
-
startSeconds: 0.083, initBytes: shared, rawBytes: raw
|
|
91
|
-
});
|
|
92
|
-
assert.deepEqual(fmp4Format.servedMediaRanges(readPresentationRanges(Buffer.concat([shared, prepared])), { initBytes: null }),
|
|
93
|
-
fmp4Format.servedMediaRanges(readPresentationRanges(raw), { initBytes: null }));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("HLS fragments use the separate init and the same timeline translation as serving", (t) => {
|
|
97
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "presentation-ranges-"));
|
|
98
|
-
const store = new SegmentStore({ root });
|
|
99
|
-
t.after(() => {
|
|
100
|
-
store.dropAll("the check is over");
|
|
101
|
-
rmSync(root, { recursive: true, force: true });
|
|
102
|
-
});
|
|
103
|
-
const key = "encoded-video";
|
|
104
|
-
store.useFormat(key, fmp4Format);
|
|
105
|
-
const dir = store.directoryFor(key);
|
|
106
|
-
const media = fragment(0, 2000);
|
|
107
|
-
writeFileSync(path.join(dir, fmp4Format.segmentFileName(1)), media);
|
|
108
|
-
assert.equal(store.mediaRangesOf(key, 1, { startSeconds: 4 }), undefined);
|
|
109
|
-
const header = init(0, 0);
|
|
110
|
-
writeFileSync(path.join(dir, fmp4Format.initFileName), header);
|
|
111
|
-
const served = fmp4Format.prepareSegmentBytes(media, { initBytes: header, startSeconds: 4 });
|
|
112
|
-
assert.deepEqual(store.mediaRangesOf(key, 1, { startSeconds: 4 }),
|
|
113
|
-
readPresentationRanges(Buffer.concat([header, served])));
|
|
114
|
-
assert.ok(store.mediaRangesOf(key, 1).tracks[0].ranges[0].start > 4n * 24000n);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("a decode gap between fragments is two ranges, for video as for audio", () => {
|
|
118
|
-
// Joining across a gap is the browser's decision, made in the viewer
|
|
119
|
-
// component; the piece states what it holds.
|
|
120
|
-
const media = Buffer.concat([fragment(0, 0), fragment(72000, 0)]);
|
|
121
|
-
for (const kind of ["vide", "soun"]) {
|
|
122
|
-
const coverage = readPresentationRanges(Buffer.concat([init(0, 0, kind), media]));
|
|
123
|
-
assert.deepEqual(coverage.tracks[0].ranges.map(({ start, end }) => [start, end]),
|
|
124
|
-
[[0n, 1000n], [72000n, 73000n]]);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("served ranges move every track by the session init's position, in ticks", () => {
|
|
129
|
-
const audioHeader = init(0, 0, "soun");
|
|
130
|
-
const audio = readPresentationRanges(Buffer.concat([audioHeader, fragment(96000, 0)]));
|
|
131
|
-
assert.equal(fmp4Format.servedMediaRanges(audio, { initBytes: audioHeader }).ranges[0].start, 96000n);
|
|
132
|
-
const shared = fmp4Format.prepareSharedInit(init(83, 2000));
|
|
133
|
-
const video = readPresentationRanges(Buffer.concat([init(83, 2000), fragment(0, 2000)]));
|
|
134
|
-
// The piece's own edit puts its first frame at round(83 ms * 24000) = 1992
|
|
135
|
-
// ticks; the served bytes carry the composition delay the shared init's
|
|
136
|
-
// edit removes, which is what a page's timestampOffset then takes back.
|
|
137
|
-
assert.equal(video.tracks[0].ranges[0].start, 1992n);
|
|
138
|
-
assert.equal(fmp4Format.servedMediaRanges(video, { initBytes: shared }).ranges[0].start, 1992n + 2000n);
|
|
139
|
-
assert.deepEqual(fmp4Format.servedMediaRanges(video, { initBytes: null }).ranges, video.tracks[0].ranges);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("a viewer retains only finite per-track player clock offsets", () => {
|
|
143
|
-
const viewer = new Viewer("clock-reading", 1);
|
|
144
|
-
viewer.report({ timestampOffsets: { video: -0.083, audio: 0, media: 400 } }, 2);
|
|
145
|
-
assert.deepEqual(viewer.linkReading().timestampOffsets, { video: -0.083, audio: 0 });
|
|
146
|
-
viewer.report({ timestampOffsets: { video: Number.POSITIVE_INFINITY } }, 3);
|
|
147
|
-
assert.deepEqual(viewer.linkReading().timestampOffsets, {});
|
|
148
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Splitting a self-contained fMP4 piece into an init segment and a media
|
|
3
|
-
* segment.
|
|
4
|
-
*
|
|
5
|
-
* The muxer that takes explicit cut times writes every piece whole —
|
|
6
|
-
* `ftyp moov moof mdat … mfra` — but HLS wants one init named by `#EXT-X-MAP`
|
|
7
|
-
* and media segments carrying only fragments. Cutting at the wrong offset does
|
|
8
|
-
* not fail loudly; it produces something that parses as garbage, which is why
|
|
9
|
-
* the boundaries are pinned here rather than trusted.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A piece shaped like the muxer's output. Sizes are what matters, not contents.
|
|
18
|
-
*
|
|
19
|
-
* @param {{ withIndex?: boolean }} [options]
|
|
20
|
-
* @returns {{ piece: Buffer, initLength: number, fragmentsLength: number }}
|
|
21
|
-
*/
|
|
22
|
-
function makePiece({ withIndex = true } = {}) {
|
|
23
|
-
const box = (type, bodyLength) => {
|
|
24
|
-
const buffer = Buffer.alloc(8 + bodyLength);
|
|
25
|
-
buffer.writeUInt32BE(8 + bodyLength, 0);
|
|
26
|
-
buffer.write(type, 4, "latin1");
|
|
27
|
-
buffer.fill(0x5a, 8);
|
|
28
|
-
return buffer;
|
|
29
|
-
};
|
|
30
|
-
const ftyp = box("ftyp", 24);
|
|
31
|
-
const moov = box("moov", 400);
|
|
32
|
-
const moof = box("moof", 120);
|
|
33
|
-
const mdat = box("mdat", 900);
|
|
34
|
-
const mfra = box("mfra", 40);
|
|
35
|
-
const parts = withIndex ? [ftyp, moov, moof, mdat, mfra] : [ftyp, moov, moof, mdat];
|
|
36
|
-
return {
|
|
37
|
-
piece: Buffer.concat(parts),
|
|
38
|
-
initLength: ftyp.length + moov.length,
|
|
39
|
-
fragmentsLength: moof.length + mdat.length
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
test("the init is everything before the first fragment", () => {
|
|
44
|
-
const { piece, initLength } = makePiece();
|
|
45
|
-
const init = fmp4Format.extractInit(piece);
|
|
46
|
-
assert.ok(init, "no init found in a piece that has one");
|
|
47
|
-
assert.equal(init.length, initLength, "init must end exactly where the first fragment begins");
|
|
48
|
-
assert.equal(init.toString("latin1", 4, 8), "ftyp", "init must start at the file header");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("the media segment is the fragments alone, index dropped", () => {
|
|
52
|
-
const { piece, initLength, fragmentsLength } = makePiece();
|
|
53
|
-
const media = fmp4Format.stripInit(piece);
|
|
54
|
-
assert.equal(media.toString("latin1", 4, 8), "moof", "a media segment must begin at a fragment");
|
|
55
|
-
assert.equal(
|
|
56
|
-
media.length,
|
|
57
|
-
fragmentsLength,
|
|
58
|
-
"the trailing random-access index must be dropped: its offsets describe a file that no longer exists"
|
|
59
|
-
);
|
|
60
|
-
assert.equal(piece.length, initLength + fragmentsLength + 48, "test fixture sanity");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("a piece without a trailing index keeps everything to the end", () => {
|
|
64
|
-
const { piece, fragmentsLength } = makePiece({ withIndex: false });
|
|
65
|
-
assert.equal(fmp4Format.stripInit(piece).length, fragmentsLength);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("a segment that is already fragments-only is left alone", () => {
|
|
69
|
-
const alreadyMedia = fmp4Format.stripInit(makePiece().piece);
|
|
70
|
-
assert.deepEqual(
|
|
71
|
-
fmp4Format.stripInit(alreadyMedia),
|
|
72
|
-
alreadyMedia,
|
|
73
|
-
"stripping twice must not eat the first fragment"
|
|
74
|
-
);
|
|
75
|
-
assert.equal(
|
|
76
|
-
fmp4Format.extractInit(alreadyMedia),
|
|
77
|
-
null,
|
|
78
|
-
"there is no init to lift out of a media segment, and inventing one would corrupt playback"
|
|
79
|
-
);
|
|
80
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stamping a segment's position onto its fragments.
|
|
3
|
-
*
|
|
4
|
-
* A segment produced by the explicit-cut muxer holds SEVERAL fragments per
|
|
5
|
-
* track — `frag_keyframe` opens one at every keyframe, while a cut point comes
|
|
6
|
-
* only every few keyframes. Writing the segment's start into each of them made
|
|
7
|
-
* them all claim the same decode time, and the player re-fetched the segment
|
|
8
|
-
* forever. The positions inside a segment must therefore survive.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { stampSegmentStartTime } from "../services/encode/segment-formats/mp4-boxes.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} type
|
|
17
|
-
* @param {Buffer} body
|
|
18
|
-
* @returns {Buffer}
|
|
19
|
-
*/
|
|
20
|
-
function box(type, body) {
|
|
21
|
-
const header = Buffer.alloc(8);
|
|
22
|
-
header.writeUInt32BE(8 + body.length, 0);
|
|
23
|
-
header.write(type, 4, "latin1");
|
|
24
|
-
return Buffer.concat([header, body]);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* One fragment: `moof > traf > (tfhd, tfdt)`, plus the payload it describes.
|
|
29
|
-
*
|
|
30
|
-
* @param {{ trackId: number, decodeTime: number, version?: 0 | 1 }} fragment
|
|
31
|
-
* @returns {Buffer}
|
|
32
|
-
*/
|
|
33
|
-
function makeFragment({ trackId, decodeTime, version = 1 }) {
|
|
34
|
-
const tfhdBody = Buffer.alloc(8);
|
|
35
|
-
tfhdBody.writeUInt32BE(trackId, 4);
|
|
36
|
-
const tfdtBody = Buffer.alloc(version === 1 ? 12 : 8);
|
|
37
|
-
tfdtBody[0] = version;
|
|
38
|
-
if (version === 1) {
|
|
39
|
-
tfdtBody.writeBigUInt64BE(BigInt(decodeTime), 4);
|
|
40
|
-
} else {
|
|
41
|
-
tfdtBody.writeUInt32BE(decodeTime, 4);
|
|
42
|
-
}
|
|
43
|
-
const traf = box("traf", Buffer.concat([box("tfhd", tfhdBody), box("tfdt", tfdtBody)]));
|
|
44
|
-
return Buffer.concat([box("moof", traf), box("mdat", Buffer.alloc(16, 0x5a))]);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Every `tfdt` in order, as `[trackId, decodeTime]`.
|
|
49
|
-
*
|
|
50
|
-
* @param {Buffer} segment
|
|
51
|
-
* @returns {Array<[number, number]>}
|
|
52
|
-
*/
|
|
53
|
-
function readDecodeTimes(segment) {
|
|
54
|
-
const found = [];
|
|
55
|
-
let offset = 0;
|
|
56
|
-
let trackId = 0;
|
|
57
|
-
while (offset + 8 <= segment.length) {
|
|
58
|
-
const size = segment.readUInt32BE(offset);
|
|
59
|
-
const type = segment.toString("latin1", offset + 4, offset + 8);
|
|
60
|
-
if (type === "tfhd") {
|
|
61
|
-
trackId = segment.readUInt32BE(offset + 12);
|
|
62
|
-
} else if (type === "tfdt") {
|
|
63
|
-
const version = segment[offset + 8];
|
|
64
|
-
found.push([
|
|
65
|
-
trackId,
|
|
66
|
-
version === 1 ? Number(segment.readBigUInt64BE(offset + 12)) : segment.readUInt32BE(offset + 12)
|
|
67
|
-
]);
|
|
68
|
-
}
|
|
69
|
-
// Descend into the containers on the way to `tfdt`; skip anything else
|
|
70
|
-
// whole, so `mdat` is never walked into.
|
|
71
|
-
offset += type === "moof" || type === "traf" ? 8 : size;
|
|
72
|
-
}
|
|
73
|
-
return found;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
test("fragments keep their distance from the start of the segment", () => {
|
|
77
|
-
// A 6 s segment holding three fragments at 0, 2 and 4 s of its own clock —
|
|
78
|
-
// the shape measured on the field host.
|
|
79
|
-
const timescale = 16_000;
|
|
80
|
-
const segment = Buffer.concat([
|
|
81
|
-
makeFragment({ trackId: 1, decodeTime: 0 }),
|
|
82
|
-
makeFragment({ trackId: 1, decodeTime: 2 * timescale }),
|
|
83
|
-
makeFragment({ trackId: 1, decodeTime: 4 * timescale })
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
const stamped = stampSegmentStartTime(segment, 6, new Map([[1, timescale]]));
|
|
87
|
-
|
|
88
|
-
assert.deepEqual(
|
|
89
|
-
readDecodeTimes(stamped).map(([, time]) => time / timescale),
|
|
90
|
-
[6, 8, 10],
|
|
91
|
-
"each fragment must land at the segment's start plus its own offset"
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("each track is shifted by its own base", () => {
|
|
96
|
-
const videoScale = 16_000;
|
|
97
|
-
const audioScale = 44_100;
|
|
98
|
-
// Audio does not start at zero: its frames do not align with the video's.
|
|
99
|
-
const segment = Buffer.concat([
|
|
100
|
-
makeFragment({ trackId: 1, decodeTime: 0 }),
|
|
101
|
-
makeFragment({ trackId: 2, decodeTime: 1024 }),
|
|
102
|
-
makeFragment({ trackId: 1, decodeTime: 2 * videoScale }),
|
|
103
|
-
makeFragment({ trackId: 2, decodeTime: 1024 + 2 * audioScale })
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
const stamped = stampSegmentStartTime(
|
|
107
|
-
segment,
|
|
108
|
-
6,
|
|
109
|
-
new Map([
|
|
110
|
-
[1, videoScale],
|
|
111
|
-
[2, audioScale]
|
|
112
|
-
])
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
assert.deepEqual(readDecodeTimes(stamped), [
|
|
116
|
-
[1, 6 * videoScale],
|
|
117
|
-
[2, 6 * audioScale],
|
|
118
|
-
[1, 8 * videoScale],
|
|
119
|
-
[2, 8 * audioScale]
|
|
120
|
-
]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("a single fragment is written outright, as before", () => {
|
|
124
|
-
const stamped = stampSegmentStartTime(
|
|
125
|
-
makeFragment({ trackId: 1, decodeTime: 1280 }),
|
|
126
|
-
12,
|
|
127
|
-
new Map([[1, 16_000]])
|
|
128
|
-
);
|
|
129
|
-
assert.deepEqual(readDecodeTimes(stamped), [[1, 12 * 16_000]]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("a 32-bit field too small for the position is left alone", () => {
|
|
133
|
-
const segment = makeFragment({ trackId: 1, decodeTime: 0, version: 0 });
|
|
134
|
-
const stamped = stampSegmentStartTime(segment, 1_000_000, new Map([[1, 90_000]]));
|
|
135
|
-
assert.deepEqual(
|
|
136
|
-
readDecodeTimes(stamped),
|
|
137
|
-
[[1, 0]],
|
|
138
|
-
"a wrapped value would send the player somewhere arbitrary; leaving it is the lesser harm"
|
|
139
|
-
);
|
|
140
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A segment that is short of a track must not be served.
|
|
3
|
-
*
|
|
4
|
-
* A run that is terminated closes its current output file properly — trailing
|
|
5
|
-
* index and all — but the file holds only what had been muxed by then, and
|
|
6
|
-
* after a seek-restart that is routinely one track of two. Nothing about it
|
|
7
|
-
* looks unfinished, so the readiness rule called it done and served it, and the
|
|
8
|
-
* player could not complete the seek. Measured 2026-08-06: segment #133 carried
|
|
9
|
-
* one `tfdt` where its neighbours carried two, every request for it was
|
|
10
|
-
* answered in 98 ms, and the viewer sat on a spinner.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
16
|
-
// Build a minimal init declaring two tracks, and fragments declaring one/two.
|
|
17
|
-
function box(type, body) {
|
|
18
|
-
const b = Buffer.alloc(8 + body.length);
|
|
19
|
-
b.writeUInt32BE(8 + body.length, 0); b.write(type, 4, "latin1"); body.copy(b, 8);
|
|
20
|
-
return b;
|
|
21
|
-
}
|
|
22
|
-
function mdhd(timescale) { const b = Buffer.alloc(20); b.writeUInt32BE(timescale, 12); return box("mdhd", b); }
|
|
23
|
-
function tkhd(id) { const b = Buffer.alloc(84); b.writeUInt32BE(id, 12); return box("tkhd", b); }
|
|
24
|
-
function trak(id, ts) { return box("trak", Buffer.concat([tkhd(id), box("mdia", mdhd(ts))])); }
|
|
25
|
-
const init = box("moov", Buffer.concat([trak(1, 16000), trak(2, 48000)]));
|
|
26
|
-
function tfhd(id) { const b = Buffer.alloc(8); b.writeUInt32BE(id, 4); return box("tfhd", b); }
|
|
27
|
-
function moof(ids) { return box("moof", Buffer.concat(ids.map((id) => box("traf", tfhd(id))))); }
|
|
28
|
-
|
|
29
|
-
test("a segment with every track is accepted", () => {
|
|
30
|
-
assert.equal(fmp4Format.hasEveryTrack(moof([1, 2]), init), true);
|
|
31
|
-
});
|
|
32
|
-
test("a segment left short of a track by a killed run is refused", () => {
|
|
33
|
-
assert.equal(fmp4Format.hasEveryTrack(moof([1]), init), false,
|
|
34
|
-
"one track of two is what a terminated run leaves behind, and it looks complete");
|
|
35
|
-
});
|
|
36
|
-
test("an empty file is refused", () => {
|
|
37
|
-
assert.equal(fmp4Format.hasEveryTrack(Buffer.alloc(0), init), false);
|
|
38
|
-
});
|
|
39
|
-
test("without an init there is nothing to compare against, so it is allowed", () => {
|
|
40
|
-
assert.equal(fmp4Format.hasEveryTrack(moof([1]), null), true);
|
|
41
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file An output is named by the format it produces, and nothing may put
|
|
3
|
-
* pieces of another format under that name.
|
|
4
|
-
*
|
|
5
|
-
* Two ways that could still happen once the name became the format: a
|
|
6
|
-
* directory left by an earlier version, whose key named the box a viewer asked
|
|
7
|
-
* for and so cannot say what format is inside; and a hardware encoder failing,
|
|
8
|
-
* after which the proxy encodes with software while outputs named by the failed
|
|
9
|
-
* encoder are still being watched.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
17
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
18
|
-
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
19
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
20
|
-
|
|
21
|
-
test("a directory whose key names an asked-for box is not adopted, and one naming a format is", (t) => {
|
|
22
|
-
const { store, root, manager, cleanup } = managerWithOwnStore();
|
|
23
|
-
t.after(cleanup);
|
|
24
|
-
const oldKey = "torrent:abc:fmt=fmp4:grid=even@0:video-only:v=0/enc:1280x720:budget";
|
|
25
|
-
const newKey = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, audioSeparate: true }).toKey();
|
|
26
|
-
const oldDir = store.directoryFor(oldKey);
|
|
27
|
-
const newDir = store.directoryFor(newKey);
|
|
28
|
-
writeFileSync(path.join(oldDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
29
|
-
writeFileSync(path.join(newDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
30
|
-
|
|
31
|
-
const result = manager.lifecycle.adoptSegmentsLeftBehind();
|
|
32
|
-
|
|
33
|
-
assert.equal(result.adopted, 1);
|
|
34
|
-
assert.equal(result.dropped, 1);
|
|
35
|
-
assert.equal(existsSync(oldDir), false, "the old directory goes");
|
|
36
|
-
assert.equal(existsSync(newDir), true, "the named format stays");
|
|
37
|
-
assert.ok(root);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("when a hardware encoder fails, the outputs it named are closed rather than continued by software", async (t) => {
|
|
41
|
-
const { manager, cleanup } = managerWithOwnStore({
|
|
42
|
-
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
43
|
-
});
|
|
44
|
-
t.after(async () => {
|
|
45
|
-
await manager.lifecycle.disposeAll();
|
|
46
|
-
cleanup();
|
|
47
|
-
});
|
|
48
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
49
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
50
|
-
const file = new SourceFile({ sourceKey: "torrent:abc", fileIndex: 0, name: "film.mkv" });
|
|
51
|
-
const record = (id, encoder) => {
|
|
52
|
-
const spec = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, encoder });
|
|
53
|
-
const output = { id, spec, get outputKey() { return this.spec.toKey(); }, file };
|
|
54
|
-
manager.outputs.set(id, output);
|
|
55
|
-
return output;
|
|
56
|
-
};
|
|
57
|
-
const failing = record("aaaaaaaa00000001", "h264_vaapi");
|
|
58
|
-
const software = record("aaaaaaaa00000002", "libx264");
|
|
59
|
-
const run = { from: 0, to: -1, argsDescribed: "ffmpeg …" };
|
|
60
|
-
manager.encodeOrchestrator.adopt(failing.outputKey, run);
|
|
61
|
-
|
|
62
|
-
manager.encodeRuns.noteRunEnded(failing, run, {
|
|
63
|
-
address: failing.outputKey,
|
|
64
|
-
run,
|
|
65
|
-
ending: ENCODE_EXIT.FAILED,
|
|
66
|
-
from: 0,
|
|
67
|
-
to: -1,
|
|
68
|
-
livedMs: 30_000,
|
|
69
|
-
because: "vaapi failed",
|
|
70
|
-
lastError: "vaapi failed",
|
|
71
|
-
producedCount: 3
|
|
72
|
-
});
|
|
73
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
74
|
-
|
|
75
|
-
assert.equal(manager.encoders.current.kind, "software");
|
|
76
|
-
assert.equal(manager.outputs.has(failing.id), false, "an output of the failed encoder is closed");
|
|
77
|
-
assert.equal(manager.outputs.has(software.id), true, "an output of another encoder is untouched");
|
|
78
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a proxy says about itself, and what the pool scores it on.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import os from "node:os";
|
|
8
|
-
|
|
9
|
-
import { collectHealthMetrics } from "../services/transport/health-collector.js";
|
|
10
|
-
import { availableMemory, availableMemoryBytes } from "../services/storage/machine-memory.js";
|
|
11
|
-
|
|
12
|
-
test("free memory is what could be given out, not what is idle this instant", () => {
|
|
13
|
-
const available = availableMemory();
|
|
14
|
-
assert.ok(Number.isFinite(available.bytes) && available.bytes > 0);
|
|
15
|
-
|
|
16
|
-
// On Linux this is the kernel's own `MemAvailable`, not `os.freemem()`: the
|
|
17
|
-
// kernel keeps free memory low on purpose and fills the rest with cache, which
|
|
18
|
-
// it hands back the moment anything asks. The old reading was `os.freemem()`,
|
|
19
|
-
// so a host with 4 GB of cache and 200 MB genuinely free reported itself nearly
|
|
20
|
-
// full while it had 4.2 GB to give — and that figure weighs 0.4 of every
|
|
21
|
-
// proxy's score. Asked of the reading itself: comparing it with a second
|
|
22
|
-
// reading taken a moment later measures whatever else the machine was doing.
|
|
23
|
-
if (os.platform() === "linux") {
|
|
24
|
-
assert.equal(available.measured, true, "the kernel's own estimate was not read");
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("the health report is three bounded numbers", () => {
|
|
29
|
-
const metrics = collectHealthMetrics({ availableMemoryBytes });
|
|
30
|
-
assert.ok(metrics.cpuLoad >= 0);
|
|
31
|
-
assert.ok(metrics.memFree > 0 && metrics.memFree <= 1);
|
|
32
|
-
assert.ok(Number.isInteger(metrics.uptime) && metrics.uptime >= 0);
|
|
33
|
-
// Three decimals, so a value that has not really moved does not produce a
|
|
34
|
-
// different message on every poll.
|
|
35
|
-
assert.equal(metrics.memFree, Math.round(metrics.memFree * 1000) / 1000);
|
|
36
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A read of what is downloaded asks for nothing, and hands the other
|
|
3
|
-
* thread memory that is its own.
|
|
4
|
-
*
|
|
5
|
-
* Until 2026-10-01 the read opened the torrent's own file stream on bytes that
|
|
6
|
-
* had not arrived; that stream selects pieces itself, outside the demand
|
|
7
|
-
* register, and waited up to thirty seconds — 36 such reads at a torrent's open
|
|
8
|
-
* competed with the file being watched. And the bytes it returns are now handed
|
|
9
|
-
* to the main thread instead of copied, which is safe only for a buffer that is
|
|
10
|
-
* the whole of its own memory. Fakes only.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { Readable } from "node:stream";
|
|
16
|
-
import { MessageChannel } from "node:worker_threads";
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
|
-
|
|
19
|
-
import { ownsItsMemory, readHeldBytes } from "../services/torrent/worker/held-bytes.js";
|
|
20
|
-
|
|
21
|
-
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
22
|
-
|
|
23
|
-
/** A torrent of one file whose stream serves views of one shared store buffer. */
|
|
24
|
-
function torrentOver(store, held) {
|
|
25
|
-
const streams = [];
|
|
26
|
-
const file = {
|
|
27
|
-
name: "film.mkv",
|
|
28
|
-
length: store.length,
|
|
29
|
-
offset: 0,
|
|
30
|
-
createReadStream({ start, end }) {
|
|
31
|
-
streams.push({ start, end });
|
|
32
|
-
return Readable.from([store.subarray(start, end + 1)]);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
return {
|
|
36
|
-
streams,
|
|
37
|
-
torrent: { pieceLength: 16, bitfield: { get: (index) => held(index) }, files: [file] }
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test("a range not wholly downloaded is answered at once with nothing, and no stream is opened", async () => {
|
|
42
|
-
const store = Buffer.alloc(64, 1);
|
|
43
|
-
const { torrent, streams } = torrentOver(store, (index) => index !== 2);
|
|
44
|
-
assert.equal(await readHeldBytes(torrent, 0, 0, 63), null);
|
|
45
|
-
assert.equal(streams.length, 0, "nothing was asked of the torrent");
|
|
46
|
-
assert.notEqual(await readHeldBytes(torrent, 0, 0, 31), null, "a range that is here is read");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("what a held read returns owns its memory; a pool slice and shared memory do not", async () => {
|
|
50
|
-
const store = Buffer.from(Array.from({ length: 64 }, (_, index) => index));
|
|
51
|
-
const { torrent } = torrentOver(store, () => true);
|
|
52
|
-
const bytes = await readHeldBytes(torrent, 0, 3, 40);
|
|
53
|
-
assert.equal(ownsItsMemory(bytes), true);
|
|
54
|
-
assert.equal(ownsItsMemory(Buffer.from("small")), false, "a slice of Node's pool");
|
|
55
|
-
assert.equal(ownsItsMemory(new Uint8Array(new SharedArrayBuffer(8))), false, "shared memory");
|
|
56
|
-
assert.equal(ownsItsMemory(store.subarray(4, 8)), false, "a view into another buffer");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("handing a read's bytes to another thread leaves the store and an overlapping read as they were", async () => {
|
|
60
|
-
const store = Buffer.from(Array.from({ length: 256 }, (_, index) => index % 251));
|
|
61
|
-
const before = digest(store);
|
|
62
|
-
const { torrent } = torrentOver(store, () => true);
|
|
63
|
-
const [first, second] = await Promise.all([readHeldBytes(torrent, 0, 10, 200), readHeldBytes(torrent, 0, 50, 120)]);
|
|
64
|
-
const secondBefore = digest(second);
|
|
65
|
-
|
|
66
|
-
const { port1, port2 } = new MessageChannel();
|
|
67
|
-
const received = new Promise((resolve) => port2.once("message", resolve));
|
|
68
|
-
port1.postMessage({ bytes: first }, [first.buffer]);
|
|
69
|
-
const message = await received;
|
|
70
|
-
port1.close();
|
|
71
|
-
port2.close();
|
|
72
|
-
|
|
73
|
-
assert.equal(first.byteLength, 0, "the sender no longer holds the memory it handed over");
|
|
74
|
-
assert.deepEqual(Buffer.from(message.bytes), store.subarray(10, 201), "the receiver has every byte");
|
|
75
|
-
assert.equal(digest(store), before, "the store's own bytes are untouched");
|
|
76
|
-
assert.equal(digest(second), secondBefore, "the overlapping read is untouched");
|
|
77
|
-
});
|