Overview
The SFU periodically sends a set of control-plane messages, which the SDK turns into fourChannelDelegate events:
These four events exist only on the SeaStart (SFU) engine. They travel over the signaling DataChannel on the subscribing PeerConnection, and the Wangsu (CDN) engine has no such path, so with CDN you receive none of them and
getConnectionQuality() also returns nil. Use Channel.streamVendor to tell the current engine—see Types.Network quality: two event streams, don’t mix them up
Quality uses two event streams, because the two kinds of needs differ in trigger frequency by an order of magnitude:didReceiveQualityReport—fires on every server report; it’s a stream of raw valuesdidChangeConnectionQuality—fires only when the level changes; it signals a state change
QualityReport has two QualitySample values, pub (uplink, client to SFU) and sub (downlink, SFU to client); for field meanings, see Types. Key points:
levelis the level given by the server; for bothscore(0–100) andmos(1.0–4.5), higher is betterlossis a ratio (0–1), not a percentagerttandjitterare in milliseconds;bitrateis in kbps- When figuring out “whose problem it is”, look at both sides: a poor
pubmeans a problem with your own uplink; a poorsubmeans a problem with the downlink or the remote side’s uplink
evaluation.overall in ConnectionQualityChange takes the worse of the uplink and downlink levels (unknown < excellent < good < poor < lost), and evaluation.mos takes the smaller of the two—both follow “the side the user perceives as weakest”. previous is the level before the change, and is .unknown on the first change.
Cold start: show the level as soon as the page opens
Events arrive only on changes, so when the UI is first created you have no value yet. CallgetConnectionQuality() once to fill in the current snapshot:
nil when no report has been received yet (which is different from “a report was received but the level is unknown”).
After a reconnect, the SDK clears the quality cache (latest evaluation, level-change baseline, speaker snapshot), so the UI doesn’t keep showing a stale
poor level after recovery. So after a reconnect, getConnectionQuality() briefly returns nil until a new report arrives—this is intentional; just treat it as “no data yet” in the UI.Active speakers
snapshot.speakers is the full list: the SDK has already merged the server’s incremental protocol (who started speaking, who stopped) into a complete snapshot, sorted by level in descending order. So:
- Your app just overwrites the UI state as a whole; you don’t need to maintain a set of “who is still speaking” yourself
- When nobody is speaking,
speakersis an empty array—the event isn’t skipped levelis a normalized linear volume (0–1), which you can use directly to draw a volume bar
ActiveSpeakerInfo carries uid and trackId—the same user may have multiple audio tracks, so use the latter when you need to pinpoint the exact track.
Simulcast: switch layers manually
When the publisher has simulcast enabled, the same video has multiple layers. By default the SFU selects a layer automatically based on bandwidth, and notifies you throughdidSwitchLayer after switching (reason is a server reason such as bwe_down / bwe_up):
didSwitchLayer fires as well, with reason set to client.
Throws:
SRTCError.engineNotSupported(_:)—the current engine isn’t SeaStart (for example, when using CDN)SRTCError.transportNotReady—the signaling channel isn’t ready yet (just joined, or reconnecting)SRTCError.webrtcError(_:)—sending on the signaling channel failed (usually buffer congestion)