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

# Multi-channel

> Join multiple channels at once with one Android RTCEngine: per-channel RTCChannel handles and callbacks, shared camera/microphone capture published per channel, per-channel subscription, leaving and releasing, and error handling. Read when your app must be in several channels at once.

The Android SRTC SDK lets a single `RTCEngine` join multiple channels at the same time within one initialization cycle. Each `join(...)` returns an `RTCChannel` handle. Local camera, microphone, and screen capture are shared by the Engine, while each channel's signaling, media streaming session, users, remote tracks, and statistics are independent of one another.

## Objects and scopes

| Object or capability | Scope | Description |
| - | - | - |
| `RTCEngine` | App / SDK initialization cycle | Manages the SDK lifecycle, IM, shared capture devices, and all channels. |
| First `RTCChannel` | Default channel | The flat channel APIs on `RTCEngine` delegate to it. |
| Subsequent `RTCChannel` | A single additional channel | Publishing, subscription, queries, statistics, and leaving affect only that channel. |
| Local Camera / Mic / Screen Track | Shared by the Engine | Start capture only once; the same track can be published to multiple channels. |
| `RTCClientEvent` / `RTCMediaEvent` | A single channel | Callbacks still carry the `channel` parameter, so you can reuse listeners and verify which channel an event belongs to. |
| Device events, PCM / local video frame events | Engine-global | Joining multiple channels doesn't produce duplicate device-level callbacks. |

:::note
The first `join` is the "default channel", not the first channel to reach `onJoinSucceed`. After the default channel is left, the next newly created channel can take the default slot. Multi-channel apps should always keep and use the `RTCChannel` returned by each `join`, rather than relying on the flat APIs to infer the target channel.
:::

## Integration flow

### 1. Create the Engine

Multiple channels need only one `RTCEngine`:

```kotlin theme={null}
val rtcEngine = RTCEngine.create(
    app = application,
    enableLocalLog = true,
    engineEvent = object : RTCEngineSimpleEvent() {
        override fun onError(channelId: String?, errorCode: Int, message: String?) {
            // When channelId has a value, route to the corresponding channel; null means an Engine-global error
        }
    }
)
rtcEngine.initSDK()
```

### 2. Start shared capture

Capture and per-channel publishing are independent. The following tracks are shared within the Engine and started only once:

```kotlin theme={null}
val cameraTrack = rtcEngine.getLocalCameraTrack(PreOptionCamera._720P)
val micTrack = rtcEngine.getLocalMicTrack(PreOptionMic.def)

cameraTrack.startCapture(object : RTCResultListener {
    override fun onSuccess() {
        // Record that the camera is ready
    }
    override fun onFail(code: Int) {
        // Handle camera start failure
    }
})
micTrack.startCapture(object : RTCResultListener {
    override fun onSuccess() {
        // Record that the microphone is ready
    }
    override fun onFail(code: Int) {
        // Handle microphone start failure
    }
})
```

Wait for each `RTCResultListener.onSuccess()` before publishing. Setting a local audio frame listener alone doesn't open the microphone; if you need PCM, you still have to call `micTrack.startCapture(...)`.

### 3. Create callbacks for each channel and join

The initial `RTCClientEvent` must be passed in through this `join(...)` call, because the join success or failure event may follow immediately. `RTCChannel.setRtcClientEvent(...)` is only for replacing or unbinding the listener after the join succeeds.

```kotlin theme={null}
private val channels = mutableMapOf<String, RTCChannel>()

private fun joinOneChannel(key: String, activity: Activity, token: String) {
    val clientEvent = object : RTCClientSimpleEvent() {
        override fun onJoinSucceed(channel: String, uid: String, whiteBoard: String?) {
            val rtcChannel = channels[key] ?: return

            // Publish the same shared capture tracks to each channel separately
            rtcChannel.publishLocalVideo(
                cameraTrack,
                PublishCustomOptions(TrackDesc.TRACK_MAIN.value, null, null),
                null
            )
            rtcChannel.publishLocalAudio(
                micTrack,
                PublishCustomOptions(TrackDesc.TRACK_AUDIO.value, null, null),
                null
            )
        }

        override fun onJoinFailed(channel: String?, statusCode: Int) {
            channels.remove(key)
        }

        override fun onStreamTrackAdd(
            uid: String,
            channel: String,
            trackId: String,
            trackDesc: String
        ) {
            subscribeVideo(key, uid, trackId, trackDesc)
        }

        override fun onDisconnected(
            channel: String,
            leaveReason: LeaveReason,
            statusCode: Int,
            message: String
        ) {
            // Clean up only the UI and app state for this channel
        }
    }

    val rtcChannel = rtcEngine.join(
        activity = activity,
        token = token,
        clientEvent = clientEvent,
        options = JoinOptions(autoSubscribeAudio = true, autoSubscribeVideo = false)
    ) ?: return

    channels[key] = rtcChannel
    rtcChannel.setRtcMediaEvent(object : RTCMediaSimpleEvent() {
        override fun onMediaConnected(channel: String) {
            // Media connection for this channel succeeded
        }

        override fun onMediaMetric(channel: String, metric: MediaMetric.Metric) {
            // Each channel has its own statistics snapshot
        }
    })
}
```

