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

# Video rendering

> Render local video, remote video, and the server-side MCU composite in SwiftUI and UIKit / AppKit with the SMeeting Swift SDK, and decide when to subscribe to remote video. Read when building the meeting's video grid or managing subscriptions yourself.

### Overview

Meeting video comes in three kinds, each with its own entry point:

| Video | SwiftUI | UIKit / AppKit |
| - | - | - |
| Local camera / sharing | `SRTCVideoView(track:)` | Pass an `SRTCVideoRenderer` when turning it on |
| Remote camera / sharing | `SMeetingRemoteVideoView` | `startPlayRemoteVideo(view:uid:trackDesc:)` |
| Server-side composite (MCU) | Get `meeting.mcuTrack` and pass it to `SRTCVideoView` | `startPlayRemoteVideoMcu(view:uid:)` |

Local video **doesn't need a subscription**; just render the track directly. Remote video **must be subscribed first** before it has any data.

***

### Local video

#### SwiftUI

Don't pass `view` when turning on the camera; afterward, pass the track directly to `SRTCVideoView`:

```swift theme={null}
try await meeting.requestOpenCamera()

if let cameraTrack = meeting.cameraTrack {
    SRTCVideoView(track: cameraTrack)
        .frame(height: 180)
}
```

Screen sharing works the same way, using `meeting.screenTrack`:

```swift theme={null}
if let screenTrack = meeting.screenTrack {
    SRTCVideoView(track: screenTrack)
}
```

> `SMeetingEngine` is not an `ObservableObject`, so `cameraTrack` / `screenTrack` changing from `nil` to a value doesn't trigger a SwiftUI refresh by itself. Keep a "camera is on" flag in your own `@Published` state and let it drive view updates.

#### UIKit / AppKit

Pass the preview view when turning on the camera:

```swift theme={null}
let renderer = SRTCVideoRenderer(frame: previewFrame)
containerView.addSubview(renderer)

try await meeting.requestOpenCamera(view: renderer)
```

> The `view` you pass must be an object that is actually attached to the view hierarchy. Passing a temporarily created local variable that hasn't been added to any superview leaves the rendering pipeline running with nothing to show.

***

### Remote video (SwiftUI)

`SMeetingRemoteVideoView` is the recommended entry point for SwiftUI. It handles the entire subscription lifecycle: it subscribes by `uid + trackDesc` when the view appears and unsubscribes when the view disappears.

```swift theme={null}
SMeetingRemoteVideoView(
    meeting: meeting,
    uid: user.uid,
    trackDesc: .cameraBig
)
.frame(height: 180)
```

To view someone's screen sharing, change `trackDesc` to `.screen`:

```swift theme={null}
SMeetingRemoteVideoView(meeting: meeting, uid: user.uid, trackDesc: .screen)
```

What the component guarantees:

* Rendering the same `(uid, trackDesc)` in multiple places doesn't make them knock out each other's subscriptions
* When a layout change recreates the view (the old instance disappears and a new one appears), no extra unsubscribe + resubscribe happens, so the video doesn't flicker
* The subscription result and the arrival of the underlying media don't happen at the same moment; the component binds automatically once the data actually arrives, so you don't need delays or retries

A typical usage is rendering a grid from the member list:

```swift theme={null}
ForEach(users, id: \.uid) { user in
    if user.uid == meeting.currentUserId {
        if let track = meeting.cameraTrack {
            SRTCVideoView(track: track)
        }
    } else if user.shareState == ShareType.screen.rawValue {
        SMeetingRemoteVideoView(meeting: meeting, uid: user.uid, trackDesc: .screen)
    } else if user.cameraState == .on {
        SMeetingRemoteVideoView(meeting: meeting, uid: user.uid, trackDesc: .cameraBig)
    }
}
```

***

### Remote video (UIKit / AppKit)

If you already hold an `SRTCVideoRenderer` attached to the view hierarchy, use this pair of convenience methods:

```swift theme={null}
let renderer = SRTCVideoRenderer(frame: frame)
containerView.addSubview(renderer)

try await meeting.startPlayRemoteVideo(
    view: renderer,
    uid: remoteUid,
    trackDesc: .cameraBig
)

// When no longer needed
try await meeting.stopPlayRemoteVideo(
    view: renderer,
    uid: remoteUid,
    trackDesc: .cameraBig
)
```

`stopPlayRemoteVideo` removes only the one renderer view you pass in; it actually unsubscribes only when the track no longer has any renderer view. So when the same video is shown in several windows, closing one of them doesn't affect the others.

***

### Control subscriptions yourself

When you need to separate subscription timing from rendering timing (for example, pre-subscribe and then decide the layout), use the core APIs:

```swift theme={null}
let track = try await meeting.subscribeRemoteVideoTrack(uid: remoteUid, trackDesc: .cameraBig)

// SwiftUI: SRTCVideoView(track: track)
// UIKit / AppKit: track.addRenderer(renderer)

try await meeting.unsubscribeRemoteVideoTrack(uid: remoteUid, trackDesc: .cameraBig)
```

Note that `unsubscribeRemoteVideoTrack` **unsubscribes unconditionally**, regardless of whether any renderer view is still using it. When rendering the same video in multiple places, use `SMeetingRemoteVideoView` or `stopPlayRemoteVideo`.

To just check whether a given track of a given member exists (without subscribing), use:

```swift theme={null}
let track = meeting.getRemoteVideoTrack(uid: remoteUid, desc: .cameraBig)
```

***

### When to subscribe

Remote video is **subscribed on demand**; the SDK doesn't subscribe to everyone automatically for you. Base the decision on member state and events:

* `MeetingUserInfo.cameraState == .on` → this member has camera video
* `MeetingUserInfo.shareState == ShareType.screen.rawValue` → this member is sharing their screen
* Refresh the layout when you receive `userCameraStateDidChange` or `roomShareDidStart` / `roomShareDidStop`

With `SMeetingRemoteVideoView`, you only need to make the views appear / disappear along with these states, and the subscriptions follow automatically.

***

### Server-side composite video (MCU)

When the meeting has a server-side composite task running, you can pull a single composite video instead of subscribing to members one by one:

```swift theme={null}
let track = try await meeting.startPlayRemoteVideoMcu(view: renderer, uid: mcuUid)
// You can also get this track at any time through meeting.mcuTrack

try await meeting.stopPlayRemoteVideoMcu(view: renderer)
```

The composite video requires the server to set up the composite task first; the layout is controlled by `adminUpdateLayout(_:)`. See [Recording and composite layout](/en/meeting/swift/advanced/recording).

***

### Related pages

* [Media control](/en/meeting/swift/advanced/media-control)
* [Screen sharing](/en/meeting/swift/advanced/screen-sharing)
* [API reference - SMeetingRemoteVideoView](/en/meeting/swift/api-reference/SMeetingRemoteVideoView)
