Files
securebit-chat/tests/webrtc-adaptation.test.mjs
T
lockbitchat b3fcf54670
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s
feat(webrtc): end-to-end encrypted voice & video calls with adaptive codecs
Add 1:1 voice and video calling over the existing SAS-verified peer
connection. Audio and video tracks ride the same RTCPeerConnection as the
chat, bundled onto one DTLS-SRTP transport, so media inherits the session's
end-to-end encryption. SDP offer/answer is renegotiated in-band over the
verified data channel — no signalling server, so the media's DTLS
fingerprints are authenticated end-to-end. Calls are gated on a connected,
SAS-verified session.

Codecs & adaptation:
- Opus tuned for lossy links (in-band FEC, DTX, RED redundancy); audio is
  bandwidth-prioritised and never throttled.
- VP9/AV1 single-encoding SVC with H.264/VP8 fallback; video degrades by
  spatial/temporal layer.
- Runtime NetworkAdaptationController trims video bitrate on loss/RTT and
  recovers as the link clears — no renegotiation. Live connection-quality
  indicator (Excellent/Good/Fair/Weak) in the call UI.

In-call controls: mute, camera on/off (voice→video upgrade in-band),
camera flip, minimize-to-widget, hang up, and accept/decline for incoming
calls. Production logging disabled (DEBUG_MODE=false); temporary call
diagnostic logger removed. Codec rationale in docs/webrtc-config.md.
2026-07-23 12:56:19 -04:00

82 lines
4.3 KiB
JavaScript

// Unit tests for the adaptive-bitrate metrics + decision logic.
// Run: node tests/webrtc-adaptation.test.mjs
import assert from 'node:assert';
import { summarizeStats, qualityFromMetrics } from '../src/network/webrtc/adaptation/metrics.js';
import { decideAdaptation } from '../src/network/webrtc/adaptation/controller.js';
import { ADAPTATION_CONFIG as CFG } from '../src/network/webrtc/config.js';
const stats = (packetsSent, packetsLost, rttSec, extra = {}) => ([
{ type: 'outbound-rtp', kind: 'video', packetsSent, qualityLimitationReason: extra.qlr || 'none' },
{ type: 'remote-inbound-rtp', kind: 'video', packetsLost, roundTripTime: rttSec, jitter: 0.01 },
{ type: 'candidate-pair', nominated: true, currentRoundTripTime: rttSec, availableOutgoingBitrate: 1000000 },
]);
function testMetrics() {
// First sample: no deltas → 0% loss.
const m1 = summarizeStats(stats(100, 0, 0.05));
assert.strictEqual(m1.counters.packetsSent, 100);
assert.strictEqual(m1.lossPct, 0, 'first sample has no loss');
assert.strictEqual(m1.rttMs, 50, 'rtt in ms');
assert.ok(m1.hasData);
// Second sample: 100 sent, 20 lost → 20/120 ≈ 16.7%.
const m2 = summarizeStats(stats(200, 20, 0.05), m1.counters);
assert.ok(Math.abs(m2.lossPct - 20 / 120) < 1e-9, `loss ~16.7% (got ${m2.lossPct})`);
// Quality thresholds.
assert.strictEqual(qualityFromMetrics({ hasData: true, lossPct: 0.01, rttMs: 100 }), 'excellent');
assert.strictEqual(qualityFromMetrics({ hasData: true, lossPct: 0.05, rttMs: 200 }), 'good');
assert.strictEqual(qualityFromMetrics({ hasData: true, lossPct: 0.12, rttMs: 350 }), 'fair');
assert.strictEqual(qualityFromMetrics({ hasData: true, lossPct: 0.30, rttMs: 500 }), 'poor');
assert.strictEqual(qualityFromMetrics({ hasData: false }), null, 'no data → null');
}
function testDecide() {
const base = { targetBitrate: 1500000, ceilingBitrate: 1500000, scaleResolutionDownBy: 1, goodTicks: 0 };
// High loss → step down 20%.
const d1 = decideAdaptation({ lossPct: 0.15, rttMs: 100, qualityLimitationReason: 'none' }, base, CFG);
assert.strictEqual(d1.targetBitrate, 1200000, 'loss backoff -20%');
assert.ok(d1.changed && d1.reason === 'backoff');
// High RTT → step down.
const d2 = decideAdaptation({ lossPct: 0, rttMs: 350, qualityLimitationReason: 'none' }, base, CFG);
assert.strictEqual(d2.targetBitrate, 1200000, 'rtt backoff -20%');
// Floor respected.
const low = { ...base, targetBitrate: CFG.minVideoBitrate };
const d3 = decideAdaptation({ lossPct: 0.5, rttMs: 500, qualityLimitationReason: 'none' }, low, CFG);
assert.strictEqual(d3.targetBitrate, CFG.minVideoBitrate, 'never below floor');
assert.strictEqual(d3.changed, false, 'no change at floor');
// CPU limitation → scale down resolution, bitrate untouched.
const d4 = decideAdaptation({ lossPct: 0, rttMs: 50, qualityLimitationReason: 'cpu' }, base, CFG);
assert.strictEqual(d4.targetBitrate, 1500000, 'cpu keeps bitrate');
assert.ok(d4.scaleResolutionDownBy > 1, 'cpu scales resolution down');
// Recovery: needs recoverStableTicks good ticks, then ramps +10%.
let st = { ...base, targetBitrate: 1000000 };
const good = { lossPct: 0.01, rttMs: 100, qualityLimitationReason: 'none' };
for (let i = 1; i < CFG.recoverStableTicks; i++) {
st = { ...st, ...decideAdaptation(good, st, CFG) };
assert.strictEqual(st.targetBitrate, 1000000, `no ramp before ${CFG.recoverStableTicks} ticks (tick ${i})`);
}
const dR = decideAdaptation(good, st, CFG);
assert.strictEqual(dR.targetBitrate, 1100000, 'ramp +10% after stable ticks');
assert.ok(dR.changed && dR.reason === 'rampup');
// Ceiling respected.
const atCeil = { ...base, targetBitrate: 1500000, goodTicks: CFG.recoverStableTicks - 1 };
const dC = decideAdaptation(good, atCeil, CFG);
assert.strictEqual(dC.targetBitrate, 1500000, 'never above ceiling');
// Neutral zone → hold, reset goodTicks.
const dN = decideAdaptation({ lossPct: 0.05, rttMs: 200, qualityLimitationReason: 'none' }, { ...base, goodTicks: 3 }, CFG);
assert.strictEqual(dN.changed, false);
assert.strictEqual(dN.goodTicks, 0, 'neutral resets goodTicks');
}
testMetrics();
testDecide();
console.log('webrtc-adaptation.test.mjs: all assertions passed');