Files
securebit-chat/doc/CALLS.md
T
lockbitchat 3212138a0d
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
docs: reorganise documentation; derive header version from package.json; release v5.7.2
No protocol or message-protection changes.

The version in the application header was a literal and had fallen behind,
showing v5.6.0 while running 5.7.1. It now comes from package.json, and a test
fails if a hard-coded one reappears or if meta.json, the README badge, the
changelog and the docs disagree about the release.

Documentation reorganised so that everything technical lives in doc/ with an
index, and the root keeps only what belongs there by convention: README,
SECURITY, CHANGELOG and LICENSE.

- SECURITY.md rewritten. It listed a supported release line three major versions
  out of date and made claims the software does not make. It now states what is
  guaranteed, what is not, and how to report a problem.
- SECURITY_DISCLAIMER.md and RESPONSIBLE_USE.md merged into doc/USE-POLICY.md,
  which says what the software cannot protect against rather than listing
  generic advice.
- doc/SECURITY-ARCHITECTURE.md renamed to doc/ARCHITECTURE.md and rewritten
  around the session lifecycle, what verification gates, and how recovery works.
- doc/CRYPTOGRAPHY.md rewritten: key schedule, the Double Ratchet, framing, and
  memory handling, with values taken from the source rather than restated.
- doc/CONFIGURATION.md rewritten with the real file-type policy, ICE and TURN
  guidance, and the deployment caching rules that matter.
- docs/webrtc-config.md moved to doc/CALLS.md and rewritten; the obsolete
  docs/webrtc-audit.md, a working document full of stale line numbers, removed
  along with the docs/ directory.
- doc/CONTRIBUTING.md records what the recent regressions taught us about
  writing tests that can actually fail.
- doc/README.md added as an index.

Internal security review notes are excluded from the repository via .gitignore.
Those describe attack paths against specific releases in enough detail to
reproduce them, which is useful privately and harmful in public while users are
still updating.
2026-08-05 23:54:50 -04:00

7.5 KiB

Voice and video calls

Calls run over the same peer connection as the chat. Media is added to the existing connection and renegotiated onto it, and the call SDP is exchanged over the encrypted data channel rather than through any signalling service. Media therefore inherits the session's verification: the DTLS-SRTP fingerprints negotiated for the media were themselves carried over an authenticated channel.

All tunable values live in src/network/webrtc/config.js. This document explains where they come from, because the numbers are otherwise indistinguishable from arbitrary choices.

Where the settings attach

A single RTCRtpSender cannot express codec ordering or fmtp parameters, so configuring a sender is spread across three WebRTC surfaces, each at the point in the lifecycle where it works:

Concern Surface When Implementation
Codec ordering (RED before Opus) transceiver.setCodecPreferences before creating the offer or answer applyAudioCodecPreferences in audio.js
Opus FEC, DTX, bitrate SDP a=fmtp rewriting after create, before setLocalDescription applyOpusSettings in sdp.js
Priority and maximum bitrate sender.setParameters after setLocalDescription configureAudioSender in audio.js

Both peers apply the same rewriting, so the negotiated session carries the parameters regardless of who called.

Audio

Speech has to stay intelligible on a bad link, and that goal drives every value below.

Opus parameters

Parameter Value Reason
minptime 10 Smaller packetisation interval, lower latency (RFC 7587, section 7)
useinbandfec 1 In-band forward error correction reconstructs a lost packet from the next one. This is the main lever for staying intelligible at 15 to 20 percent loss (RFC 6716, section 2.1.7)
usedtx 1 Discontinuous transmission stops sending during silence, leaving the transport free for video and FEC (RFC 7587, section 3.1.3)
stereo 0 Mono halves the bitrate with no loss for speech
maxaveragebitrate 32000 Comfortable wideband speech
cbr 0 Variable bitrate spends bits only when needed

Redundancy

RED (RFC 2198) carries the previous frame's payload alongside each packet, so isolated losses recover without retransmission. It is enabled only when the browser advertises audio/red in RTCRtpSender.getCapabilities('audio'), which Chromium does and others vary on; when absent it is skipped silently. RED is ordered before Opus in codec preferences.

Sender parameters

