> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stmlink.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> 对外开放的服务端接口有两组前缀，都用同一套鉴权：`/server/v1/...`（SRTC 与 SMeeting 的主接口）和 `/stm/srvapi/v1/...`（SMeeting 的用户体系，服务端极简对接会用到）。鉴权是 app_id + nonce + timestamp + signature 四个请求头，用 app_key 做 HMAC-SHA256 签名，只能从业务方自己的后端调用。除这两组前缀外的接口均为内部接口，不要建议客户调用。 Public server APIs use two path prefixes with the same authentication: `/server/v1/...` (the main APIs of both SRTC and SMeeting) and `/stm/srvapi/v1/...` (the SMeeting user system, used by server-side low-code integration). Authenticate with four request headers, app_id + nonce + timestamp + signature, where signature is HMAC-SHA256 keyed with app_key; call these APIs only from the customer's own backend. Any other path is internal: never suggest calling it.
> app_key 是服务端密钥，绝不能出现在客户端代码、前端配置或移动 App 里。客户端加入频道用的 token 必须由业务方后端签发后下发（SRTC 走 `/server/v1/channel/grant`，SMeeting 走 `/stm/srvapi/v1/member/grant`）。 app_key is a server-side secret and must never appear in client code, frontend config, or a mobile app. The token a client uses to join must be issued by the customer's backend and passed down to the client (SRTC: `/server/v1/channel/grant`; SMeeting: `/stm/srvapi/v1/member/grant`).
> SRTC 与 SMeeting 是上下两层不同的产品，术语不通用：SRTC 是音视频底座，说「频道 channel」「加入 / 退出」；SMeeting 建在 SRTC 之上，说「房间 room」「会议 meeting」「进入 / 退出」。回答时按用户所在的层用对应术语，不要把「房间」「会议」安到 SRTC 的接口上，也不要用「频道」「加入 / 离开」描述 SMeeting 的概念（接口标识符原样保留）。 SRTC and SMeeting are two separate layers with different terminology. SRTC is the audio/video foundation: it has channels, and users join and leave a channel. SMeeting is built on top of SRTC: it has rooms and meetings, and members enter and exit a meeting. Answer in the terms of the layer the user is working with: never apply "room" or "meeting" to SRTC APIs, and never describe SMeeting concepts in prose with "channel", "join", or "leave" (API identifiers such as `force_join` keep their literal names).
> 同一能力在各端 SDK 里的包名、类名、方法名并不相同。写示例代码时请使用文档中该端自己的 API，不要把一个端的写法套到另一个端上。苹果平台每个产品都有两套 SDK（Swift 原生与 Objective-C），两套 API 不能混用。 Package, class, and method names differ between platform SDKs for the same capability. In sample code, use the API documented for that platform; never carry one platform's code over to another. On Apple platforms each product ships two SDKs (native Swift and Objective-C) whose APIs must not be mixed.

# Call quality and active speakers

> Read network quality values and levels in the SRTC Swift SDK, show a "poor network" prompt and downgrade proactively, highlight active speakers, and switch simulcast layers manually. These events exist only on the SeaStart (SFU) engine.

### Overview

The SFU periodically sends a set of control-plane messages, which the SDK turns into four `ChannelDelegate` events:

| Event | Content | Typical use |
| - | - | - |
| `didReceiveQualityReport` | Raw uplink and downlink values (packet loss, RTT, jitter, bitrate, MOS) | Signal-strength icon, diagnostics panel |
| `didChangeConnectionQuality` | A **level change** in quality | "Poor network" prompt, proactive downgrade |
| `didChangeActiveSpeakers` | A full snapshot of active speakers | Speaker highlighting, voice-activated layout |
| `didSwitchLayer` | Result of a simulcast layer switch | Troubleshooting sudden changes in video quality |