The SDK guarantees that `onJoinSucceed(...)` is dispatched only after `join(...)` returns. A non-null handle only means the request was accepted; channel operations such as publishing and subscribing called before the success callback are blocked with `CHANNEL_NOT_START`.

### 4. Subscribe and render per channel

Remote tracks must be obtained and subscribed through the same `RTCChannel` that produced the event:

```kotlin theme={null}
private fun subscribeVideo(
    key: String,
    uid: String,
    trackId: String,
    trackDesc: String
) {
    val rtcChannel = channels[key] ?: return
    val remoteTrack = rtcChannel.getRemoteVideoTrack(uid, trackDesc)
    remoteTrack?.addPlayView(remoteViewFor(key, uid, trackDesc))
    rtcChannel.subscribeRemoteTrack(uid, trackId, null, null)
}
```

Don't take a `uid` / `trackId` from channel A and query or subscribe with it on channel B's handle. Even if the strings are identical, users, tracks, and media state are not shared between the two channel sessions.

## Shared capture and per-channel publishing

Capture, publishing, and muting are three different layers:

| Operation | Scope | Typical use |
| - | - | - |
| `track.startCapture(...)` / `stopCapture()` | The data source shared by all channels | Open or close the physical device. |
| `channel.publishLocalAudio/Video(...)` | The current channel | Decide whether to send the shared capture data into that channel. |
| `channel.enableLocalAudio(track, false)` | Published audio in the current channel | Frequent muting without removing the remote track. |
| `channel.unPublishLocalAudio/Video(...)` | The current channel | Stop publishing to that channel without closing shared capture. |

So unpublishing in channel A doesn't affect channel B, but calling `micTrack.stopCapture()` or `cameraTrack.stopCapture()` closes the shared data source, and every channel still publishing that track loses its capture data.

## Leaving and releasing

To leave a single channel, call its handle; other channels are unaffected:

```kotlin theme={null}
channels.remove("channel-a")?.leave()
```

When you're done with everything, first unpublish and leave channel by channel, then close shared capture, and finally release the Engine:

```kotlin theme={null}
channels.values.toList().forEach { channel ->
    channel.unPublishLocalAudio(micTrack, null)
    channel.unPublishLocalVideo(cameraTrack, null)
    channel.leave()
}
channels.clear()

micTrack.stopCapture()
cameraTrack.stopCapture()
rtcEngine.releaseSDK()
```

`releaseSDK()` releases all channels and shared resources in the current initialization cycle as a fallback. To use the SDK again, call `initSDK()` again.

## Failures and error handling

* `join(...)` returns `null`: the request was rejected before the channel session was created; the status code from this call's `onJoinFailed(...)` is still authoritative.
* Joining the same channel twice: `onJoinFailed(channel, RtcChannelErrorCode.CHANNEL_ALREADY_EXISTS)` (`102208`); the existing channel and listener are unaffected.
* SDK not initialized or already released: `join(...)` synchronously throws `SdkNotInitializedException`.
* Asynchronous operations that fail after joining: reported through each operation's `RTCResultListener.onFail(code)`.
* Operations blocked by the Engine or global errors: reported through `RTCEngineEvent.onError(channelId, errorCode, message)`.
* Channel disconnects and reconnects: distinguished by `onDisconnected`, `onReconnecting`, and `onReconnected`, which carry the `channel` parameter.

For error code ownership and the `102xxx` domain constants, see [Error codes](/en/rtc/android/error-codes). For the full channel API, see [RTCChannel](/en/rtc/android/api-reference/RTCChannel).
