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

1"""Bandwidth model — mesh vs SFU per-peer cost crossover by call kind. 

2 

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. 

6 

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. 

10 

11Topology cost model (per peer, sustained bitrate): 

12 

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) 

15 

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) 

18 

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)×.) 

23 

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: 

26 

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. 

35 

36So we use a different *practical* crossover per kind: 

37 

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. 

43 

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+). 

50 

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+). 

55 

56These are *defaults*. Operators can override per-kind via env vars 

57(see api_calls._mesh_threshold). Live measurement (Task #276) will 

58validate. 

59""" 

60 

61from __future__ import annotations 

62 

63from typing import Dict, NamedTuple 

64 

65 

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} 

74 

75 

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 

82 

83 

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 

98 

99 

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. 

102 

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 

115 

116 

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 

121 

122 

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. 

153 

154OPERATIONAL_THRESHOLDS = { 

155 'voice': 4, 

156 'video': 3, 

157 'screen_share': 1, 

158 'mixed': 1, 

159} 

160 

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} 

173 

174 

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]