Parameter Value Reason
maxBitrate 40000 bps Headroom above the 32 kbps Opus target for RED redundancy
priority high Audio wins bandwidth arbitration against video within the connection
networkPriority high DSCP hint so audio is prioritised on the wire

Audio is never throttled by the adaptation controller. Under sustained loss the video degrades and speech continues.

Video

Codec preference order is VP9, AV1, H.264, VP8, applied through setCodecPreferences. Retransmission and FEC codecs are kept after the media codecs so they still function. VP9 and AV1 provide scalable coding; H.264 and VP8 do not.

Scalable coding rather than simulcast

This is a one-to-one connection with a single receiver, so one encoding with SVC is the right tool: a single stream that degrades by spatial or temporal layer. It is applied through sender.setParameters and needs no addTransceiver or rid configuration, which keeps it away from the media path that actually works.

Codec scalabilityMode maxBitrate degradationPreference
VP9 L3T3_KEY (3 spatial, 3 temporal, key-aligned) 1.5 Mbps balanced
AV1 L1T3 1.2 Mbps maintain-framerate
H.264, VP8 none 1.5 Mbps balanced

networkPriority is medium, below audio. If a browser rejects the scalability mode, which Firefox and Safari do in places, configureVideoSender retries with a plain encoding.

Why media is attached with addTrack

An explicit addTransceiver({ sendEncodings }) path was built and then removed, because it broke media on real devices in two ways. On the answering side, reusing the transceiver created by setRemoteDescription rejected the SVC parameters outright. On repeat and role-reversed calls, the reused transceiver directions desynchronised: the call connected and ontrack fired, but no media flowed.

What ships instead attaches media with addTrack, reused across calls through replaceTrack, and lets the browser manage transceiver direction. This is what keeps audio and video flowing across reversed and repeated calls.

Multi-rid simulcast primitives (buildVideoSendEncodings) are kept and tested for a future group-call path, but are not wired into the one-to-one flow. They need addTransceiver({ sendEncodings }), which requires the problems above to be solved first, ideally with a two-connection browser test rig that is not set up. The adaptation controller is already simulcast-aware for when that lands.

Transport feedback

The call m-lines need RTCP feedback and header extensions present. Most browsers emit them already, so this is an idempotent safety net:

Media RTCP feedback Header extension
Video transport-cc, nack, nack pli, ccm fir, goog-remb transport-wide congestion control
Audio transport-cc, nack transport-wide congestion control

These are added only when missing, never duplicated, and applied only to primary codecs. Transport-wide congestion control is what feeds the bandwidth estimator that adaptation reads.

Rewritten local SDP is applied with progressive fallback: full rewrite, then Opus only, then raw. A browser that rejects an injected line cannot break the call.

Adaptation

NetworkAdaptationController samples pc.getStats() every second and reacts:

Condition Action
Loss above 10 percent, or round trip above 300 ms Reduce video maxBitrate by 20 percent, floor 100 kbps
Loss below 3 percent and round trip below 150 ms, sustained 5 samples Raise video maxBitrate by 10 percent, up to the ceiling
qualityLimitationReason is cpu Scale resolution down by 1.5, bitrate unchanged

Every change goes through sender.setParameters. There is no renegotiation and no track restart, so adaptation is invisible to the call. Audio is never touched.

The decision function and the stats parsing are pure and unit-tested against recorded getStats output.

Quality indicator

The same sample produces a coarse label shown in the call interface:

Label Condition
Excellent Loss below 3 percent and round trip below 150 ms
Good Loss below 7 percent and round trip below 250 ms
Fair Loss below 15 percent and round trip below 400 ms
Weak Anything else

It appears in the voice overlay, the video top bar, and as compact bars in the minimized widget, and stays hidden until the first sample has data.

Verifying a change

Unit tests cover SDP rewriting, video codec selection and the adaptation decision:

npm test

For anything touching media in practice, open chrome://webrtc-internals during a call and check that:

  • outbound audio shows Opus with the fmtp parameters above, appearing as red and opus on Chromium
  • throttling the link steps outbound video targetBitrate down within a second or two and recovers when the link clears
  • audio bitrate holds steady while video adapts
  • the in-call indicator moves through Fair and Weak as the link degrades