392 lines
19 KiB
HTML
392 lines
19 KiB
HTML
<!DOCTYPE html>
|
|||
|
|
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||
|
|
change the Markdown instead. -->
|
||
|
|
<html lang="en" dir="ltr">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||
|
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||
|
|
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||
|
|
<title>Voice and video calls - SecureBit.chat</title>
|
||
|
|
<meta name="description" content="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…">
|
||
|
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||
|
|
<link rel="canonical" href="https://securebit.chat/docs/calls/">
|
||
|
|
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||
|
|
<meta property="og:site_name" content="SecureBit.chat">
|
||
|
|
<meta property="og:title" content="Voice and video calls - SecureBit.chat">
|
||
|
|
<meta property="og:description" content="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…">
|
||
|
|
<meta property="og:url" content="https://securebit.chat/docs/calls/">
|
||
|
|
<meta property="og:type" content="article">
|
||
|
|
<meta property="og:locale" content="en_US">
|
||
|
|
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||
|
|
<meta name="twitter:card" content="summary_large_image">
|
||
|
|
<meta name="twitter:title" content="Voice and video calls - SecureBit.chat">
|
||
|
|
<meta name="twitter:description" content="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…">
|
||
|
|
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||
|
|
<script type="application/ld+json">
|
||
|
|
{
|
||
|
|
"@context": "https://schema.org",
|
||
|
|
"@type": "TechArticle",
|
||
|
|
"headline": "Voice and video calls",
|
||
|
|
"description": "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…",
|
||
|
|
"url": "https://securebit.chat/docs/calls/",
|
||
|
|
"inLanguage": "en",
|
||
|
|
"isPartOf": {
|
||
|
|
"@type": "WebSite",
|
||
|
|
"@id": "https://securebit.chat/#website"
|
||
|
|
},
|
||
|
|
"publisher": {
|
||
|
|
"@id": "https://securebit.chat/#organization"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
<style>
|
||
|
|
:root { color-scheme: dark; }
|
||
|
|
* { box-sizing: border-box; }
|
||
|
|
body {
|
||
|
|
margin: 0;
|
||
|
|
background: #0f0f11;
|
||
|
|
color: #d6d6dc;
|
||
|
|
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
|
|
font-size: 16px;
|
||
|
|
line-height: 1.68;
|
||
|
|
}
|
||
|
|
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||
|
|
.top {
|
||
|
|
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||
|
|
padding-bottom: 16px; margin-bottom: 40px;
|
||
|
|
border-bottom: 1px solid rgba(255,255,255,.08);
|
||
|
|
font-size: 13.5px;
|
||
|
|
}
|
||
|
|
.top a { color: #8a8a92; text-decoration: none; }
|
||
|
|
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||
|
|
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||
|
|
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||
|
|
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||
|
|
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||
|
|
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||
|
|
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||
|
|
p, li { color: #a9a9b3; }
|
||
|
|
p { margin: 0 0 16px; }
|
||
|
|
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||
|
|
li { margin: 5px 0; }
|
||
|
|
a { color: #f0892a; text-underline-offset: 2px; }
|
||
|
|
strong { color: #e8e8eb; }
|
||
|
|
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||
|
|
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||
|
|
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||
|
|
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||
|
|
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||
|
|
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||
|
|
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||
|
|
td:first-child, th:first-child { padding-inline-start: 0; }
|
||
|
|
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||
|
|
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||
|
|
img { max-width: 100%; height: auto; }
|
||
|
|
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||
|
|
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||
|
|
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||
|
|
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||
|
|
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||
|
|
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="wrap">
|
||
|
|
<nav class="top">
|
||
|
|
<a class="brand" href="/">SecureBit.chat</a>
|
||
|
|
<a href="/docs/">Documentation</a>
|
||
|
|
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||
|
|
<span class="here">Voice and video calls</span>
|
||
|
|
</nav>
|
||
|
|
<h1>Voice and video calls</h1>
|
||
|
|
<p>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.</p>
|
||
|
|
<p>All tunable values live in <code>src/network/webrtc/config.js</code>. This document explains
|
||
|
|
where they come from, because the numbers are otherwise indistinguishable from
|
||
|
|
arbitrary choices.</p>
|
||
|
|
<h2 id="where-the-settings-attach">Where the settings attach</h2>
|
||
|
|
<p>A single <code>RTCRtpSender</code> 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:</p>
|
||
|
|
<div class="tablewrap"><table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Concern</th>
|
||
|
|
<th>Surface</th>
|
||
|
|
<th>When</th>
|
||
|
|
<th>Implementation</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody><tr>
|
||
|
|
<td>Codec ordering (RED before Opus)</td>
|
||
|
|
<td><code>transceiver.setCodecPreferences</code></td>
|
||
|
|
<td>before creating the offer or answer</td>
|
||
|
|
<td><code>applyAudioCodecPreferences</code> in <code>audio.js</code></td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Opus FEC, DTX, bitrate</td>
|
||
|
|
<td>SDP <code>a=fmtp</code> rewriting</td>
|
||
|
|
<td>after create, before <code>setLocalDescription</code></td>
|
||
|
|
<td><code>applyOpusSettings</code> in <code>sdp.js</code></td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Priority and maximum bitrate</td>
|
||
|
|
<td><code>sender.setParameters</code></td>
|
||
|
|
<td>after <code>setLocalDescription</code></td>
|
||
|
|
<td><code>configureAudioSender</code> in <code>audio.js</code></td>
|
||
|
|
</tr>
|
||
|
|
</tbody></table></div>
|
||
|
|
<p>Both peers apply the same rewriting, so the negotiated session carries the
|
||
|
|
parameters regardless of who called.</p>
|
||
|
|
<h2 id="audio">Audio</h2>
|
||
|
|
<p>Speech has to stay intelligible on a bad link, and that goal drives every value
|
||
|
|
below.</p>
|
||
|
|
<h3 id="opus-parameters">Opus parameters</h3>
|
||
|
|
<div class="tablewrap"><table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Parameter</th>
|
||
|
|
<th>Value</th>
|
||
|
|
<th>Reason</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody><tr>
|
||
|
|
<td><code>minptime</code></td>
|
||
|
|
<td>10</td>
|
||
|
|
<td>Smaller packetisation interval, lower latency (RFC 7587, section 7)</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>useinbandfec</code></td>
|
||
|
|
<td>1</td>
|
||
|
|
<td>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)</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>usedtx</code></td>
|
||
|
|
<td>1</td>
|
||
|
|
<td>Discontinuous transmission stops sending during silence, leaving the transport free for video and FEC (RFC 7587, section 3.1.3)</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>stereo</code></td>
|
||
|
|
<td>0</td>
|
||
|
|
<td>Mono halves the bitrate with no loss for speech</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>maxaveragebitrate</code></td>
|
||
|
|
<td>32000</td>
|
||
|
|
<td>Comfortable wideband speech</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>cbr</code></td>
|
||
|
|
<td>0</td>
|
||
|
|
<td>Variable bitrate spends bits only when needed</td>
|
||
|
|
</tr>
|
||
|
|
</tbody></table></div>
|
||
|
|
<h3 id="redundancy">Redundancy</h3>
|
||
|
|
<p>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 <code>audio/red</code> in <code>RTCRtpSender.getCapabilities('audio')</code>, which
|
||
|
|
Chromium does and others vary on; when absent it is skipped silently. RED is
|
||
|
|
ordered before Opus in codec preferences.</p>
|
||
|
|
<h3 id="sender-parameters">Sender parameters</h3>
|
||
|
|
<div class="tablewrap"><table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Parameter</th>
|
||
|
|
<th>Value</th>
|
||
|
|
<th>Reason</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody><tr>
|
||
|
|
<td><code>maxBitrate</code></td>
|
||
|
|
<td>40000 bps</td>
|
||
|
|
<td>Headroom above the 32 kbps Opus target for RED redundancy</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>priority</code></td>
|
||
|
|
<td><code>high</code></td>
|
||
|
|
<td>Audio wins bandwidth arbitration against video within the connection</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>networkPriority</code></td>
|
||
|
|
<td><code>high</code></td>
|
||
|
|
<td>DSCP hint so audio is prioritised on the wire</td>
|
||
|
|
</tr>
|
||
|
|
</tbody></table></div>
|
||
|
|
<p>Audio is never throttled by the adaptation controller. Under sustained loss the
|
||
|
|
video degrades and speech continues.</p>
|
||
|
|
<h2 id="video">Video</h2>
|
||
|
|
<p>Codec preference order is VP9, AV1, H.264, VP8, applied through
|
||
|
|
<code>setCodecPreferences</code>. 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.</p>
|
||
|
|
<h3 id="scalable-coding-rather-than-simulcast">Scalable coding rather than simulcast</h3>
|
||
|
|
<p>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 <code>sender.setParameters</code> and needs no <code>addTransceiver</code> or rid
|
||
|
|
configuration, which keeps it away from the media path that actually works.</p>
|
||
|
|
<div class="tablewrap"><table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Codec</th>
|
||
|
|
<th>scalabilityMode</th>
|
||
|
|
<th>maxBitrate</th>
|
||
|
|
<th>degradationPreference</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody><tr>
|
||
|
|
<td>VP9</td>
|
||
|
|
<td><code>L3T3_KEY</code> (3 spatial, 3 temporal, key-aligned)</td>
|
||
|
|
<td>1.5 Mbps</td>
|
||
|
|
<td><code>balanced</code></td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>AV1</td>
|
||
|
|
<td><code>L1T3</code></td>
|
||
|
|
<td>1.2 Mbps</td>
|
||
|
|
<td><code>maintain-framerate</code></td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>H.264, VP8</td>
|
||
|
|
<td>none</td>
|
||
|
|
<td>1.5 Mbps</td>
|
||
|
|
<td><code>balanced</code></td>
|
||
|
|
</tr>
|
||
|
|
</tbody></table></div>
|
||
|
|
<p><code>networkPriority</code> is <code>medium</code>, below audio. If a browser rejects the scalability
|
||
|
|
mode, which Firefox and Safari do in places, <code>configureVideoSender</code> retries with
|
||
|
|
a plain encoding.</p>
|
||
|
|
<h3 id="why-media-is-attached-with-addtrack">Why media is attached with addTrack</h3>
|
||
|
|
<p>An explicit <code>addTransceiver({ sendEncodings })</code> 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 <code>setRemoteDescription</code> rejected the SVC
|
||
|
|
parameters outright. On repeat and role-reversed calls, the reused transceiver
|
||
|
|
directions desynchronised: the call connected and <code>ontrack</code> fired, but no media
|
||
|
|
flowed.</p>
|
||
|
|
<p>What ships instead attaches media with <code>addTrack</code>, reused across calls through
|
||
|
|
<code>replaceTrack</code>, and lets the browser manage transceiver direction. This is what
|
||
|
|
keeps audio and video flowing across reversed and repeated calls.</p>
|
||
|
|
<p>Multi-rid simulcast primitives (<code>buildVideoSendEncodings</code>) are kept and tested for
|
||
|
|
a future group-call path, but are not wired into the one-to-one flow. They need
|
||
|
|
<code>addTransceiver({ sendEncodings })</code>, 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.</p>
|
||
|
|
<h2 id="transport-feedback">Transport feedback</h2>
|
||
|
|
<p>The call m-lines need RTCP feedback and header extensions present. Most browsers
|
||
|
|
emit them already, so this is an idempotent safety net:</p>
|
||
|
|
<div class="tablewrap"><table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Media</th>
|
||
|
|
<th>RTCP feedback</th>
|
||
|
|
<th>Header extension</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody><tr>
|
||
|
|
<td>Video</td>
|
||
|
|
<td><code>transport-cc</code>, <code>nack</code>, <code>nack pli</code>, <code>ccm fir</code>, <code>goog-remb</code></td>
|
||
|
|
<td>transport-wide congestion control</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Audio</td>
|
||
|
|
<td><code>transport-cc</code>, <code>nack</code></td>
|
||
|
|
<td>transport-wide congestion control</td>
|
||
|
|
</tr>
|
||
|
|
</tbody></table></div>
|
||
|
|
<p>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.</p>
|
||
|
|
<p>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.</p>
|
||
|
|
<h2 id="adaptation">Adaptation</h2>
|
||
|
|
<p><code>NetworkAdaptationController</code> samples <code>pc.getStats()</code> every second and reacts:</p>
|
||
|
|
<div class="tablewrap"><table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Condition</th>
|
||
|
|
<th>Action</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody><tr>
|
||
|
|
<td>Loss above 10 percent, or round trip above 300 ms</td>
|
||
|
|
<td>Reduce video <code>maxBitrate</code> by 20 percent, floor 100 kbps</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Loss below 3 percent and round trip below 150 ms, sustained 5 samples</td>
|
||
|
|
<td>Raise video <code>maxBitrate</code> by 10 percent, up to the ceiling</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td><code>qualityLimitationReason</code> is <code>cpu</code></td>
|
||
|
|
<td>Scale resolution down by 1.5, bitrate unchanged</td>
|
||
|
|
</tr>
|
||
|
|
</tbody></table></div>
|
||
|
|
<p>Every change goes through <code>sender.setParameters</code>. There is no renegotiation and
|
||
|
|
no track restart, so adaptation is invisible to the call. Audio is never touched.</p>
|
||
|
|
<p>The decision function and the stats parsing are pure and unit-tested against
|
||
|
|
recorded <code>getStats</code> output.</p>
|
||
|
|
<h3 id="quality-indicator">Quality indicator</h3>
|
||
|
|
<p>The same sample produces a coarse label shown in the call interface:</p>
|
||
|
|
<div class="tablewrap"><table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Label</th>
|
||
|
|
<th>Condition</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody><tr>
|
||
|
|
<td>Excellent</td>
|
||
|
|
<td>Loss below 3 percent and round trip below 150 ms</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Good</td>
|
||
|
|
<td>Loss below 7 percent and round trip below 250 ms</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Fair</td>
|
||
|
|
<td>Loss below 15 percent and round trip below 400 ms</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Weak</td>
|
||
|
|
<td>Anything else</td>
|
||
|
|
</tr>
|
||
|
|
</tbody></table></div>
|
||
|
|
<p>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.</p>
|
||
|
|
<h2 id="verifying-a-change">Verifying a change</h2>
|
||
|
|
<p>Unit tests cover SDP rewriting, video codec selection and the adaptation
|
||
|
|
decision:</p>
|
||
|
|
<pre><code class="language-bash">npm test
|
||
|
|
</code></pre>
|
||
|
|
<p>For anything touching media in practice, open <code>chrome://webrtc-internals</code> during
|
||
|
|
a call and check that:</p>
|
||
|
|
<ul>
|
||
|
|
<li>outbound audio shows Opus with the fmtp parameters above, appearing as <code>red</code>
|
||
|
|
and <code>opus</code> on Chromium</li>
|
||
|
|
<li>throttling the link steps outbound video <code>targetBitrate</code> down within a second
|
||
|
|
or two and recovers when the link clears</li>
|
||
|
|
<li>audio bitrate holds steady while video adapts</li>
|
||
|
|
<li>the in-call indicator moves through Fair and Weak as the link degrades</li>
|
||
|
|
</ul>
|
||
|
|
|
||
|
|
<nav class="more">
|
||
|
|
<h2>More documentation</h2>
|
||
|
|
<ul>
|
||
|
|
<li><a href="/docs/">Documentation</a></li>
|
||
|
|
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||
|
|
<li><a href="/docs/architecture/">Architecture</a></li>
|
||
|
|
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||
|
|
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||
|
|
<li><a href="/docs/configuration/">Configuration</a></li>
|
||
|
|
<li><a href="/docs/api/">API Notes</a></li>
|
||
|
|
<li><a href="/docs/contributing/">Contributing</a></li>
|
||
|
|
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||
|
|
</ul>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
</html>
|