@torrent-tv/proxy 2.89.17 → 2.89.18
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/.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,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The wire format of a body frame.
|
|
3
|
-
*
|
|
4
|
-
* The browser parses these bytes, so the layout is a contract:
|
|
5
|
-
* `[flags(1)][idLen(1)][requestId][payload]`. The framing was rewritten to stop
|
|
6
|
-
* copying every chunk twice (75.9 ms per 13 MB segment against 40.0 on the
|
|
7
|
-
* field host), and a rewrite of something a client parses needs the format
|
|
8
|
-
* pinned down, not just the timing.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { encodeFrame } from "../services/transport/data-channel-handler.js";
|
|
14
|
-
|
|
15
|
-
const requestId = Buffer.from("abc123", "ascii");
|
|
16
|
-
|
|
17
|
-
test("a body frame carries the id and the payload unchanged", () => {
|
|
18
|
-
const payload = Buffer.from([1, 2, 3, 4, 250, 255]);
|
|
19
|
-
const frame = encodeFrame(requestId, payload, false);
|
|
20
|
-
|
|
21
|
-
assert.equal(frame[0], 0, "flagged as done");
|
|
22
|
-
assert.equal(frame[1], requestId.length);
|
|
23
|
-
assert.deepEqual(frame.subarray(2, 2 + requestId.length), requestId);
|
|
24
|
-
assert.deepEqual(frame.subarray(2 + requestId.length), payload);
|
|
25
|
-
assert.equal(frame.length, 2 + requestId.length + payload.length);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("the done frame carries no payload", () => {
|
|
29
|
-
const frame = encodeFrame(requestId, null, true);
|
|
30
|
-
|
|
31
|
-
assert.equal(frame[0], 1);
|
|
32
|
-
assert.equal(frame.length, 2 + requestId.length);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("an empty payload is treated as no payload", () => {
|
|
36
|
-
const frame = encodeFrame(requestId, new Uint8Array(0), false);
|
|
37
|
-
assert.equal(frame.length, 2 + requestId.length);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("a payload that is a view into a larger buffer is copied correctly", () => {
|
|
41
|
-
// Chunks arrive as views into a bigger allocation, so copying the whole
|
|
42
|
-
// underlying buffer instead of the view would send the wrong bytes at the
|
|
43
|
-
// wrong length — silently.
|
|
44
|
-
const backing = Buffer.alloc(64, 9);
|
|
45
|
-
backing.fill(42, 16, 32);
|
|
46
|
-
const view = new Uint8Array(backing.buffer, backing.byteOffset + 16, 16);
|
|
47
|
-
|
|
48
|
-
const frame = encodeFrame(requestId, view, false);
|
|
49
|
-
|
|
50
|
-
assert.equal(frame.length, 2 + requestId.length + 16);
|
|
51
|
-
assert.deepEqual(frame.subarray(2 + requestId.length), Buffer.alloc(16, 42));
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("a large payload survives framing byte for byte", () => {
|
|
55
|
-
const payload = Buffer.allocUnsafeSlow(64 * 1024);
|
|
56
|
-
for (let at = 0; at < payload.length; at += 1) {
|
|
57
|
-
payload[at] = at % 251;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const frame = encodeFrame(requestId, payload, false);
|
|
61
|
-
|
|
62
|
-
assert.deepEqual(frame.subarray(2 + requestId.length), payload);
|
|
63
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { createDataChannelHandler } from "../services/transport/data-channel-handler.js";
|
|
4
|
-
|
|
5
|
-
class FakeDataChannel {
|
|
6
|
-
constructor(label) {
|
|
7
|
-
this.label = label;
|
|
8
|
-
this.messages = [];
|
|
9
|
-
this.closed = null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
getLabel() {
|
|
13
|
-
return this.label;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
onMessage(callback) {
|
|
17
|
-
this.message = callback;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
onClosed(callback) {
|
|
21
|
-
this.closed = callback;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
onError() {}
|
|
25
|
-
|
|
26
|
-
sendMessage(message) {
|
|
27
|
-
this.messages.push(message);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
close() {
|
|
31
|
-
this.closed?.();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test("a viewer remains present until every data channel on the connection closes", () => {
|
|
36
|
-
const gone = [];
|
|
37
|
-
const handler = createDataChannelHandler({
|
|
38
|
-
proxyPort: 9090,
|
|
39
|
-
getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
|
|
40
|
-
onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
|
|
41
|
-
});
|
|
42
|
-
const media = new FakeDataChannel("proxy");
|
|
43
|
-
const control = new FakeDataChannel("proxy-control");
|
|
44
|
-
const probe = new FakeDataChannel("proxy-fast");
|
|
45
|
-
|
|
46
|
-
handler.handleChannel("peer-session", media);
|
|
47
|
-
handler.handleChannel("peer-session", control);
|
|
48
|
-
handler.handleChannel("peer-session", probe);
|
|
49
|
-
control.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
|
|
50
|
-
|
|
51
|
-
control.close();
|
|
52
|
-
assert.deepEqual(gone, [], "closing the control channel alone does not release the viewer");
|
|
53
|
-
|
|
54
|
-
media.close();
|
|
55
|
-
assert.deepEqual(gone, [], "the viewer remains while the probe channel is still open");
|
|
56
|
-
|
|
57
|
-
probe.close();
|
|
58
|
-
assert.deepEqual(gone, [
|
|
59
|
-
{ consumerId: "viewer-1", reason: "the connection closed" }
|
|
60
|
-
]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("a closing connection does not release a viewer already named on its replacement", () => {
|
|
64
|
-
const gone = [];
|
|
65
|
-
const handler = createDataChannelHandler({
|
|
66
|
-
proxyPort: 9090,
|
|
67
|
-
getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
|
|
68
|
-
onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
|
|
69
|
-
});
|
|
70
|
-
const oldControl = new FakeDataChannel("proxy-control");
|
|
71
|
-
const oldMedia = new FakeDataChannel("proxy");
|
|
72
|
-
const newControl = new FakeDataChannel("proxy-control");
|
|
73
|
-
const newMedia = new FakeDataChannel("proxy");
|
|
74
|
-
|
|
75
|
-
handler.handleChannel("old-peer", oldControl);
|
|
76
|
-
handler.handleChannel("old-peer", oldMedia);
|
|
77
|
-
oldControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
|
|
78
|
-
handler.handleChannel("new-peer", newControl);
|
|
79
|
-
handler.handleChannel("new-peer", newMedia);
|
|
80
|
-
newControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
|
|
81
|
-
|
|
82
|
-
oldControl.close();
|
|
83
|
-
oldMedia.close();
|
|
84
|
-
assert.deepEqual(gone, [], "the old connection no longer owns the viewer");
|
|
85
|
-
|
|
86
|
-
newControl.close();
|
|
87
|
-
assert.deepEqual(gone, [], "the replacement still has its media channel");
|
|
88
|
-
newMedia.close();
|
|
89
|
-
assert.deepEqual(gone, [
|
|
90
|
-
{ consumerId: "viewer-1", reason: "the connection closed" }
|
|
91
|
-
]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* A page on an old connection that raises a trial connection beside it, the
|
|
96
|
-
* way it does when the old one stops delivering.
|
|
97
|
-
*/
|
|
98
|
-
function trialBeside(gone) {
|
|
99
|
-
const handler = createDataChannelHandler({
|
|
100
|
-
proxyPort: 9090,
|
|
101
|
-
getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
|
|
102
|
-
viewersWantingCues: () => ["viewer-1"],
|
|
103
|
-
onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
|
|
104
|
-
});
|
|
105
|
-
const old = { control: new FakeDataChannel("proxy-control"), media: new FakeDataChannel("proxy") };
|
|
106
|
-
const trial = { control: new FakeDataChannel("proxy-control"), media: new FakeDataChannel("proxy") };
|
|
107
|
-
handler.handleChannel("old-peer", old.control);
|
|
108
|
-
handler.handleChannel("old-peer", old.media);
|
|
109
|
-
old.control.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
|
|
110
|
-
handler.handleChannel("trial-peer", trial.control);
|
|
111
|
-
handler.handleChannel("trial-peer", trial.media);
|
|
112
|
-
trial.control.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
|
|
113
|
-
const cuesOn = (channel) => channel.messages.filter((message) => message.includes("subtitle-cues")).length;
|
|
114
|
-
const push = () =>
|
|
115
|
-
handler.publishSubtitleCues({
|
|
116
|
-
sourceKey: "torrent:abc",
|
|
117
|
-
fileIndex: 0,
|
|
118
|
-
trackIndex: 0,
|
|
119
|
-
cues: [{ start: 1, end: 2, text: "line" }],
|
|
120
|
-
language: "en",
|
|
121
|
-
detectedLanguage: null,
|
|
122
|
-
cursor: 1
|
|
123
|
-
});
|
|
124
|
-
return { old, trial, cuesOn, push };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
test("closing a trial connection keeps the viewer and their cues on the old one", () => {
|
|
128
|
-
// The old connection delivered again before the trial was adopted, so the
|
|
129
|
-
// page closes the trial. The viewer never left.
|
|
130
|
-
const gone = [];
|
|
131
|
-
const { old, trial, cuesOn, push } = trialBeside(gone);
|
|
132
|
-
trial.control.close();
|
|
133
|
-
trial.media.close();
|
|
134
|
-
assert.deepEqual(gone, [], "the viewer is still on the old connection");
|
|
135
|
-
push();
|
|
136
|
-
assert.equal(cuesOn(old.control), 1, "cues reach the channel the viewer named themselves on");
|
|
137
|
-
old.control.close();
|
|
138
|
-
old.media.close();
|
|
139
|
-
assert.deepEqual(gone, [{ consumerId: "viewer-1", reason: "the connection closed" }]);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("after a trial connection is adopted, cues go to it and the old one's close releases nobody", () => {
|
|
143
|
-
const gone = [];
|
|
144
|
-
const { old, trial, cuesOn, push } = trialBeside(gone);
|
|
145
|
-
push();
|
|
146
|
-
assert.equal(cuesOn(trial.control), 1, "the newest connection carrying the viewer is used");
|
|
147
|
-
assert.equal(cuesOn(old.control), 0);
|
|
148
|
-
old.control.close();
|
|
149
|
-
old.media.close();
|
|
150
|
-
assert.deepEqual(gone, []);
|
|
151
|
-
push();
|
|
152
|
-
assert.equal(cuesOn(trial.control), 2);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("a viewer whose newest channel closed is reached on the older connection", () => {
|
|
156
|
-
const gone = [];
|
|
157
|
-
const { old, trial, cuesOn, push } = trialBeside(gone);
|
|
158
|
-
trial.control.close();
|
|
159
|
-
push();
|
|
160
|
-
assert.equal(cuesOn(old.control), 1);
|
|
161
|
-
assert.deepEqual(gone, [], "the trial connection still has its media channel");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("a connection the transport no longer knows is not probed any more, though no channel said it closed", (t) => {
|
|
165
|
-
t.mock.timers.enable({ apis: ["setInterval", "setTimeout", "Date"] });
|
|
166
|
-
let alive = true;
|
|
167
|
-
const handler = createDataChannelHandler({
|
|
168
|
-
proxyPort: 9090,
|
|
169
|
-
getTransportSnapshot: () => (alive ? { bytesSent: 0, bytesReceived: 0 } : null),
|
|
170
|
-
onViewerGone: () => {}
|
|
171
|
-
});
|
|
172
|
-
const channels = ["proxy", "proxy-control", "proxy-fast"].map((label) => new FakeDataChannel(label));
|
|
173
|
-
for (const channel of channels) {
|
|
174
|
-
handler.handleChannel("trial-peer", channel);
|
|
175
|
-
}
|
|
176
|
-
const probes = () =>
|
|
177
|
-
channels.reduce((sum, channel) => sum + channel.messages.filter((message) => message.includes('"probe"')).length, 0);
|
|
178
|
-
t.mock.timers.tick(2_000);
|
|
179
|
-
assert.ok(probes() > 0, "an open connection is probed");
|
|
180
|
-
|
|
181
|
-
// Field 2026-10-04: the peer connection closed 70 ms after its channels
|
|
182
|
-
// opened, and none of them reported `onClosed`.
|
|
183
|
-
alive = false;
|
|
184
|
-
const advance = (ms) => {
|
|
185
|
-
// Second by second: the watcher compares the clock between its own ticks,
|
|
186
|
-
// and one long jump lands every tick on the same instant.
|
|
187
|
-
for (let elapsed = 0; elapsed < ms; elapsed += 1_000) t.mock.timers.tick(1_000);
|
|
188
|
-
};
|
|
189
|
-
advance(30_000);
|
|
190
|
-
const afterGone = probes();
|
|
191
|
-
advance(30_000);
|
|
192
|
-
assert.equal(probes(), afterGone, "nothing is sent on a connection that is gone");
|
|
193
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A torrent that was destroyed must not be handed to a reader.
|
|
3
|
-
*
|
|
4
|
-
* The worker remembers each source as a promise, and only ever forgot one when
|
|
5
|
-
* the ADD failed. But the pool destroys a torrent that has gone unread for a
|
|
6
|
-
* quarter of an hour, and under disk pressure — clearing its own map, not this
|
|
7
|
-
* one. The promise then went on resolving to a corpse: a destroyed torrent
|
|
8
|
-
* keeps its object and loses its files.
|
|
9
|
-
*
|
|
10
|
-
* What that did to a viewer, measured 2026-08-06 on two sessions in a row: the
|
|
11
|
-
* plan answered from cache in 23 ms, the session was created from cache in
|
|
12
|
-
* 2 ms, nothing waited for metadata because everything believed the torrent was
|
|
13
|
-
* known, and ffmpeg's first read died 130 ms in with `File 0 not found`. Every
|
|
14
|
-
* request for the playlist then answered 500 until the viewer gave up.
|
|
15
|
-
*
|
|
16
|
-
* The rule under test is the whole fix: a handle that cannot be read from is
|
|
17
|
-
* not returned, it is replaced.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { isUsableTorrentHandle } from "../services/torrent/worker/handle-state.js";
|
|
23
|
-
|
|
24
|
-
test("a live torrent is usable", () => {
|
|
25
|
-
assert.equal(isUsableTorrentHandle({ destroyed: false, files: [{ name: "a.mkv" }] }), true);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("a destroyed torrent is not, even while it still lists files", () => {
|
|
29
|
-
assert.equal(
|
|
30
|
-
isUsableTorrentHandle({ destroyed: true, files: [{ name: "a.mkv" }] }),
|
|
31
|
-
false,
|
|
32
|
-
"WebTorrent's own flag is the earliest signal that a handle is finished"
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("a torrent with no files is not — that is what the reader actually hits", () => {
|
|
37
|
-
assert.equal(
|
|
38
|
-
isUsableTorrentHandle({ destroyed: false, files: [] }),
|
|
39
|
-
false,
|
|
40
|
-
"an empty file list is what produced `File 0 not found` in the field"
|
|
41
|
-
);
|
|
42
|
-
assert.equal(
|
|
43
|
-
isUsableTorrentHandle({ destroyed: false }),
|
|
44
|
-
false,
|
|
45
|
-
"and a handle with no file list at all is the same case"
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("nothing at all is not usable", () => {
|
|
50
|
-
assert.equal(isUsableTorrentHandle(null), false);
|
|
51
|
-
assert.equal(isUsableTorrentHandle(undefined), false);
|
|
52
|
-
});
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the master playlist declares a variant carries.
|
|
3
|
-
*
|
|
4
|
-
* Measured, and the measurement was available all along: it used to be
|
|
5
|
-
* `height * height * 3.2`, justified by a comment saying no measurement existed
|
|
6
|
-
* before encoding starts. Field 2026-09-08: 3.73 Mbit/s declared for a file
|
|
7
|
-
* carrying 18.4, and the browser sizes its cushion in BYTES from that figure —
|
|
8
|
-
* 120 s asked bought 26 s of film, and the deepest it ever held was 17.1 s.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { declaredRates } from "../services/encode/output/rates.js";
|
|
14
|
-
import { bitrateFor } from "../services/encode/output/playlists.js";
|
|
15
|
-
|
|
16
|
-
// The field file: 2 806 246 976 bytes over 20:18, cut into 291 pieces.
|
|
17
|
-
const FIELD = { fileLength: 2_806_246_976, durationSeconds: 1218.73 };
|
|
18
|
-
|
|
19
|
-
test("the average is the file's own length over its duration", () => {
|
|
20
|
-
const { averageBitsPerSecond } = declaredRates(FIELD);
|
|
21
|
-
|
|
22
|
-
// 18.4 Mbit/s, against the 3.73 that was declared.
|
|
23
|
-
assert.ok(
|
|
24
|
-
Math.abs(averageBitsPerSecond / 1_000_000 - 18.42) < 0.01,
|
|
25
|
-
`got ${averageBitsPerSecond}`
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("nothing measured is stated as nothing, not as a guess", () => {
|
|
30
|
-
assert.deepEqual(
|
|
31
|
-
declaredRates({ fileLength: 0, durationSeconds: 0 }),
|
|
32
|
-
{ averageBitsPerSecond: 0, peakOverAverage: 1 }
|
|
33
|
-
);
|
|
34
|
-
assert.deepEqual(
|
|
35
|
-
declaredRates({ fileLength: 100, durationSeconds: 0 }),
|
|
36
|
-
{ averageBitsPerSecond: 0, peakOverAverage: 1 }
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("the peak comes from the biggest piece made and the span it covers", () => {
|
|
41
|
-
// The field peak: 38.4 MB over 4.2 s is 73 Mbit/s, four times the average.
|
|
42
|
-
const boundaries = Array.from({ length: 292 }, (_, index) => index * 4.2);
|
|
43
|
-
const { averageBitsPerSecond, peakOverAverage } = declaredRates({
|
|
44
|
-
...FIELD,
|
|
45
|
-
largest: { index: 2, size: 40_288_299 },
|
|
46
|
-
boundaries
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const peak = averageBitsPerSecond * peakOverAverage;
|
|
50
|
-
assert.ok(Math.abs(peak / 1_000_000 - 76.7) < 0.5, `got ${peak / 1_000_000}Mbit/s`);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("no piece made yet means the peak is not yet measured, so it equals the average", () => {
|
|
54
|
-
// A ratio invented meanwhile is exactly the fabrication this replaced.
|
|
55
|
-
assert.equal(declaredRates(FIELD).peakOverAverage, 1);
|
|
56
|
-
assert.equal(
|
|
57
|
-
declaredRates({ ...FIELD, largest: { index: -1, size: 0 }, boundaries: [0, 4] }).peakOverAverage,
|
|
58
|
-
1
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("the peak is never below the average", () => {
|
|
63
|
-
// A short session whose only piece happens to be a small one must not lower
|
|
64
|
-
// the figure the player sizes its cushion from.
|
|
65
|
-
const boundaries = Array.from({ length: 292 }, (_, index) => index * 4.2);
|
|
66
|
-
assert.equal(
|
|
67
|
-
declaredRates({ ...FIELD, largest: { index: 5, size: 1000 }, boundaries }).peakOverAverage,
|
|
68
|
-
1
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("a smaller picture is declared at the pixel share of the source's rate", () => {
|
|
73
|
-
const measured = 18_420_000;
|
|
74
|
-
|
|
75
|
-
assert.equal(
|
|
76
|
-
bitrateFor({ averageBitsPerSecond: measured, height: 1080, sourceHeight: 1080 }),
|
|
77
|
-
Math.round(measured),
|
|
78
|
-
"the source's own height carries the source's bits"
|
|
79
|
-
);
|
|
80
|
-
assert.equal(
|
|
81
|
-
bitrateFor({ averageBitsPerSecond: measured, height: 2160, sourceHeight: 1080 }),
|
|
82
|
-
Math.round(measured),
|
|
83
|
-
"and a height above it carries no more — there is nothing to upscale from"
|
|
84
|
-
);
|
|
85
|
-
assert.equal(
|
|
86
|
-
bitrateFor({ averageBitsPerSecond: measured, height: 540, sourceHeight: 1080 }),
|
|
87
|
-
Math.round(measured / 4),
|
|
88
|
-
"half the height is a quarter of the pixels"
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("a capped height is declared at its cap, which is exact", () => {
|
|
93
|
-
assert.equal(
|
|
94
|
-
bitrateFor({ averageBitsPerSecond: 18_420_000, height: 720, sourceHeight: 1080, capKbps: 2500 }),
|
|
95
|
-
2_500_000,
|
|
96
|
-
"what we impose is known, not estimated"
|
|
97
|
-
);
|
|
98
|
-
assert.equal(
|
|
99
|
-
bitrateFor({ averageBitsPerSecond: 1_000_000, height: 1080, sourceHeight: 1080, capKbps: 99_000 }),
|
|
100
|
-
1_000_000,
|
|
101
|
-
"and a cap above what the source carries takes nothing away from it"
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("the floor is a floor on what may be declared, not a belief about content", () => {
|
|
106
|
-
assert.equal(
|
|
107
|
-
bitrateFor({ averageBitsPerSecond: 0, height: 1080, sourceHeight: 1080 }),
|
|
108
|
-
400_000,
|
|
109
|
-
"a file whose rate is not known yet"
|
|
110
|
-
);
|
|
111
|
-
assert.equal(
|
|
112
|
-
bitrateFor({ averageBitsPerSecond: 18_420_000, height: 4, sourceHeight: 1080 }),
|
|
113
|
-
400_000,
|
|
114
|
-
"and a variant so small the arithmetic would go under it"
|
|
115
|
-
);
|
|
116
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the proxy promises the browser must be answerable.
|
|
3
|
-
*
|
|
4
|
-
* The session manager asks the planner's media-info cache which tracks the
|
|
5
|
-
* output will carry. That cache never held the codecs — it holds dimensions,
|
|
6
|
-
* duration, fps, start time and an HDR flag — so the read produced `undefined`
|
|
7
|
-
* for both and the declaration came out `{video: false, audio: false}` for
|
|
8
|
-
* EVERY session since the check existed. Measured 2026-08-11 in the field.
|
|
9
|
-
*
|
|
10
|
-
* Nothing caught it: the writer and the reader each looked correct alone, and
|
|
11
|
-
* no test ever compared the shape of what is stored with the shape of what is
|
|
12
|
-
* read. That comparison is this file's whole job.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
import { readFileSync } from "node:fs";
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
test("every field the declaration reads is a field the cache stores", () => {
|
|
23
|
-
// What `declaredTracks` reads off the cached media info.
|
|
24
|
-
const read = [...sessions.matchAll(/probed\?\.([A-Za-z]+)/g)].map((match) => match[1]);
|
|
25
|
-
assert.ok(read.length > 0, "the declaration must read something, or it declares nothing");
|
|
26
|
-
|
|
27
|
-
const stored = planner.slice(planner.indexOf("mediaInfoCache.set("));
|
|
28
|
-
for (const field of new Set(read)) {
|
|
29
|
-
assert.ok(
|
|
30
|
-
stored.includes(`${field}:`),
|
|
31
|
-
`the declaration reads "${field}" and the media-info cache never stores it — ` +
|
|
32
|
-
"the read yields undefined and the promise silently becomes false"
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("an empty container track list remains pending for late sidecar metadata", () => {
|
|
38
|
-
assert.match(
|
|
39
|
-
planner,
|
|
40
|
-
/complete: Array\.isArray\(tracks\) && tracks\.length > 0/,
|
|
41
|
-
"ContainerOrchestrator leaves empty track reads uncached while the header is unavailable"
|
|
42
|
-
);
|
|
43
|
-
});
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The decode-cost fit, and the case it exists to prevent.
|
|
3
|
-
*
|
|
4
|
-
* The failure being pinned is real and dated: on 2026-08-17 three clips for
|
|
5
|
-
* three unknowns returned `0.007542 × Mpx/s + 0.000000 × Mbit/s + 0.0000 s/s`,
|
|
6
|
-
* with the bitrate term and the constant exactly zero, and the prediction built
|
|
7
|
-
* on it was 1.8-2.2x optimistic. An exact system cannot notice that two of its
|
|
8
|
-
* points said the same thing; these tests hold the replacement to noticing.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
|
|
14
|
-
import { decodeCostOf, fitDecodeCost } from "../services/encode/decode-cost-fit.js";
|
|
15
|
-
|
|
16
|
-
const FPS = 24;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A clip's measurement, priced by a known truth so a fit can be checked against
|
|
20
|
-
* the answer it should recover.
|
|
21
|
-
*
|
|
22
|
-
* @param {number} width
|
|
23
|
-
* @param {number} height
|
|
24
|
-
* @param {number} megabitsPerSecond
|
|
25
|
-
* @param {{ pixel: number, bitrate: number, constant: number, noise?: number }} truth
|
|
26
|
-
* @returns {{ megapixelsPerSecond: number, megabitsPerSecond: number, costSecondsPerSecond: number }}
|
|
27
|
-
*/
|
|
28
|
-
function clip(width, height, megabitsPerSecond, truth) {
|
|
29
|
-
const megapixelsPerSecond = (width * height * FPS) / 1e6;
|
|
30
|
-
const cost =
|
|
31
|
-
truth.pixel * megapixelsPerSecond + truth.bitrate * megabitsPerSecond + truth.constant + (truth.noise ?? 0);
|
|
32
|
-
return { megapixelsPerSecond, megabitsPerSecond, costSecondsPerSecond: cost };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The set the clips were cut to: three sizes, two bitrates, varied independently. */
|
|
36
|
-
const SIZES = [
|
|
37
|
-
[1920, 1080],
|
|
38
|
-
[1280, 720],
|
|
39
|
-
[854, 480]
|
|
40
|
-
];
|
|
41
|
-
const BITRATES = [9.5, 1.1];
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {{ pixel: number, bitrate: number, constant: number }} truth
|
|
45
|
-
* @param {number[]} [noise] - Per-clip disturbance, in order.
|
|
46
|
-
* @returns {ReturnType<typeof clip>[]}
|
|
47
|
-
*/
|
|
48
|
-
function wellConditionedSet(truth, noise = []) {
|
|
49
|
-
const samples = [];
|
|
50
|
-
let index = 0;
|
|
51
|
-
for (const [width, height] of SIZES) {
|
|
52
|
-
for (const bitrate of BITRATES) {
|
|
53
|
-
samples.push(clip(width, height, bitrate, { ...truth, noise: noise[index] ?? 0 }));
|
|
54
|
-
index += 1;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return samples;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
test("a well-conditioned set recovers every term", () => {
|
|
61
|
-
const truth = { pixel: 0.0055, bitrate: 0.0099, constant: 0.057 };
|
|
62
|
-
const model = fitDecodeCost(wellConditionedSet(truth));
|
|
63
|
-
|
|
64
|
-
assert.ok(model, "six clips over three unknowns must produce a model");
|
|
65
|
-
assert.equal(model.shape, "pixels+bitrate+constant");
|
|
66
|
-
assert.deepEqual(model.dropped, []);
|
|
67
|
-
assert.ok(Math.abs(model.pixelTerm - truth.pixel) < 1e-6, `pixel term ${model.pixelTerm}`);
|
|
68
|
-
assert.ok(Math.abs(model.bitrateTerm - truth.bitrate) < 1e-6, `bitrate term ${model.bitrateTerm}`);
|
|
69
|
-
assert.ok(Math.abs(model.constantTerm - truth.constant) < 1e-6, `constant ${model.constantTerm}`);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("three clips are refused outright — an exact system cannot see its own degeneracy", () => {
|
|
73
|
-
// Exactly the shape that shipped: two clips at the same pixel rate differing
|
|
74
|
-
// only in bitrate, and a third at another size.
|
|
75
|
-
const truth = { pixel: 0.0055, bitrate: 0.0099, constant: 0.057 };
|
|
76
|
-
const three = [clip(1920, 1080, 11.4, truth), clip(1920, 1080, 0.97, truth), clip(1280, 720, 2.25, truth)];
|
|
77
|
-
|
|
78
|
-
assert.equal(
|
|
79
|
-
fitDecodeCost(three),
|
|
80
|
-
null,
|
|
81
|
-
"with no residual there is nothing to notice a degeneracy with, so no model may be published"
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("a term the measurements do not determine is dropped, and said so", () => {
|
|
86
|
-
// A host whose decoding does not depend on bitrate at all: the term is not
|
|
87
|
-
// small, it is absent. What must NOT happen is publishing a zero as though it
|
|
88
|
-
// had been measured.
|
|
89
|
-
const truth = { pixel: 0.0055, bitrate: 0, constant: 0.057 };
|
|
90
|
-
// Deliberately NOT alternating with the bitrate column: an earlier version
|
|
91
|
-
// of this test put positive noise on every high-bitrate clip and negative on
|
|
92
|
-
// every low one, which IS a bitrate signal — the fit found it, correctly, and
|
|
93
|
-
// the test was wrong.
|
|
94
|
-
const noise = [0.004, 0.003, -0.004, 0.002, -0.003, -0.002];
|
|
95
|
-
const model = fitDecodeCost(wellConditionedSet(truth, noise));
|
|
96
|
-
|
|
97
|
-
assert.ok(model);
|
|
98
|
-
assert.ok(model.dropped.includes("bitrate"), `dropped: ${model.dropped.join(",") || "nothing"}`);
|
|
99
|
-
assert.equal(model.bitrateTerm, 0);
|
|
100
|
-
assert.ok(model.shape.includes("pixels"));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("noise in the readings does not become a term", () => {
|
|
104
|
-
// Pure noise around a pixels-only truth, larger than any bitrate effect.
|
|
105
|
-
const truth = { pixel: 0.0055, bitrate: 0, constant: 0 };
|
|
106
|
-
const noise = [0.01, 0.009, -0.012, 0.011, -0.008, -0.01];
|
|
107
|
-
const model = fitDecodeCost(wellConditionedSet(truth, noise));
|
|
108
|
-
|
|
109
|
-
assert.ok(model);
|
|
110
|
-
assert.ok(model.pixelTerm > 0, "the one relationship every reading agrees on survives");
|
|
111
|
-
assert.ok(
|
|
112
|
-
model.dropped.length > 0,
|
|
113
|
-
"and the terms the noise could have supplied are reported as undetermined"
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("without a measurable dependence on the source there is no model", () => {
|
|
118
|
-
// Every clip costs the same regardless of size or bitrate: nothing here says
|
|
119
|
-
// what a bigger picture costs, and inventing it is worse than having none.
|
|
120
|
-
const flat = wellConditionedSet({ pixel: 0, bitrate: 0, constant: 0.3 });
|
|
121
|
-
assert.equal(fitDecodeCost(flat), null);
|
|
122
|
-
assert.equal(fitDecodeCost([]), null);
|
|
123
|
-
assert.equal(fitDecodeCost(null), null);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("readings that are not measurements are left out", () => {
|
|
127
|
-
const truth = { pixel: 0.0055, bitrate: 0.0099, constant: 0.057 };
|
|
128
|
-
const samples = [
|
|
129
|
-
...wellConditionedSet(truth),
|
|
130
|
-
{ megapixelsPerSecond: Number.NaN, megabitsPerSecond: 5, costSecondsPerSecond: 1 },
|
|
131
|
-
{ megapixelsPerSecond: 20, megabitsPerSecond: 5, costSecondsPerSecond: 0 }
|
|
132
|
-
];
|
|
133
|
-
const model = fitDecodeCost(samples);
|
|
134
|
-
assert.ok(model);
|
|
135
|
-
assert.equal(model.samples, 6);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("what a model prices a film at", () => {
|
|
139
|
-
const model = fitDecodeCost(wellConditionedSet({ pixel: 0.0055, bitrate: 0.0099, constant: 0.057 }));
|
|
140
|
-
// The field film: 1080p24 at about 8 Mbit/s.
|
|
141
|
-
const cost = decodeCostOf(model, { megapixelsPerSecond: 49.77, megabitsPerSecond: 8 });
|
|
142
|
-
assert.ok(cost > 0);
|
|
143
|
-
// Which is a decode speed of 1/cost — the figure the quality offer rests on.
|
|
144
|
-
assert.ok(1 / cost > 1 && 1 / cost < 10, `decodes at ${(1 / cost).toFixed(2)}x`);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* THE ORDERINGS, STATED WHERE THEY ARE DETERMINISTIC.
|
|
149
|
-
*
|
|
150
|
-
* What the quality offer needs of this model is not precision but ORDER: a
|
|
151
|
-
* bigger, richer, dearer-to-decode source must never be priced cheaper, because
|
|
152
|
-
* the model is asked about rungs nobody has decoded. That is a property of the
|
|
153
|
-
* fit, and it is a property of the fit whatever machine runs the test.
|
|
154
|
-
*
|
|
155
|
-
* These three used to be asserted over LIVE readings — decode two clips through
|
|
156
|
-
* real ffmpeg and require the smaller to come back faster. That measures the
|
|
157
|
-
* scheduler, not the code: four failures in one day, and the worst of them read
|
|
158
|
-
* 480p at 8.4x against 1080p at 20.8x, an inversion of two and a half times
|
|
159
|
-
* with nothing wrong. The readings themselves are unavailable to a test at all,
|
|
160
|
-
* since a clip that "said nothing" under load makes the whole benchmark answer
|
|
161
|
-
* with nothing.
|
|
162
|
-
*/
|
|
163
|
-
|
|
164
|
-
test("a bigger picture is priced dearer than a smaller one of the same bitrate", () => {
|
|
165
|
-
const model = fitDecodeCost(wellConditionedSet({ pixel: 0.0055, bitrate: 0.0099, constant: 0.057 }));
|
|
166
|
-
const big = decodeCostOf(model, { megapixelsPerSecond: (1920 * 1080 * FPS) / 1e6, megabitsPerSecond: 4 });
|
|
167
|
-
const small = decodeCostOf(model, { megapixelsPerSecond: (854 * 480 * FPS) / 1e6, megabitsPerSecond: 4 });
|
|
168
|
-
|
|
169
|
-
assert.ok(big > small, `1080p at ${big.toFixed(4)} s/s against 480p at ${small.toFixed(4)}`);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("a thicker stream is priced dearer than a thin one of the same size", () => {
|
|
173
|
-
const model = fitDecodeCost(wellConditionedSet({ pixel: 0.0055, bitrate: 0.0099, constant: 0.057 }));
|
|
174
|
-
const pixels = (854 * 480 * FPS) / 1e6;
|
|
175
|
-
const thick = decodeCostOf(model, { megapixelsPerSecond: pixels, megabitsPerSecond: 9.5 });
|
|
176
|
-
const thin = decodeCostOf(model, { megapixelsPerSecond: pixels, megabitsPerSecond: 1.1 });
|
|
177
|
-
|
|
178
|
-
assert.ok(thick > thin, `9.5 Mbit/s at ${thick.toFixed(4)} s/s against 1.1 at ${thin.toFixed(4)}`);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("a family measured to be dearer prices its own sources dearer", () => {
|
|
182
|
-
// Why the model is fitted per codec family at all: HEVC costs more than H.264
|
|
183
|
-
// for the same picture on the same machine, so a source that has to be
|
|
184
|
-
// re-encoded — which is usually one the browser could not decode — must be
|
|
185
|
-
// priced by its own family and not by H.264's.
|
|
186
|
-
const cheap = { pixel: 0.0055, bitrate: 0.0099, constant: 0.057 };
|
|
187
|
-
const dear = { pixel: 0.011, bitrate: 0.0198, constant: 0.114 };
|
|
188
|
-
const model = {
|
|
189
|
-
...fitDecodeCost(wellConditionedSet(cheap)),
|
|
190
|
-
families: {
|
|
191
|
-
h264: fitDecodeCost(wellConditionedSet(cheap)),
|
|
192
|
-
hevc: fitDecodeCost(wellConditionedSet(dear))
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
const source = { megapixelsPerSecond: (854 * 480 * FPS) / 1e6, megabitsPerSecond: 1.1 };
|
|
196
|
-
|
|
197
|
-
assert.ok(
|
|
198
|
-
decodeCostOf(model, { ...source, codec: "hevc" }) >
|
|
199
|
-
decodeCostOf(model, { ...source, codec: "h264" }),
|
|
200
|
-
"the family chooses the terms, and the dearer family answers dearer"
|
|
201
|
-
);
|
|
202
|
-
});
|