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

# LocalCustomVideoTrack

> Android local track for pushing external raw YUV (I420) video frames into a channel, for whiteboards, canvases, players, or third-party sources: the preOpt preset, inputData frame format requirements, publishing flow, and caveats. Read when publishing custom video on Android.

## Description

`LocalCustomVideoTrack` pushes external **raw YUV video frames** (such as a whiteboard, canvas, player output, or third-party capture source) into a published track. Get it through [`RTCEngine.getLocalCustomVideoTrack`](/en/rtc/android/api-reference/RTCEngine). It extends `LocalVideoTrack` and can be passed directly as the input track to `publishLocalVideo` / `unPublishLocalVideo`.

This page is the API reference; for the full integration flow, frame format conversion, and troubleshooting tips, see [Custom tracks](/en/rtc/android/advanced/custom-track).

The input is **unencoded** raw YUV (I420) frames. The SDK encodes them according to the preset parameters, so you don't need to encode them yourself.

## Properties

### preOpt

```kotlin theme={null}
var preOpt: PreOptionCustomVideo
```

Description: The custom video preset used by this track (capture parameters + publish parameters); see [Custom video preset](/en/rtc/android/presets/custom-video).

* `preOpt.publish.desc` determines the track description of this track, and `inputData` also uses it to locate the target track.
* The SDK caches the track instance as a singleton: repeated calls to `getLocalCustomVideoTrack(preOpt)` return the same instance and overwrite it with the `preOpt` you pass. To distinguish between a "custom track" and a "screen sharing track", republish after switching presets instead of alternating frames between two `desc` values at the same time.

## LocalCustomVideoTrack methods

### inputData(yuv, width, height, strideY, strideU, strideV, rotation, stamp)

```kotlin theme={null}
fun inputData(
    yuv: ByteArray, width: Int, height: Int,
    strideY: Int, strideU: Int, strideV: Int,
    rotation: Int, stamp: Long
)
```

Description: Pushes one frame of raw YUV data into the published custom video track. Internally, the SDK looks up the published track by `preOpt.publish.desc` and feeds the frame into the encoding pipeline; if the track isn't published yet, the frame is silently dropped.\
Parameters:

| Parameter | Data type | Description |
| - | - | - |
| yuv | `ByteArray` | Tightly packed I420 data, at least `width * height * 3 / 2` bytes long. |
| width | `Int` | Frame width; must be even. |
| height | `Int` | Frame height; must be even. |
| strideY | `Int` | Row stride of the Y plane; pass `width`. |
| strideU | `Int` | Row stride of the U plane; pass `width / 2`. |
| strideV | `Int` | Row stride of the V plane; pass `width / 2`. |
| rotation | `Int` | Frame rotation angle, one of `0` / `90` / `180` / `270`, passed as frame metadata to the encoding and rendering side. |
| stamp | `Long` | Frame timestamp in **nanoseconds** (the same basis as Camera2's `timestampNs`). |

Returns: None (`Unit`).

#### Frame data format requirements

* **Must be tightly packed I420**: the `Y` plane of `width * height` bytes, immediately followed by the `U` plane of `(width/2) * (height/2)` bytes, then a `V` plane of the same size, with no gaps between the three segments.
* **`stride` must match the tightly packed layout** (`width`, `width/2`, `width/2`). The SDK currently computes plane offsets from the tightly packed layout, so passing row strides with padding causes misaligned or corrupted video. If upstream data has padding (for example, Camera2's `rowStride > width`), copy the valid pixels into a tightly packed array first.
* If the data is too short, an `IllegalArgumentException` (`Invalid I420 size`) is thrown before encoding.
* You don't need to align the resolution for the encoder: the SDK aligns and scales according to the device encoder's requirements; however, `width`/`height` must be even.
* You control the frame pacing. The frame rate and bitrate caps are determined by `preOpt`; pushing frames faster than the preset frame rate only adds unnecessary overhead.

## Rendering methods inherited from VideoTrack

`addPlayView` / `replacePlayView` / `removePlayView` / `removeAllPlayView` are provided by the base class `VideoTrack`, with the same signatures as [`LocalScreenTrack`](/en/rtc/android/api-reference/LocalScreenTrack).

> **Note**: The SDK **doesn't** echo frames sent through `inputData` to these render views (local echo only applies to camera tracks). For a local preview of custom video, draw from your data source yourself; you don't need to add render views to this track.

## Typical integration flow

```kotlin theme={null}
// 1. Get the track (you can customize the preset; the default is PreOptionCustomVideo.def)
val customTrack = rtcEngine.getLocalCustomVideoTrack(PreOptionCustomVideo.def)

// 2. Publish the track; you can only push frames after publishing succeeds
rtcEngine.publishLocalVideo(customTrack, null, object : RTCResultListener {
    override fun onSuccess() {
        // 3. Keep pushing frames at your own pace (a single frame is shown here)
        customTrack.inputData(
            yuv = i420Bytes,          // Tightly packed I420
            width = 1920,
            height = 1080,
            strideY = 1920,
            strideU = 960,
            strideV = 960,
            rotation = 0,
            stamp = System.nanoTime()
        )
    }

    override fun onFail(code: Int) {
        // See the error codes page for error codes
    }
})

// 4. Stop pushing
rtcEngine.unPublishLocalVideo(customTrack, null)
```

To publish external video with the screen sharing track description (`TRACK_SHARE`), use `PreOptionCustomVideo.screen` instead, or override `desc` with `PublishCustomOptions(desc = ...)` when publishing:

```kotlin theme={null}
val shareTrack = rtcEngine.getLocalCustomVideoTrack(PreOptionCustomVideo.screen)
rtcEngine.publishLocalVideo(shareTrack, null, null)
```

## Notes

* **Publish first, then push frames**: call `inputData` only after the `publishLocalVideo` success callback; otherwise frames are dropped without any notice.
* **Keep `desc` consistent**: if you override `desc` with `PublishCustomOptions` when publishing, the SDK writes it back to `preOpt.publish.desc`, and `inputData` still locates the track by the latest `desc`; don't keep a separate copy of the old `desc` in your code for decisions.
* **Publish/unpublish callbacks are coalesced**: as with the camera, when `publish`/`unpublish` are called in rapid succession, intermediate coalesced calls may not get a callback; rely on the callback of the last call or the final state.
* **Republish after leaving the channel**: `leave()` releases the media streaming engine. The track instance remains, but its publish state is no longer valid; after joining again, you must call `publishLocalVideo` again before pushing frames. `releaseSDK()` also clears the local track cache, after which you need to call `getLocalCustomVideoTrack` again.
* **Reuse input arrays**: `inputData` copies the data into the encoding buffer internally, so you can reuse the `ByteArray` as soon as the call returns. A buffer pool is recommended to reduce GC pressure.