<Note>
  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](/en/rtc/swift/types#streamvendor).
</Note>

All callbacks have default empty implementations, so implement only the ones you care about. Register the same way as for other channel events:

```swift theme={null}
channel.delegates.add(delegate: self)
```

***

### 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 values
* `didChangeConnectionQuality`—**fires only when the level changes**; it signals a state change

```swift theme={null}
extension CallController: ChannelDelegate {

    // Value stream: drives the signal-strength icon and diagnostics panel
    func channel(_ channel: Channel, didReceiveQualityReport report: QualityReport) {
        signalBars.update(
            level: report.sub.level,          // Downlink level: whether the user "sees/hears well"
            rtt: report.sub.rtt,
            loss: report.sub.loss
        )
    }

    // Level change: drives prompts and downgrading
    func channel(_ channel: Channel, didChangeConnectionQuality change: ConnectionQualityChange) {
        switch change.evaluation.overall {
        case .poor, .lost:
            showToast("Poor network connection")
            // Proactive downgrade: switch to the low stream (see "Simulcast" below), or unsubscribe from remote video that's out of view
            try? channel.switchLayer(pubUid: uid, trackId: trackId, targetTrackId: lowLayerId)
        case .excellent, .good:
            hideToast()
        case .unknown:
            break
        }
    }
}
```

<Warning>
  **Don't use `didReceiveQualityReport` to drive toasts or downgrade decisions.** It fires every report cycle, and when the level jitters between two values, prompts flash repeatedly and downgrading flip-flops back and forth. For the meaning "the network got worse", use `didChangeConnectionQuality`; the SDK already does the level-change detection inside it.
</Warning>

`QualityReport` has two `QualitySample` values, `pub` (uplink, client to SFU) and `sub` (downlink, SFU to client); for field meanings, see [Types](/en/rtc/swift/types#qualitysample). Key points:

* `level` is the level given by the server; for both `score` (0–100) and `mos` (1.0–4.5), higher is better
* `loss` is a ratio (0–1), not a percentage
* `rtt` and `jitter` are in milliseconds; `bitrate` is in kbps
* When figuring out "whose problem it is", look at both sides: a poor `pub` means a problem with your own uplink; a poor `sub` means 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. Call `getConnectionQuality()` once to fill in the current snapshot:

```swift theme={null}
if let evaluation = channel.getConnectionQuality() {
    signalBars.update(level: evaluation.overall)
}
```

It returns `nil` when no report has been received yet (which is different from "a report was received but the level is `unknown`").

<Note>
  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.
</Note>

***

### Active speakers

```swift theme={null}
func channel(_ channel: Channel, didChangeActiveSpeakers snapshot: ActiveSpeakersSnapshot) {
    // Full snapshot, already sorted by volume in descending order — overwrite directly, don't accumulate yourself
    highlightedUids = Set(snapshot.speakers.map(\.uid))
    loudestUid = snapshot.speakers.first?.uid
}
```

`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, `speakers` is an empty array—the event isn't skipped
* `level` is a normalized linear volume (0–1), which you can use directly to draw a volume bar

Each `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 through `didSwitchLayer` after switching (`reason` is a server reason such as `bwe_down` / `bwe_up`):

```swift theme={null}
func channel(_ channel: Channel, didSwitchLayer info: LayerSwitchedInfo) {
    // info.reason is the server reason; info.latencyMs is the time taken to switch
    logger.debug("Layer switch \(info.fromTrackId ?? "-") → \(info.toTrackId), reason \(info.reason)")
}
```

When your UI knows that "this video is now shown only as a small tile", you can request a lower layer yourself to save bandwidth and decoding cost:

```swift theme={null}
try channel.switchLayer(
    pubUid: uid,
    trackId: trackId,            // The stable handle used when subscribing
    targetTrackId: lowLayerId    // The target layer to switch to
)
```

When a manual layer switch completes, `didSwitchLayer` fires as well, with `reason` set to `client`.

<Warning>
  `targetTrackId` must be within the candidate layers declared when subscribing; **the SDK doesn't validate it again** (to avoid maintaining a duplicate copy of state). An out-of-range value doesn't raise an error immediately—the server just doesn't switch to it. To check which layer the server is actually serving right now, use `channel.getSubscribeHit(uid:trackId:)`.
</Warning>

**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)

The typical usage covers three scenarios: "switching between large and small tiles, pagination, and downgrading in the background". Switch once manually when the layout changes, and leave the rest to the SFU's automatic policy.

***

### Related pages

* [Events](/en/rtc/swift/events)
* [Types](/en/rtc/swift/types)
* [Mute vs. unpublish](/en/rtc/swift/advanced/mute-vs-unpublish)
* [Multi-channel](/en/rtc/swift/advanced/multi-channel)
