How encrypted video calls work
This page explains how end-to-end encrypted (E2EE) video calls work in iCallU. The code blocks are safe-to-publish examples showing coordination patterns and where encryption is applied. iCallU servers never see video/audio content or encryption keys.
1. Call setup (signaling only)
The Node.js server helps two users find each other and exchange connection information. This is called signaling. No media flows through the server.
// server/signaling.js
io.on('connection', (socket) => {
socket.on('call:invite', ({ toUserId }) => {
io.to(toUserId).emit('call:incoming', {
from: socket.userId
});
});
});
The server only relays metadata needed to establish the call.
2. Key agreement (peer-to-peer)
Each device generates encryption keys locally and exchanges public key material through the signaling channel.
// server/e2ee-relay.js
socket.on('e2ee:public-key', ({ to, publicKey }) => {
io.to(to).emit('e2ee:public-key', {
from: socket.userId,
publicKey
});
});
Private keys never leave the user's device.
3. Encrypted media transport (WebRTC)
WebRTC transports audio/video peer-to-peer. With E2EE enabled, frames are encrypted before leaving the device.
A) Basic WebRTC setup
// client/webrtc-basic.js
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
});
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(t => pc.addTrack(t, stream));
B) Minimal signaling flow
// client/signaling-demo.js
socket.on("rtc:signal", async (msg) => {
if (msg.type === "offer") {
await pc.setRemoteDescription(msg.sdp);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit("rtc:signal", { to: msg.from, type: "answer", sdp: pc.localDescription });
}
});
C) Insertable Streams (where E2EE hooks in)
// conceptual sender transform
const { readable, writable } = sender.createEncodedStreams();
readable
.pipeThrough(new TransformStream({
transform(frame, ctl) {
frame.data = encrypt(frame.data);
ctl.enqueue(frame);
}
}))
.pipeTo(writable);
Even TURN servers cannot decrypt media frames.
4. Call lifecycle (server-side)
// server/call-tracking.js
socket.on('call:started', ({ callId }) => {
calls.set(callId, { startedAt: Date.now() });
});
socket.on('call:ended', ({ callId }) => {
const c = calls.get(callId);
if (c) recordDuration(Date.now() - c.startedAt);
});
Duration can be tracked without inspecting content.
5. Ephemeral teardown
// client behavior
peerConnection.close();
encryptionKeys.zeroize();
Past calls cannot be decrypted - even by participants.