> ## 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.

# Events

> Full list of Web SDK events and when they fire: in-channel events via onNotifyChannelEvent (channel and connection, remote users and tracks, tracks and devices, network quality and active speakers) and out-of-channel IM events via onNotifyImEvent, with string values and data types.

### In-channel events (onNotifyChannelEvent)

Register a callback with `srtc.onNotifyChannelEvent`; we recommend setting it before `join`:

```typescript theme={null}
srtc.onNotifyChannelEvent = (evt: ChannelEvent) => {
  switch (evt.type) {
    case ChannelEventType.USER_JOIN:
      // evt.data
      break;
    case ChannelEventType.TRACK_PIP_ENTER:
      // evt.data
      break;
  }
};
```

`ChannelEvent` and `ImEvent` are both discriminated unions at the type level: when you branch with `switch (evt.type)`, `evt.data` is automatically narrowed to the data structure for that event type. If you only use them for type annotations in TypeScript, we recommend importing them with `import type`.

***

#### Channel and connection

| Event constant | String value | When it fires | `data` type |
| - | - | - | - |
| `JOIN_SUCCEED` | `'join_succeed'` | Joined the channel successfully for the first time | `ChannelInfo` |
| `CHANNEL_UPDATE` | `'channel_update'` | The channel's custom properties `props` were updated | `ChannelInfo` |
| `ME_UPDATE` | `'me_update'` | Your own user info was updated by the server | `UserInfo` |
| `RECONNECTING` | `'reconnecting'` | Network fluctuation; automatic reconnection started | None |
| `RECONNECTED` | `'reconnected'` | Automatic reconnection succeeded | None |
| `DISCONNECTED` | `'disconnected'` | Forcibly removed from the channel, or an unrecoverable error occurred | `DisconnectEventData` |
| `CUSTOM_MSG` | `'custom_msg'` | Received an in-channel custom message | `CustomMsgData` |

***

#### Remote users and tracks

| Event constant | String value | When it fires | `data` type |
| - | - | - | - |
| `USER_JOIN` | `'user_join'` | A remote user joined the channel | `UserInfo` |
| `USER_UPDATE` | `'user_update'` | A remote user's info was updated | `UserInfo` |
| `USER_LEAVE` | `'user_leave'` | A remote user left the channel | `UserLeaveEventData` |
| `USER_TRACK_ADD` | `'user_track_add'` | A remote user published a new track | `{ user: UserInfo, track: TrackInfo }` |
| `USER_TRACK_UPDATE` | `'user_track_update'` | A remote user updated track info | `{ user: UserInfo, track: TrackInfo }` |
| `USER_TRACK_REMOVE` | `'user_track_remove'` | A remote user unpublished a track | `{ user: UserInfo, track: TrackInfo }` |

> When a remote user leaves, the SDK automatically unsubscribes from all of that user's tracks; on `USER_TRACK_REMOVE`, it automatically unsubscribes from the corresponding track.

***

#### Tracks and devices

| Event constant | String value | When it fires | `data` type |
| - | - | - | - |
| `TRACK_MUTED` | `'track_muted'` | A track paused sending data | `BaseTrack` |
| `TRACK_UNMUTED` | `'track_unmuted'` | A track resumed sending data | `BaseTrack` |
| `TRACK_ENDED` | `'track_ended'` | A track stopped, e.g., a device was unplugged or the user clicked the browser's "Stop sharing" | `BaseTrack` |
| `TRACK_AUTOPLAY_FAIL` | `'track_autoplay_fail'` | The browser blocked autoplay; call `startPlay` again after a user gesture | `BaseTrack` |
| `TRACK_PIP_ENTER` | `'track_pip_enter'` | A video track entered picture-in-picture mode | `BaseTrack` |
| `TRACK_PIP_EXIT` | `'track_pip_exit'` | A video track exited picture-in-picture mode | `BaseTrack` |
| `TRACK_POPOUT_OPEN` | `'track_popout_open'` | A video track popped out to a separate window | `BaseTrack` |
| `TRACK_POPOUT_CLOSE` | `'track_popout_close'` | A video track closed its separate window | `BaseTrack` |
| `DEVICE_ADD` | `'device_add'` | A device was plugged in | `MediaDeviceInfo` |
| `DEVICE_REMOVE` | `'device_remove'` | A device was unplugged | `MediaDeviceInfo` |

***

#### Network quality and active speakers

| Event constant | String value | When it fires | `data` type |
| - | - | - | - |
| `CONNECTION_QUALITY_CHANGED` | `'connection_quality_changed'` | The connection quality level changed | `ConnectionQualityEventData` |
| `CPU_CONSTRAINED` | `'cpu_constrained'` | The sender is persistently CPU-limited | `QualityEvaluation` |
| `BANDWIDTH_CONSTRAINED` | `'bandwidth_constrained'` | The sender is persistently bandwidth-limited | `QualityEvaluation` |
| `ACTIVE_SPEAKERS_CHANGED` | `'active_speakers_changed'` | The current active speaker list changed; only supported by the SeaStart SFU | `ActiveSpeakersEventData` |

`data.speakers` of `ACTIVE_SPEAKERS_CHANGED` is a full snapshot of the users currently speaking, sorted by `level` from high to low; when nobody is speaking it's an empty array. Your app can overwrite the UI state directly, without merging increments yourself.

***

### Out-of-channel message events (onNotifyImEvent)

Register a callback with `srtc.onNotifyImEvent`; call `srtc.enableIm(token)` first:

```typescript theme={null}
srtc.onNotifyImEvent = (evt: ImEvent) => {
  switch (evt.type) {
    case ImEventType.IM_MSG:
      // evt.data
      break;
  }
};
```

| Event constant | String value | When it fires | `data` type |
| - | - | - | - |
| `ENABLE_SUCCEED` | `'enable_succeed'` | IM connected successfully for the first time | None |
| `IM_MSG` | `'im_msg'` | Received an IM message | `ImMsgData` |
| `RECONNECTING` | `'reconnecting'` | IM connection dropped; reconnection started | None |
| `RECONNECTED` | `'reconnected'` | IM reconnected successfully | None |
| `DISCONNECTED` | `'disconnected'` | IM was forcibly disconnected | `ImDisconnectEventData` |
