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

# Instance and channel

> C SDK instance lifecycle (rtc_create / rtc_destroy and when it is safe to free callback context), joining and leaving channels, rtc_get_last_error, auto-subscribe, log level, and every callback registration function with its threading rules.

This page covers creating and destroying SDK instances, joining and leaving channels, and every callback registration function.

All functions are thread-safe.

***

## Logging

### rtc\_set\_log\_level

```c theme={null}
void rtc_set_log_level(int level);
```

Sets the global log level. It applies to the whole process; we recommend calling it before `rtc_create`.

| Parameter | Description |
| - | - |
| `level` | `RTC_LOG_DEBUG`(0) / `RTC_LOG_INFO`(1) / `RTC_LOG_WARN`(2) / `RTC_LOG_ERROR`(3); any other value is treated as `INFO` |

***

## Instance lifecycle

### rtc\_create

```c theme={null}
void* rtc_create();
```

Creates an SDK instance and returns the instance handle. Every channel-related function afterward takes this handle.

One handle corresponds to one channel connection. To connect to multiple channels at the same time, create multiple instances.

### rtc\_destroy

```c theme={null}
void rtc_destroy(void* handle);
```

Destroys the instance and releases its resources. **Starting with 0.0.11**, `rtc_destroy` waits for all running callbacks to return before it returns. After it returns, no further callbacks fire for this instance, and you can safely free the `context` you passed to the callbacks.

* You may call `rtc_destroy` from within one of this instance's callbacks (including the disconnect callback). In that case it only waits for callbacks on other threads—**don't free the current callback's `context` until that callback returns**
* If a callback does slow work (decoding, writing files), `rtc_destroy` waits correspondingly longer
* Neither `rtc_leave_channel` nor replacing a callback with `rtc_set_*_callback` waits for running callbacks, so neither is a safe point to free `context`

<Warning>
  **0.0.10 and earlier don't provide these guarantees**: callbacks may still be running when `rtc_destroy` returns, and calling `rtc_destroy` from the disconnect callback deadlocks. With these older versions, don't free `context` right after `rtc_destroy`, and don't destroy the instance in the disconnect callback; we recommend upgrading to 0.0.11.
</Warning>

<Warning>
  You must call `rtc_destroy`; otherwise the instance's resources are not released. The handle can't be used after it's destroyed.
</Warning>

***

## Joining and leaving a channel

### rtc\_join\_channel

```c theme={null}
int rtc_join_channel(void* handle, const char* token);
```

Joins a channel asynchronously and returns immediately. The actual connection result is reported through the connection state callback (`rtc_set_connection_callback`).

| Parameter | Description |
| - | - |
| `token` | Channel join token issued by the server; see [Server API · Get a channel join token](/en/rtc/server-api/channel) |

**Returns**

