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.
118 lines
5.2 KiB
JavaScript
118 lines
5.2 KiB
JavaScript
// Unit tests for the pure SDP munging utilities (src/network/webrtc/sdp.js).
|
|
// Run: node tests/webrtc-sdp.test.mjs
|
|
import assert from 'node:assert';
|
|
import {
|
|
splitSdp, joinSdp, sectionKind, findPayloadTypes, upsertFmtp, applyOpusSettings,
|
|
getCodecPayloadTypes, ensureRtcpFb, ensureExtmap, applyTransport,
|
|
} from '../src/network/webrtc/sdp.js';
|
|
import { AUDIO_CONFIG, TRANSPORT_CONFIG } from '../src/network/webrtc/config.js';
|
|
|
|
const CRLF = '\r\n';
|
|
// Synthetic SDP: one audio m-line (opus 111 + telephone-event 126, opus already
|
|
// has a partial fmtp) and one video m-line (VP9).
|
|
const SDP = [
|
|
'v=0',
|
|
'o=- 1 2 IN IP4 127.0.0.1',
|
|
's=-',
|
|
't=0 0',
|
|
'a=group:BUNDLE 0 1',
|
|
'm=audio 9 UDP/TLS/RTP/SAVPF 111 126',
|
|
'c=IN IP4 0.0.0.0',
|
|
'a=rtpmap:111 opus/48000/2',
|
|
'a=fmtp:111 minptime=10;useinbandfec=1',
|
|
'a=rtpmap:126 telephone-event/8000',
|
|
'a=mid:0',
|
|
'm=video 9 UDP/TLS/RTP/SAVPF 96',
|
|
'c=IN IP4 0.0.0.0',
|
|
'a=rtpmap:96 VP9/90000',
|
|
'a=mid:1',
|
|
].join(CRLF) + CRLF;
|
|
|
|
function run() {
|
|
// splitSdp / kinds
|
|
const parsed = splitSdp(SDP);
|
|
assert.strictEqual(parsed.eol, CRLF, 'detects CRLF');
|
|
assert.strictEqual(parsed.media.length, 2, 'two m-sections');
|
|
assert.strictEqual(sectionKind(parsed.media[0]), 'audio');
|
|
assert.strictEqual(sectionKind(parsed.media[1]), 'video');
|
|
|
|
// findPayloadTypes
|
|
assert.deepStrictEqual(findPayloadTypes(parsed.media[0], 'opus'), ['111']);
|
|
assert.deepStrictEqual(findPayloadTypes(parsed.media[1], 'VP9'), ['96']);
|
|
|
|
// applyOpusSettings: all requested params present, existing ones merged (not dropped).
|
|
const munged = applyOpusSettings(SDP, AUDIO_CONFIG.opusFmtp);
|
|
const fmtp = munged.split(/\r\n/).find(l => l.startsWith('a=fmtp:111 '));
|
|
assert.ok(fmtp, 'opus fmtp line exists');
|
|
for (const [k, v] of Object.entries(AUDIO_CONFIG.opusFmtp)) {
|
|
assert.ok(fmtp.includes(`${k}=${v}`), `fmtp has ${k}=${v} (got: ${fmtp})`);
|
|
}
|
|
assert.ok(fmtp.includes('minptime=10'), 'pre-existing param retained');
|
|
|
|
// No duplicate fmtp keys.
|
|
const params = fmtp.slice('a=fmtp:111 '.length).split(';').map(s => s.split('=')[0]);
|
|
assert.strictEqual(new Set(params).size, params.length, 'no duplicate fmtp keys');
|
|
|
|
// Idempotent: munging twice equals munging once.
|
|
assert.strictEqual(applyOpusSettings(munged, AUDIO_CONFIG.opusFmtp), munged, 'idempotent');
|
|
|
|
// Line endings preserved.
|
|
assert.ok(munged.includes(CRLF), 'CRLF preserved');
|
|
|
|
// Video section untouched (still exactly one line for VP9, no fmtp injected).
|
|
assert.ok(!munged.includes('a=fmtp:96'), 'video section not modified');
|
|
|
|
// No-Opus SDP returned unchanged (identity).
|
|
const noOpus = 'v=0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\na=mid:0\r\n';
|
|
assert.strictEqual(applyOpusSettings(noOpus, AUDIO_CONFIG.opusFmtp), noOpus, 'no-opus unchanged');
|
|
|
|
// upsertFmtp creates a line when none exists.
|
|
const p2 = splitSdp('m=audio 9 x 111\r\na=rtpmap:111 opus/48000/2\r\n');
|
|
upsertFmtp(p2.media[0], '111', { usedtx: 1 });
|
|
assert.ok(joinSdp(p2).includes('a=fmtp:111 usedtx=1'), 'fmtp created when missing');
|
|
|
|
// joinSdp round-trips an unmodified SDP exactly (trailing CRLF preserved).
|
|
assert.strictEqual(joinSdp(splitSdp(SDP)), SDP, 'round-trip preserves SDP exactly');
|
|
|
|
// ── Transport feedback (Step 4) ──────────────────────────────────────────
|
|
// Video section with a real codec (96) + rtx (97). Only 96 is a primary codec.
|
|
const vsdp = [
|
|
'v=0', 't=0 0',
|
|
'm=video 9 UDP/TLS/RTP/SAVPF 96 97',
|
|
'a=rtpmap:96 VP9/90000',
|
|
'a=rtpmap:97 rtx/90000',
|
|
'a=fmtp:97 apt=96',
|
|
'a=mid:0',
|
|
'',
|
|
].join(CRLF);
|
|
const vparsed = splitSdp(vsdp);
|
|
assert.deepStrictEqual(getCodecPayloadTypes(vparsed.media[0]), ['96'], 'rtx excluded from codec PTs');
|
|
|
|
const t = applyTransport(vsdp, TRANSPORT_CONFIG);
|
|
for (const fb of TRANSPORT_CONFIG.video.rtcpFb) {
|
|
assert.ok(t.includes(`a=rtcp-fb:96 ${fb}`), `video has rtcp-fb 96 ${fb}`);
|
|
}
|
|
assert.ok(!t.includes('a=rtcp-fb:97'), 'no rtcp-fb on rtx');
|
|
assert.ok(t.includes(`a=extmap:1 ${TRANSPORT_CONFIG.twccUri}`), 'TWCC extension added (id 1)');
|
|
|
|
// Idempotent: applying twice adds nothing new.
|
|
assert.strictEqual(applyTransport(t, TRANSPORT_CONFIG), t, 'applyTransport idempotent');
|
|
const fbCount = (t.match(/a=rtcp-fb:96 transport-cc/g) || []).length;
|
|
assert.strictEqual(fbCount, 1, 'no duplicate rtcp-fb line');
|
|
|
|
// ensureExtmap allocates the next free id, not a colliding one.
|
|
const ep = splitSdp('m=video 9 x 96\r\na=rtpmap:96 VP9/90000\r\na=extmap:3 urn:foo\r\na=mid:0\r\n');
|
|
ensureExtmap(ep.media[0], 'urn:bar');
|
|
assert.ok(joinSdp(ep).includes('a=extmap:4 urn:bar'), 'next free extmap id used');
|
|
|
|
// Audio gets the subset (transport-cc + nack), no pli/fir/remb.
|
|
const at = applyTransport(SDP, TRANSPORT_CONFIG);
|
|
assert.ok(at.includes('a=rtcp-fb:111 transport-cc'), 'audio transport-cc');
|
|
assert.ok(at.includes('a=rtcp-fb:111 nack'), 'audio nack');
|
|
assert.ok(!at.includes('a=rtcp-fb:111 nack pli'), 'audio has no pli');
|
|
|
|
console.log('webrtc-sdp.test.mjs: all assertions passed');
|
|
}
|
|
|
|
run();
|