Coverage for integrations / social / _mesh_bandwidth_model.py: 75.9%
29 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""Bandwidth model — mesh vs SFU per-peer cost crossover by call kind.
3This module is the SINGLE SOURCE OF TRUTH for per-kind mesh thresholds.
4``api_calls._DEFAULT_KIND_THRESHOLDS`` is an import-alias of
5``OPERATIONAL_THRESHOLDS`` defined here — there is no duplicate dict.
7Real-world measurement (Task #276 live phase) is expected to validate
8or refine these constants — until then they are the *theoretical*
9crossover from first principles, not an arbitrary choice of 4.
11Topology cost model (per peer, sustained bitrate):
13 Mesh: upload = (N - 1) × bitrate_per_track (encode once, send N-1 copies — *)
14 download = (N - 1) × bitrate_per_track (one stream per other peer)
16 SFU: upload = 1 × bitrate_per_track (one stream up to SFU)
17 download = (N - 1) × bitrate_per_track (SFU forwards each peer's stream)
19(* `react-native-webrtc` and the LiveKit web SDK both emit a single
20 encoded copy per outgoing track and the OS-level networking layer
21 does the duplicate sends — so the *encode* cost is 1×, but the
22 *upload bandwidth* cost is (N-1)×.)
24For each call kind, the SFU starts winning on **per-peer upload** the
25moment (N - 1) > 1, i.e. at N ≥ 3. But that's not the whole story:
27 - Mesh has a smaller signaling round-trip on the happy path
28 (no SFU media transit).
29 - Mesh is the only mode that actually preserves end-to-end media
30 encryption between users (SFU sees the media frames).
31 - Mesh keeps working when central / regional HARTOS is down.
32 - SFU adds operational complexity (binary lifecycle, port mgmt,
33 Redis for clusters) — only worth it when the bandwidth pain
34 actually shows up.
36So we use a different *practical* crossover per kind:
38 * voice (32 kbps Opus):
39 mesh upload at N=4 = 3 × 32 = 96 kbps — fine on every uplink.
40 mesh upload at N=5 = 4 × 32 = 128 kbps — still fine.
41 Default threshold = 4 (promote at N=5+). Adoptable up to ~6
42 if measured.
44 * video (500 kbps default VP8):
45 mesh upload at N=3 = 2 × 500 = 1.0 Mbps — borderline on
46 residential ADSL up / shared 4G uplink.
47 mesh upload at N=4 = 3 × 500 = 1.5 Mbps — actively painful
48 for many residential connections.
49 Default threshold = 3 (promote at N=4+).
51 * screen_share, mixed:
52 Always SFU regardless of N. Multi-track re-negotiation (camera +
53 screen + audio) on a mesh re-shuffles SDP for every peer; SFU
54 handles it once. Threshold = 1 (promote at N=2+).
56These are *defaults*. Operators can override per-kind via env vars
57(see api_calls._mesh_threshold). Live measurement (Task #276) will
58validate.
59"""
61from __future__ import annotations
63from typing import Dict, NamedTuple
66# Approximate sustained bitrates by media kind (kbps per outgoing track).
67KIND_BITRATE_KBPS = {
68 'voice': 32, # Opus narrowband sweet spot
69 'video': 500, # VP8 default, no simulcast
70 'video_simulcast': 1500, # VP8 simulcast 3 layers (worst case)
71 'av1_video': 300, # AV1 — 30-40% cheaper than VP8
72 'screen_share': 800, # screen 1080p25 typical
73}
76class CostPoint(NamedTuple):
77 n: int
78 mesh_up_kbps: float
79 mesh_down_kbps: float
80 sfu_up_kbps: float
81 sfu_down_kbps: float
84def crossover_table(kind: str = 'voice', max_n: int = 8) -> Dict[int, CostPoint]:
85 """Return per-peer bandwidth cost table for a given kind, N=2..max_n."""
86 bitrate = KIND_BITRATE_KBPS.get(kind, 500)
87 table: Dict[int, CostPoint] = {}
88 for n in range(2, max_n + 1):
89 peers = n - 1
90 table[n] = CostPoint(
91 n=n,
92 mesh_up_kbps=peers * bitrate,
93 mesh_down_kbps=peers * bitrate,
94 sfu_up_kbps=bitrate,
95 sfu_down_kbps=peers * bitrate,
96 )
97 return table
100def first_n_where_mesh_upload_exceeds(kind: str, ceiling_kbps: float) -> int:
101 """Return the smallest N where mesh upload per peer > ceiling_kbps.
103 Used to compute a 'promote to SFU' threshold given an uplink
104 budget. E.g. if your residential ADSL uplink is 1000 kbps and the
105 call kind is video (500 kbps), this returns 3 (mesh upload at
106 N=3 = 2 × 500 = 1000 kbps which equals — at N=4 it exceeds).
107 """
108 bitrate = KIND_BITRATE_KBPS.get(kind, 500)
109 n = 2
110 while n <= 64: # safety cap
111 if (n - 1) * bitrate > ceiling_kbps:
112 return n
113 n += 1
114 return 64
117# Bandwidth ceilings — conservative residential numbers used as
118# inputs to the model.
119RESIDENTIAL_UPLINK_KBPS = 1500 # 1.5 Mbps — ADSL / shared 4G floor
120MOBILE_4G_UPLINK_KBPS = 5000 # decent 4G — comfortable video uplink
123# ── Operational vs theoretical thresholds ─────────────────────────────
124#
125# The pure-bandwidth model alone produces *generous* thresholds:
126# voice on 1500 kbps uplink: mesh ok through N=46 (32 kbps × 45)
127# video on 1500 kbps uplink: mesh ok through N=4 (500 kbps × 3 = 1500)
128#
129# But mesh has costs the bandwidth model ignores:
130# 1. CPU encode is 1× regardless of N (the encoder emits one stream
131# and the OS networking layer fans it out), BUT codec parameters
132# become harder to negotiate as N grows — each additional peer's
133# ICE/DTLS/SDP exchange happens on the call's hot path.
134# 2. Signaling round-trips: mesh has O(N²) signaling messages on
135# join. Past N=4 this starts adding visible "joining" latency.
136# 3. Subjective UX research (e.g., Hangouts / Meet older designs)
137# consistently puts the "too noisy" boundary at 4-5 simultaneous
138# audio sources without any active speaker detection.
139#
140# So the *operationally-chosen* defaults are tighter than the pure
141# bandwidth crossover. Encoded in OPERATIONAL_THRESHOLDS below — this
142# is the canonical home; api_calls imports it as
143# _DEFAULT_KIND_THRESHOLDS:
144#
145# voice → 4 (mesh mode for ≤ 4 participants; SFU at 5+)
146# video → 3 (mesh for ≤ 3; SFU at 4+ to leave headroom under 1500)
147# screen_share → 1 (always SFU — multi-track re-negotiation is hard)
148# mixed → 1 (always SFU — same reason)
149#
150# Live measurement (Task #276) is expected to validate these. When it
151# does, update OPERATIONAL_THRESHOLDS below — api_calls picks it up
152# automatically via the import alias.
154OPERATIONAL_THRESHOLDS = {
155 'voice': 4,
156 'video': 3,
157 'screen_share': 1,
158 'mixed': 1,
159}
161# Pure-bandwidth crossover (informational; not used as defaults). These
162# are the largest N where mesh upload still fits within the residential
163# uplink ceiling. Useful for operators on faster uplinks who want to
164# raise the threshold above the operational default.
165BANDWIDTH_ONLY_CEILING_AT_RESIDENTIAL = {
166 'voice': first_n_where_mesh_upload_exceeds(
167 'voice', RESIDENTIAL_UPLINK_KBPS) - 1,
168 'video': first_n_where_mesh_upload_exceeds(
169 'video', RESIDENTIAL_UPLINK_KBPS) - 1,
170 'screen_share': first_n_where_mesh_upload_exceeds(
171 'screen_share', RESIDENTIAL_UPLINK_KBPS) - 1,
172}
175__all__ = [
176 'KIND_BITRATE_KBPS',
177 'CostPoint',
178 'crossover_table',
179 'first_n_where_mesh_upload_exceeds',
180 'RESIDENTIAL_UPLINK_KBPS',
181 'MOBILE_4G_UPLINK_KBPS',
182 'OPERATIONAL_THRESHOLDS',
183 'BANDWIDTH_ONLY_CEILING_AT_RESIDENTIAL',
184]