| Return value | Meaning |
| - | - |
| `RTC_OK` | The join request was initiated (doesn't mean the connection succeeded) |
| `RTC_INVALID_PARAM` | Invalid handle, or the token is an empty string |

### rtc\_join\_channel\_sync

```c theme={null}
int rtc_join_channel_sync(void* handle, const char* token, int timeout_ms);
```

Joins a channel synchronously, blocking until the connection succeeds, fails, or times out.

| Parameter | Description |
| - | - |
| `token` | Channel join token issued by the server |
| `timeout_ms` | Timeout in milliseconds. On timeout, this join attempt is fully aborted, leaving no background tasks behind |

**Returns**

| Return value | Meaning |
| - | - |
| `RTC_OK` | Joined successfully |
| `RTC_INVALID_PARAM` | Invalid handle, or the token is an empty string |
| `RTC_ERROR` | Join failed (token invalid, session already taken, rejected by the server, etc.); get the specific reason with `rtc_get_last_error` |
| `RTC_TIMEOUT` | Didn't complete before the timeout |

### rtc\_get\_last\_error

```c theme={null}
int rtc_get_last_error(void* handle, char* msg_buf, int buf_len);
```

Gets the error details of this instance's **most recent failed call** (since 0.0.9). Call it after join, subscribe, publish, or similar functions return `RTC_ERROR` / `RTC_TIMEOUT`.

| Parameter | Description |
| - | - |
| `msg_buf` / `buf_len` | Optional. If provided, the error reason is written into it (truncated if too long, always `\0`-terminated); pass `NULL, 0` if you don't need the reason |

**Returns:** The error code. `180xxx` is an SDK error, `≥1000` is a server error code, `-1` is an internal error with no specific code, and `0` means nothing was recorded.

```c theme={null}
if (rtc_join_channel_sync(rtc, token, 10000) != RTC_OK) {
    char msg[256];
    int code = rtc_get_last_error(rtc, msg, sizeof(msg));
    fprintf(stderr, "join failed: %d %s\n", code, msg);    // e.g. 1033 concurrency limit reached
}
```

<Note>
  Each instance keeps only the most recent error. When multiple threads call functions on the same instance concurrently, a later failure overwrites an earlier one.
</Note>

### rtc\_leave\_channel

```c theme={null}
int rtc_leave_channel(void* handle);
```

Leaves the channel. The instance stays valid after leaving and can join again. When you're done with it for good, you still need to call `rtc_destroy`.

**Returns:** `RTC_OK` / `RTC_INVALID_PARAM` (invalid handle).

***

## Auto-subscribe

### rtc\_set\_auto\_subscribe

```c theme={null}
void rtc_set_auto_subscribe(void* handle, int auto_audio, int auto_video);
```

Sets whether to automatically subscribe to remote tracks. When on, every already-published and newly published track of the corresponding type in the channel is subscribed automatically, and all data goes through the track data callback.

| Parameter | Description |
| - | - |
| `auto_audio` | 1 = auto-subscribe to all audio, 0 = don't |
| `auto_video` | 1 = auto-subscribe to all video, 0 = don't |

<Warning>
  Must be called **before** joining the channel. Setting it after joining has no effect on existing tracks.
</Warning>

<Tip>
  If a user wants to "hear everyone," the right approach is `rtc_set_auto_subscribe(rtc, 1, 0)` to subscribe to everyone's audio and then mix it yourself. **Don't** subscribe to the audio composite stream—it includes your own voice and causes echo.
</Tip>

***

## Callback registration

Every callback carries your context through the `context` parameter; the SDK passes it back unchanged without interpreting it.

<Warning>
  `context` must be a **real pointer** (or `NULL`). Don't cast small integers like `1` or `2` to pointers to use as IDs: the SDK stores it internally as a pointer, and a value below 4096 is treated as an invalid pointer and terminates the process immediately. If you need an ID, pass the address of memory that holds it.
</Warning>

<Warning>
  **Callbacks run on internal SDK threads.** Keep three things in mind:

  1. Different callbacks can fire concurrently, so protect shared state yourself
  2. Don't do slow work in callbacks, or you'll block the SDK event loop; queue slow processing and hand it to your own threads
  3. Pointers in callback parameters (`data`, `props`, `content_json`, `speakers`, etc.) are **only valid during the callback**; copy them right away if you need to keep them
</Warning>

### rtc\_set\_connection\_callback

```c theme={null}
typedef void (*rtc_connection_callback)(void* context, int state);
void rtc_set_connection_callback(void* handle, rtc_connection_callback callback, void* context);
```

Connection state changes. `state`: `0`=connecting, `1`=connected, `2`=disconnected, `3`=reconnecting.

### rtc\_set\_disconnected\_callback

```c theme={null}
typedef void (*rtc_disconnected_callback)(void* context, int reason, int code, const char* msg);
void rtc_set_disconnected_callback(void* handle, rtc_disconnected_callback callback, void* context);
```

Fires once when the instance finally leaves the channel (at the same moment as `state=2` in the connection state callback, and before it), with the disconnect reason (since 0.0.9).

| Parameter | Description |
| - | - |
| `reason` | Disconnect reason `RTC_DISCONNECT_*`; see [Types · Callback parameter enums](/en/rtc/capi/types#callback-parameter-enums) |
| `code` | The error code if there was an error (`180xxx` or a server `1xxx`; see [Error codes](/en/rtc/capi/error-codes)), otherwise `0` |
| `msg` | The error reason; `NULL` if there was no error. Only valid during the callback |

<Tip>
  Use it to decide whether to rejoin automatically: don't rejoin automatically when removed from the channel (`KICKED`), replaced by another session with the same uid (`REPLACE`), or when the channel is destroyed (`DESTROY`); in cases such as a heartbeat timeout (`TIMEOUT`), you can rejoin with a newly issued token.
</Tip>

### rtc\_set\_user\_event\_callback

```c theme={null}
typedef void (*rtc_user_event_callback)(void* context, const char* uid, int event_type);
void rtc_set_user_event_callback(void* handle, rtc_user_event_callback callback, void* context);
```

Users joining and leaving the channel. `event_type`: `0`=joined, `1`=left.

### rtc\_set\_track\_event\_callback

```c theme={null}
typedef void (*rtc_track_event_callback)(void* context, const char* uid,
                                         rtc_track_info_t* track_info, int event_type);
void rtc_set_track_event_callback(void* handle, rtc_track_event_callback callback, void* context);
```

Remote tracks added, updated, or removed. `event_type`: `0`=added, `1`=updated, `2`=removed. With manual subscription, use this callback to discover tracks you can subscribe to.

### rtc\_set\_track\_sample\_callback

```c theme={null}
typedef void (*rtc_track_sample_callback)(void* context,
                                          rtc_user_info_t* user_info,
                                          rtc_track_info_t* track_info,
                                          uint8_t* data, int len,
                                          int64_t timestamp, int64_t duration);
void rtc_set_track_sample_callback(void* handle, rtc_track_sample_callback callback, void* context);
```

Media data for subscribed tracks. Every subscribed track (including composite streams) comes out of this single callback; use `user_info->uid` + `track_info->track_id` to tell the sources apart.

`data` is a complete encoded frame (one frame for video, one encoded packet for audio). The pointer is only valid during the callback:

```c theme={null}
uint8_t* saved = malloc(len);
memcpy(saved, data, len);   // Copy it if you need to keep it
```

### rtc\_set\_custom\_msg\_callback

```c theme={null}
typedef void (*rtc_custom_msg_callback)(void* context, const rtc_custom_msg_t* msg);
void rtc_set_custom_msg_callback(void* handle, rtc_custom_msg_callback callback, void* context);
```

In-channel custom messages; see [Custom messages](/en/rtc/capi/advanced/custom-msg).

### SeaStart-only callbacks

The following three callbacks only fire when the channel uses the SeaStart engine; with other engines they're never called. See [SeaStart advanced features](/en/rtc/capi/advanced/seastart).

```c theme={null}
void rtc_set_layer_switched_callback(void* handle, rtc_layer_switched_callback callback, void* context);
void rtc_set_connection_quality_callback(void* handle, rtc_connection_quality_callback callback, void* context);
void rtc_set_active_speakers_callback(void* handle, rtc_active_speakers_callback callback, void* context);
```

***

## Typical call order

```c theme={null}
rtc_set_log_level(RTC_LOG_INFO);

void* rtc = rtc_create();

// 1) Register callbacks first
rtc_set_connection_callback(rtc, on_conn, ctx);
rtc_set_user_event_callback(rtc, on_user, ctx);
rtc_set_track_event_callback(rtc, on_track, ctx);
rtc_set_track_sample_callback(rtc, on_sample, ctx);

// 2) Then configure auto-subscribe
rtc_set_auto_subscribe(rtc, 1, 1);

// 3) Finally join the channel
rtc_join_channel_sync(rtc, token, 10000);

// ... application runs ...

rtc_leave_channel(rtc);
rtc_destroy(rtc);
```
