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

# Local recording

> Record the current user's call view in the browser with LocalCompositeRecorder: composite video tiles you lay out yourself plus mixed audio into one webm file, start options and video item fields, updating the view, pause/resume, chunked upload, state, and cleanup.

### Overview

Local recording suits recording "the call view the current user sees" in the browser: for example a 3-tile or 9-tile grid, the current page after paging, or a layout that prioritizes screen sharing. The SDK draws the video tracks you pass in onto an internal canvas, mixes the audio tracks into the same recording stream, and finally outputs a webm file through `MediaRecorder`. Your app decides which videos to record right now and where each one goes on the recording canvas.

`LocalCompositeRecorder` only handles media compositing and the recording lifecycle and **has no built-in call layout algorithm**—your app passes in the layout based on the current UI view.

***

### Basic usage

```typescript theme={null}
const recorder = srtc.createLocalCompositeRecorder();

await recorder.start({
  width: 1280,
  height: 720,
  fps: 15,
  videoItems: [
    {
      id: 'local-camera',
      track: localCameraTrack,
      label: 'Me',
      rect: { x: 0, y: 0, width: 640, height: 360 },
      fit: 'contain',
    },
    {
      id: 'remote-user-1-camera',
      track: remoteVideoTrack,
      label: 'Remote user',
      rect: { x: 640, y: 0, width: 640, height: 360 },
      fit: 'contain',
    },
  ],
  audioTracks: [
    localMicTrack,
    remoteAudioMixTrack,
  ],
});

// After the call view pages, the grid changes, or subscriptions change, update the current recording view.
recorder.updateVideoItems(nextVideoItems);
await recorder.updateAudioTracks(nextAudioTracks);

// Pause and resume apply to the same final file.
recorder.pause();
recorder.resume();

// Stop recording and return the complete webm Blob.
const blob = await recorder.stop();
```

Once you have the `blob`, you can download or upload it:

```typescript theme={null}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'recording.webm';
a.click();
URL.revokeObjectURL(url);
```

***

### Start options

`LocalCompositeRecorderStartOptions` for `start(options)`:

| Field | Type | Default | Description |
| - | - | - | - |
| `videoItems` | `LocalCompositeRecorderVideoItem[]` | — | Initial list of videos to draw, required |
| `audioTracks` | `LocalCompositeRecorderTrack[]` | — | Audio tracks mixed into the recording file |
| `width` | `number` | `1280` | Output video width (pixels) |
| `height` | `number` | `720` | Output video height (pixels) |
| `fps` | `number` | `15` | `canvas.captureStream` frame rate |
| `mimeType` | `string` | Auto | If omitted or unsupported, a webm format supported by the browser is chosen automatically |
| `timeslice` | `number` | — | `MediaRecorder` chunk interval (milliseconds); if omitted, everything is returned at once on `stop` |
| `background` | `string` | `#101216` | Overall background color of the recording canvas |
| `labelBackground` | `string` | `rgba(0,0,0,0.58)` | Label background color |
| `onDataAvailable` | `(blob: Blob) => void` | — | Fires each time a valid chunk is output; can be used to upload while recording |

`LocalCompositeRecorderVideoItem` (a single video item):

| Field | Type | Default | Description |
| - | - | - | - |
| `id` | `string` | — | Stable unique identifier maintained by the caller; when the track for the same id changes, the slot is reused and the decode source replaced |
| `track` | `LocalCompositeRecorderTrack \| null` | — | Video track to draw; pass `null` for an empty slot |
| `rect` | `{ x, y, width, height }` | — | Drawing area on the canvas (canvas pixel coordinates) |
| `label` | `string` | — | Label drawn in the bottom-left corner (such as the user name); not drawn if omitted |
| `avatar` | `string` | — | URL of the round avatar drawn in the center of the tile when the camera is off (no video track); loaded anonymously cross-origin, and falls back to a gray circle with the nickname's first letter if loading fails or cross-origin access is refused |
| `fit` | `'contain' \| 'cover'` | `contain` | `contain` keeps the full video, `cover` fills the area and crops the overflow |
| `background` | `string` | `#1a1c22` | Background color of a single slot |

***

### Input tracks

Both `videoItems[].track` and `audioTracks[]` accept an SDK Track or a native browser `MediaStreamTrack`:

* `LocalVideoTrack`
* `RemoteVideoTrack`
* `LocalAudioTrack`
* `RemoteAudioTrack`
* `MediaStreamTrack`

If you pass an SDK Track, the recorder listens for internal media track replacement events and automatically switches to the new track when a reconnect or resubscription changes the underlying `MediaStreamTrack`, with no action needed from your app.

***

### Layout responsibility

`LocalCompositeRecorder` has no built-in call layout algorithm. Your app should generate `videoItems` based on the current UI view:

* If only 3 users are shown right now, pass only those 3 video items.
* If it's a 9-tile grid, compute `rect` for 9 slots.
* After the user pages left or right, call `updateVideoItems(...)` to switch to the new page.
* Special layouts such as screen sharing or speaker mode also have their `rect` computed by your app.

This way the recording follows the current view passed in by your app, rather than always recording all remote streams.

***

### Audio recommendations

For call recording, we generally recommend passing in both the local microphone track and the remote mixed audio track:

```typescript theme={null}
const remoteAudioMixTrack = await srtc.subscribeRemoteAudioMixTrack();

await recorder.updateAudioTracks([
  localMicTrack,
  remoteAudioMixTrack,
]);
```

To record only remote audio, pass only `remoteAudioMixTrack`; to record only the local microphone, pass only `localMicTrack`.

***

### Upload while recording

Pass `timeslice` and `onDataAvailable` to get data in chunks in real time, for uploading or writing to disk while recording, so long recordings don't take up a lot of memory:

```typescript theme={null}
await recorder.start({
  videoItems,
  audioTracks,
  timeslice: 5000, // Produce a chunk every 5 seconds
  onDataAvailable: (chunk) => {
    uploadChunk(chunk); // Your app's upload logic
  },
});
```

> Note: webm chunks are a streaming container; a single chunk usually can't be played on its own, and the server needs to concatenate them in order into a complete file.

***

### State and cleanup

```typescript theme={null}
// 'inactive' not started or ended | 'recording' recording | 'paused' paused
const state = recorder.getState();

// Release internal resources (canvas, AudioContext, MediaRecorder, etc.) when no longer needed
recorder.destroy();
```

***

### Notes

* Must run over HTTPS (or localhost); `AudioContext` may need a user gesture before it can start.
* The output format is webm; browsers support different codecs (vp9/vp8/opus), and the SDK falls back to an available format automatically.
* `pause` / `resume` apply to the same final file and don't produce multiple files.
* The higher the recording resolution and frame rate, the higher the CPU usage; choose `width`/`height`/`fps` to suit your scenario.
