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.
This commit is contained in:
@@ -1,143 +0,0 @@
|
||||
# WebRTC Audit — SecureBit.chat call stack
|
||||
|
||||
**Step 1 deliverable. No code changed.** This maps the existing WebRTC/call code so we can plan the adaptive voice/video stack (Opus FEC/DTX/RED, VP9-SVC + fallbacks, TWCC/NACK/PLI, getStats adaptation) *without* creating a parallel branch.
|
||||
|
||||
---
|
||||
|
||||
## 0. Reality vs. the task spec (read this first)
|
||||
|
||||
The task is written against a modular TypeScript layout (`src/webrtc/codecs/*.ts`, `call.ts`, `config.ts`, …). **That layout does not exist and does not match the repo.** Concretely:
|
||||
|
||||
| Task assumption | Actual repo |
|
||||
|---|---|
|
||||
| TypeScript (`.ts`) | Plain JS / JSX. No `tsconfig`, no `.ts` files, esbuild bundles JS as-is. |
|
||||
| `src/webrtc/` modular stack | One monolith: `src/network/EnhancedSecureWebRTCManager.js` (~14.8k lines). |
|
||||
| Fresh `RTCPeerConnection` per call, `addTransceiver` at init | **One long-lived PC** shared with the encrypted **data channel**; calls are **renegotiated onto it**. Media is added with `addTrack`, **never** `addTransceiver`. |
|
||||
| Standard signalling server / offer at init | **Two separate SDP paths** (see §5). Call SDP is exchanged **in-band over the E2E data channel**. |
|
||||
| Playwright available | Not installed. Tests are plain `node tests/*.test.mjs`. No `RTCPeerConnection` in Node. |
|
||||
| `debug('webrtc:adapt')` | No `debug` dependency. Logging is `_secureLog(...)` + the new `window.sbCallLog(...)`. |
|
||||
|
||||
**Consequence:** we integrate into the existing manager + a small set of **new plain-JS helper modules** it imports. We do **not** add TypeScript or a `src/webrtc/` TS tree. Proposed JS layout in §7.
|
||||
|
||||
---
|
||||
|
||||
## 1. Module map
|
||||
|
||||
| File | Kind | Exports / globals | Role |
|
||||
|---|---|---|---|
|
||||
| `src/network/EnhancedSecureWebRTCManager.js` | ES module | `export { EnhancedSecureWebRTCManager, SecureMasterKeyManager, SecureIndexedDBWrapper, SecurePersistentKeyStorage }` (line 14812). Also `window.EnhancedSecureWebRTCManager`. | **All** transport + crypto + call logic. The class to extend. |
|
||||
| `src/components/ui/CallUI.jsx` | ES module (side-effect) | `window.CallUIComponent`, `export { CallUIComponent }` | Presentational call overlay (voice/video/minimized/incoming). Pure UI; no media/crypto. |
|
||||
| `src/scripts/app-boot.js` | bundled entry | imports the manager + UI components; sets `window.*` | Real loader (→ `dist/app-boot.js`). CallUI is imported here. |
|
||||
| `src/app.jsx` | bundled entry | React app (`h`/createElement) | Header call buttons + mounts `CallUI`. getUserMedia for **voice messages** (unrelated to calls) at 816–817. |
|
||||
| `config/ice-servers*.js` | script | `window.SECUREBIT_ICE_SERVERS` | Operator STUN/TURN override. |
|
||||
| `src/network/iceServers.js`, `iceSettingsStore.js` | ES modules | ICE validation/persistence | User-supplied ICE servers. |
|
||||
|
||||
> `src/scripts/bootstrap-modules.js` exists but is **dead** (not referenced anywhere). Ignore it.
|
||||
|
||||
---
|
||||
|
||||
## 2. RTCPeerConnection: creation & config
|
||||
|
||||
- **`createPeerConnection()`** — `EnhancedSecureWebRTCManager.js:7731`. Single `new RTCPeerConnection(config)` at **7737**. Wires `onconnectionstatechange`, `oniceconnectionstatechange`, `onicecandidateerror`, **`ontrack`** (7802, call media), `ondatachannel`.
|
||||
- Called from **9983** (initiator/offer path) and **10646** (responder/answer path).
|
||||
- **`_buildPeerConnectionConfig()`** — `7661`. Returns:
|
||||
```js
|
||||
{ iceServers, iceCandidatePoolSize: 10, bundlePolicy: 'balanced' } // + iceTransportPolicy:'relay' in privacy mode
|
||||
```
|
||||
No `rtcpMuxPolicy`, no codec/degradation config (nothing to do there; those live on senders/transceivers).
|
||||
- One PC per session; multi-session keeps a `Map<id, manager>` (each with its own PC).
|
||||
|
||||
## 3. SDP formation
|
||||
|
||||
No codec munging exists today — only **DTLS fingerprint extraction** (`_extractDTLSFingerprintFromSDP`) and **ICE candidate diagnostics** (`_summarizeIceCandidatesInSDP` 1057, `_logIceCandidateDiagnostics` 1119, `_warnIfRemoteCandidatesNeedRelay` 1145).
|
||||
|
||||
| Op | Line(s) | Path |
|
||||
|---|---|---|
|
||||
| `createOffer` | 10004 | **Data-channel handshake** (initial connection) |
|
||||
| `setLocalDescription(offer)` | 10009 | handshake |
|
||||
| `setRemoteDescription(offer)` | 10682 | handshake |
|
||||
| `createAnswer` | 10717 | handshake |
|
||||
| `setLocalDescription(answer)` | 10727 | handshake |
|
||||
| `setRemoteDescription(answer)` | 11462 | handshake |
|
||||
| `createOffer` | 13047, 13262 | **Call** (startCall / renegotiate) |
|
||||
| `setLocalDescription` | 13048, 13263 | call |
|
||||
| `setRemoteDescription(offer)` | 13081 | call |
|
||||
| `createAnswer` + `setLocalDescription` | 13087–13088 | call |
|
||||
| `setRemoteDescription(answer)` | 13285 | call |
|
||||
| `setLocalDescription({type:'rollback'})` | 13176 | call teardown |
|
||||
|
||||
**Where codec/RTP munging must hook:** only the **call** offers/answers (13047/13087/13262). The handshake SDP (m=application/data channel only) must stay untouched.
|
||||
|
||||
## 4. Media: getUserMedia / addTrack / transceivers
|
||||
|
||||
- **getUserMedia (calls):** `_acquireLocalMedia` 13012, `upgradeToVideo` 13221, `switchCamera` 13245.
|
||||
- Unrelated: voice-message recorder `app.jsx:817`; QR scanner `QRScanner.js:72`.
|
||||
- **addTrack:** `_acquireLocalMedia` 13017, `upgradeToVideo` 13230. **← the only way media is attached today.**
|
||||
- **replaceTrack:** upgrade 13228, switchCamera 13252, teardown 13151.
|
||||
- **removeTrack / getReceivers / getTransceivers / .stop():** teardown 13146–13158.
|
||||
- **`addTransceiver`: NONE.** `sendEncodings`: NONE. `setParameters`/`getParameters`: NONE. `setCodecPreferences`/`getCapabilities`: NONE.
|
||||
- **`ontrack`** handler: 7802 (accumulates into `this.remoteMediaStream`).
|
||||
|
||||
## 5. Signalling model (two SDP paths — important)
|
||||
|
||||
1. **Connection handshake** (data channel): offer/answer compressed and exchanged **out-of-band** (QR / copy-paste), authenticated by **SAS**. Establishes the DTLS transport + `securechat` data channel (`createDataChannel` 9986).
|
||||
2. **Call setup** (media): after the channel is verified, call SDP rides **in-band over that data channel** via `MESSAGE_TYPES.CALL_OFFER/CALL_ANSWER/CALL_ICE/CALL_DECLINE/CALL_END`. Routed in the live `dataChannel.onmessage` (~7996) → `_handleCallSignal` (13274). Media BUNDLEs onto the existing transport, so **no new ICE**.
|
||||
|
||||
→ Adaptive stack changes affect **path 2 only**.
|
||||
|
||||
## 6. Call subsystem inventory (all in the manager, added recently)
|
||||
|
||||
State/getters: `callState` object; `getCallState` 12957, `getRemoteMediaStream` 12961, `getLocalMediaStream` 12965, `_updateCallState` 12969 (fires `onCallStateChanged` + `securebit-call-state` DOM event).
|
||||
Lifecycle: `_callCanStart` 12983 (gated on connected+verified), `_acquireLocalMedia` 12999, `startCall` 13025, `_onIncomingCallOffer` 13065, `_answerCallOffer` 13080, `acceptCall`/`declineCall`, `endCall` 13120, `_teardownCallMedia` 13132.
|
||||
Controls: `setMicEnabled` 13186, `toggleMic` 13193, `setCameraEnabled` 13197, `toggleCamera` 13215, `upgradeToVideo` 13218, `switchCamera` 13241, `_renegotiateCall` 13258.
|
||||
Signalling: `_handleCallSignal` 13274, `_sendCallSignal`, `MESSAGE_TYPES.CALL_*`.
|
||||
Debug: `window.sbCallLog` / `window.__sbCallLog` (module top of the manager).
|
||||
UI: `CallUI.jsx` + header buttons (`app.jsx`) + overlay mount in `EnhancedChatInterface`.
|
||||
|
||||
## 7. Gaps vs. the target spec
|
||||
|
||||
| Requirement | Status | Note |
|
||||
|---|---|---|
|
||||
| Opus fmtp (FEC/DTX/RED, maxavgbitrate, cbr=0) | ❌ | No SDP munging. |
|
||||
| RED via setCodecPreferences | ❌ | No codec prefs. |
|
||||
| Audio sender priority/networkPriority/maxBitrate | ❌ | No setParameters. |
|
||||
| VP9 SVC (`L3T3_KEY`) + simulcast fallback | ❌ | No `sendEncodings`; uses `addTrack`. |
|
||||
| AV1 / H.264 / VP8 fallback order | ❌ | No `getCapabilities` sort. |
|
||||
| TWCC/NACK/PLI/FIR/REMB on m-lines | ⚠️ | Whatever the browser emits by default; not enforced/verified. |
|
||||
| `NetworkAdaptationController` (getStats loop) | ❌ | None. |
|
||||
| Config with sourced constants | ❌ | Only `_config.webrtc` (ICE/privacy). |
|
||||
|
||||
## 8. Proposed integration (for confirmation — not yet built)
|
||||
|
||||
New **plain-JS** modules under `src/network/webrtc/` (imported by the manager), mapping to the task's requested files:
|
||||
|
||||
| Task file | Proposed actual file |
|
||||
|---|---|
|
||||
| `src/webrtc/config.ts` | `src/network/webrtc/config.js` — all constants + source comments |
|
||||
| `src/webrtc/codecs/audio.ts` | `src/network/webrtc/audio.js` — `configureAudioSender` |
|
||||
| `src/webrtc/codecs/video.ts` | `src/network/webrtc/video.js` — `configureVideoSender` |
|
||||
| `src/webrtc/codecs/sdp.ts` | `src/network/webrtc/sdp.js` — pure SDP munging utils (testable in Node) |
|
||||
| `src/webrtc/adaptation/metrics.ts` | `src/network/webrtc/adaptation/metrics.js` — getStats parsing |
|
||||
| `src/webrtc/adaptation/controller.ts` | `src/network/webrtc/adaptation/controller.js` — `NetworkAdaptationController` |
|
||||
| `src/webrtc/call.ts` | **integrate into `EnhancedSecureWebRTCManager.js`** call methods |
|
||||
|
||||
Hook points in the manager:
|
||||
- `_acquireLocalMedia` (12999): switch `addTrack` → `addTransceiver('audio'/'video', {direction, sendEncodings})`, then `setCodecPreferences` on the transceivers.
|
||||
- `startCall`/`_answerCallOffer`/`_renegotiateCall`: after `createOffer/createAnswer`, run `sdp.js` munging before `setLocalDescription`; after `setRemoteDescription`, call `configureAudioSender`/`configureVideoSender` and start the controller.
|
||||
- `_teardownCallMedia`: stop the controller.
|
||||
|
||||
**Key compromise to confirm:** the PC is long-lived and shared with the data channel, so we **cannot** follow Step-6's "addTransceiver at PC init" literally. Transceivers are added/negotiated in-band at call start on the existing PC (the audio/video transceivers can be created once on first call and reused via `replaceTrack` thereafter — this also fixes the transceiver-accumulation issue seen earlier).
|
||||
|
||||
## 9. Constraints / risks
|
||||
|
||||
- **Browser matrix:** `scalabilityMode`/`setCodecPreferences` support varies. Firefox: SVC limited → simulcast fallback mandatory. Safari: VP9 often absent → H.264 fallback; RED support varies. All feature-detected via `getCapabilities`/try-catch.
|
||||
- **SDP munging is fragile** across browsers (m-line/payload ordering). Utilities must be defensive and idempotent (spec says "no duplicate lines").
|
||||
- **Testing:** SDP utils + controller (mock `getStats`) → plain `node .mjs` unit tests (fits current infra). **Integration test needs a real browser** (two `RTCPeerConnection`s + throttling) — **Playwright is not installed**; that item needs either adding Playwright (a dependency — task says avoid unless necessary) or manual `chrome://webrtc-internals` verification.
|
||||
- **In-band renegotiation** must keep the perfect-negotiation guards already in place to avoid glare.
|
||||
|
||||
## 10. Open questions before Step 2
|
||||
|
||||
1. Confirm JS (not TS) + `src/network/webrtc/` layout in §8.
|
||||
2. Confirm the long-lived-PC compromise in §8 (reuse transceivers via `replaceTrack`, negotiate in-band).
|
||||
3. Playwright: add it for the integration test, or rely on `chrome://webrtc-internals` manual verification for the "adaptation visible under throttling" criterion?
|
||||
4. Commit-per-step + deploy to Fly after each step (as before), GitHub untouched — same as current workflow?
|
||||
@@ -1,160 +0,0 @@
|
||||
# WebRTC call configuration & rationale
|
||||
|
||||
All tunables live in `src/network/webrtc/config.js`. This doc explains the values
|
||||
and where they come from. Built incrementally per step; **all steps shipped** —
|
||||
adaptive audio, SVC video, transport feedback, and runtime bitrate adaptation are
|
||||
live in the 1:1 call flow.
|
||||
|
||||
## How the pieces attach (why not one `configureAudioSender`)
|
||||
|
||||
A single `RTCRtpSender` cannot carry codec ordering or fmtp, so the brief's
|
||||
`configureAudioSender` is split across the three WebRTC surfaces at their correct
|
||||
lifecycle points (see `EnhancedSecureWebRTCManager` call methods):
|
||||
|
||||
| Concern | API surface | When | Code |
|
||||
|---|---|---|---|
|
||||
| RED-first / Opus-second ordering | `transceiver.setCodecPreferences` | before `createOffer`/`createAnswer` | `applyAudioCodecPreferences` (audio.js) via `_applyAudioCodecPrefs` |
|
||||
| Opus FEC/DTX/bitrate | SDP `a=fmtp` munging | after create, before `setLocalDescription` | `applyOpusSettings` (sdp.js) via `_mungeCallSdp` |
|
||||
| priority / networkPriority / maxBitrate | `sender.setParameters` | after `setLocalDescription` | `configureAudioSender` (audio.js) via `_applyCallSenderParams` |
|
||||
|
||||
Both peers run the same munging, so the negotiated session carries the params.
|
||||
|
||||
## Step 2 — Audio (`AUDIO_CONFIG`)
|
||||
|
||||
### Opus fmtp (`opusFmtp`)
|
||||
| Param | Value | Why |
|
||||
|---|---|---|
|
||||
| `minptime` | 10 | Smaller packetisation time → lower latency. Opus RFC 7587 §7. |
|
||||
| `useinbandfec` | 1 | In-band FEC reconstructs a lost packet from the next one — the main lever for the 15–20% loss target. RFC 6716 §2.1.7. |
|
||||
| `usedtx` | 1 | Discontinuous transmission: stop sending in silence, freeing the shared transport for video/FEC. RFC 7587 §3.1.3. |
|
||||
| `stereo` | 0 | Mono voice — halves bitrate, no quality loss for speech. |
|
||||
| `maxaveragebitrate` | 32000 | Comfortable wideband speech; brief-specified. |
|
||||
| `cbr` | 0 | Variable bitrate spends bits only when needed — better quality/bit than CBR for speech. |
|
||||
|
||||
### RED (`preferRed: true`)
|
||||
RFC 2198 redundant audio: each packet also carries the previous frame's payload,
|
||||
so isolated losses recover without retransmission. Enabled only when the browser
|
||||
advertises `audio/red` in `RTCRtpSender.getCapabilities('audio')` (Chromium yes;
|
||||
Safari/Firefox vary → silently skipped). Ordered **RED first, Opus second** via
|
||||
`setCodecPreferences`.
|
||||
|
||||
### Sender (`sender`)
|
||||
| Param | Value | Why |
|
||||
|---|---|---|
|
||||
| `maxBitrate` | 40000 bps | Brief value; head-room above 32 kbps Opus for RED redundancy. |
|
||||
| `priority` | `high` | Bandwidth arbitration within the PC — audio wins over video. |
|
||||
| `networkPriority` | `high` | DSCP hint so audio packets are prioritised on the wire. |
|
||||
|
||||
## Step 3 — Video (`VIDEO_CONFIG`)
|
||||
|
||||
### Codec preference (`codecPreferenceOrder`)
|
||||
`VP9 → AV1 → H.264 → VP8`, applied with `setCodecPreferences` (via
|
||||
`applyVideoCodecPreferences`). rtx/red/fec codecs are kept after the media codecs
|
||||
so retransmission/FEC still work. VP9/AV1 give SVC; H.264/VP8 are plain.
|
||||
|
||||
### Single-encoding SVC (why not multi-rid simulcast)
|
||||
This is **1:1 P2P** (one receiver), so a single encoding with **SVC** is the right
|
||||
tool: one stream that degrades by spatial/temporal layer. It's applied through
|
||||
`sender.setParameters` (`configureVideoSender`) and needs **no** `addTransceiver`/
|
||||
rids, so it doesn't disturb the working `addTrack` media path.
|
||||
|
||||
| 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 (plain) | 1.5 Mbps | `balanced` |
|
||||
|
||||
`networkPriority: 'medium'` — below audio's `high`. If a browser rejects the SVC
|
||||
mode (Firefox/Safari gaps), `configureVideoSender` retries with a plain encoding.
|
||||
|
||||
### Initialization (Step 6) + simulcast (Step 3) — what shipped, and why
|
||||
|
||||
**Attempted then reverted:** an explicit `addTransceiver({sendEncodings})` path
|
||||
(single-encoding SVC / multi-rid simulcast) was implemented but **broke media on
|
||||
real devices** during testing:
|
||||
- on the **answerer**, reusing the transceiver created by `setRemoteDescription`
|
||||
rejected the SVC `setParameters` (`"parameters are not valid"`), and
|
||||
- on **role-reversed / repeat calls**, the reused transceiver **directions
|
||||
desynced** — the call connected (timer ran, `ontrack` fired) but no audio/video
|
||||
actually flowed.
|
||||
|
||||
**What ships instead:** media is attached with **`addTrack`** (reused across calls
|
||||
via `replaceTrack`), letting the browser manage transceiver direction implicitly —
|
||||
this is what keeps audio/video flowing across reversed and repeat calls. Video uses
|
||||
**single-encoding SVC** (VP9 `L3T3_KEY` / AV1 `L1T3`) applied via
|
||||
`configureVideoSender`→`setParameters`, plus `setCodecPreferences` (VP9→AV1→H264→VP8).
|
||||
For 1:1 P2P a single SVC stream is the right tool anyway — it degrades by
|
||||
spatial/temporal layer, which is what the "video degrades gracefully" requirement
|
||||
needs. `configureVideoSender` falls back to a plain encoding if a scalabilityMode
|
||||
is rejected.
|
||||
|
||||
**Multi-rid simulcast** (`buildVideoSendEncodings`, `VIDEO_CONFIG.*.simulcast`) is
|
||||
kept as **tested, exported primitives** for a future SFU/group-call path, but is
|
||||
**not wired into the 1:1 call flow** — it needs `addTransceiver({sendEncodings})`,
|
||||
which requires the answerer-direction / setParameters issues above to be solved
|
||||
first (ideally with a Playwright two-PC test rig, which isn't set up). The
|
||||
adaptation controller is already simulcast-aware (`_applyToVideoSender` throttles
|
||||
only the top layer) for when that lands.
|
||||
|
||||
## Step 4 — Transport (`TRANSPORT_CONFIG`)
|
||||
|
||||
Ensures the RTCP feedback / header extensions are present on the call m-lines
|
||||
(most browsers already emit them, so this is an idempotent safety net):
|
||||
|
||||
| m-line | rtcp-fb | header ext |
|
||||
|---|---|---|
|
||||
| video | `transport-cc`, `nack`, `nack pli`, `ccm fir`, `goog-remb` | TWCC (`…transport-wide-cc…`) |
|
||||
| audio | `transport-cc`, `nack` | TWCC |
|
||||
|
||||
Implemented as pure, idempotent SDP utils (`ensureRtcpFb`, `ensureExtmap`,
|
||||
`applyTransport`) — added only when missing, never duplicated, applied only to
|
||||
primary codecs (rtx/red/fec skipped). `transport-cc` (TWCC) is what feeds the
|
||||
bandwidth estimator the Step-5 controller reads.
|
||||
|
||||
**Safety:** munged local SDP is set via `_setLocalMunged` with progressive
|
||||
fallback — full munge → Opus-only → raw — so a browser rejecting an injected
|
||||
line can never break the call.
|
||||
|
||||
## Step 5 — Adaptation + quality indicator (`ADAPTATION_CONFIG`)
|
||||
|
||||
`NetworkAdaptationController` reads `pc.getStats()` every 1 s and reacts:
|
||||
|
||||
| Condition | Action | Note |
|
||||
|---|---|---|
|
||||
| loss > 10% **or** RTT > 300 ms | video `maxBitrate` −20% (floor 100 kbps) | audio never touched |
|
||||
| loss < 3% **and** RTT < 150 ms, 5 ticks | video `maxBitrate` +10% (up to ceiling) | gradual recovery |
|
||||
| `qualityLimitationReason === 'cpu'` | `scaleResolutionDownBy` ×1.5 | bitrate unchanged |
|
||||
|
||||
All changes go through `sender.setParameters` — **no renegotiation, no track
|
||||
restart**. **Audio is never throttled** by the controller, so speech stays intact
|
||||
regardless of loss (stronger than the brief's "≥25%" guard). The pure decision
|
||||
(`decideAdaptation`) and stats parsing (`summarizeStats`) are unit-tested with
|
||||
mock getStats.
|
||||
|
||||
### Connection-quality indicator (user-facing)
|
||||
The same `getStats` sample yields a coarse label (`qualityFromMetrics`) surfaced in
|
||||
`callState.quality` and rendered in the call UI as signal bars + text:
|
||||
|
||||
| Label | Loss / RTT | Colour |
|
||||
|---|---|---|
|
||||
| Excellent | <3% and <150 ms | green |
|
||||
| Good | <7% and <250 ms | green |
|
||||
| Fair | <15% and <400 ms | yellow |
|
||||
| Weak | otherwise | red |
|
||||
|
||||
Shown in the voice overlay, the video top bar, and (compact bars) the minimized
|
||||
widget. Hidden until the first sample has data.
|
||||
|
||||
## Verification (all steps)
|
||||
- Unit: `npm test` (SDP, video codecs, adaptation) — all green.
|
||||
- Manual (`chrome://webrtc-internals`, per criteria): throttle the link (DevTools
|
||||
Network / OS) → outbound video `targetBitrate` steps down within ~1–2 s and
|
||||
recovers when the link clears; audio bitrate holds; the in-call indicator moves
|
||||
Excellent → Fair → Weak.
|
||||
- Audio: in a call, open `chrome://webrtc-internals` → audio outbound-rtp should
|
||||
show Opus with the fmtp above; on Chromium the codec is `red`/`opus`. Under
|
||||
packet loss, `useinbandfec` keeps audio intelligible.
|
||||
|
||||
> The temporary `[Call]` / `window.__sbCallLog` diagnostic logger used during
|
||||
> bring-up has been removed for production; the manager's `_secureLog` handles
|
||||
> runtime logging and stays silent in production builds (`DEBUG_MODE = false`).
|
||||
Reference in New Issue
Block a user