LocalCustomVideoTrack 把业务侧自己生成的画面发布到频道。分工是固定的:
- 你负责产出帧:持续提供未编码的原始 YUV(I420)数据,无需自行编码。
- SDK 负责编码与传输:按预设参数编码、发布,远端像普通视频流一样订阅播放。
- 白板 / 画布 / 自绘内容推流
- 本地播放器解码后的画面转推
- 第三方 SDK(美颜、AR、AI 生成)处理后的输出
- 非标准采集设备(外接采集卡、USB 设备)的帧数据
1. 整体流程
- 入会之后才能发布(未入会
publishLocalVideo回调onFail(102202),频道未启动)。 - 发布成功之后才能送帧。轨道未发布时
inputData会被静默丢弃,没有任何提示 —— 这是最常见的“远端看不到画面”的原因。 - 观众身份不能发布(
onFail(102207))。可先用rtcEngine.isAudience()判断。
2. 选择预设
预设决定编码分辨率、帧率、码率以及轨道描述(desc),完整字段见 自定义视频流预设。
- 静态内容为主(白板、文档、PPT):帧率取 5~10 fps 就够,分辨率优先(文字清晰度靠分辨率而不是帧率)。
- 动态内容(视频转推、游戏画面):帧率取 15~25 fps,同时把码率相应提高,否则运动画面会明显发糊。
- 宽高必须是偶数(I420 的色度平面按
width/2 × height/2计算)。
3. 获取轨道并发布
- 它在 SDK 内部按单例缓存。重复调用
getLocalCustomVideoTrack(preOpt)返回同一个对象,并用新传入的preOpt覆盖旧值。因此不要用两套desc交替送帧来模拟“两路自定义流”,同一时刻只应有一套生效的预设。 - 若在发布时通过
PublishCustomOptions(desc = ...)覆盖了desc,SDK 会把它写回preOpt.publish.desc,inputData自动按最新的desc定位轨道,业务侧无需额外处理。
4. 准备帧数据(关键)
inputData 只接受紧凑排布的 I420。这一节的约定不满足,表现就是远端花屏、错位或绿边。
4.1 内存布局
stride 参数按紧凑值传:
⚠️ SDK 当前按紧凑布局计算平面偏移,传入带 padding 的行跨度不会被用于寻址。若上游数据每行带 padding(如 Camera2 的 rowStride > width),必须先按有效像素逐行拷贝成紧凑数组,再送入。
数据长度不足时,SDK 会在送编码前抛出 IllegalArgumentException("Invalid I420 size: ..."),可据此快速定位布局问题。
4.2 从 Bitmap / Canvas 生成 I420
白板、自绘内容通常先画到Bitmap(ARGB_8888),再转 I420:
上面是纯 Kotlin 实现,便于理解与自测,但 1080p 逐像素循环在中低端机上开销明显。生产环境建议改用 libyuv(ARGBToI420)或 GPU 方案;也可以先降到 720p 再推流。SDK 内部虽然集成了 libyuv,但未对外暴露转换接口,需要业务侧自行引入。
4.3 从 YUV_420_888 生成 I420
若数据来自ImageReader / 第三方采集(YUV_420_888),需要处理三件事再送入:
rowStride可能大于width→ 逐行按width拷贝,去掉行尾 padding。pixelStride可能为 2(半平面 NV12/NV21 形态)→ 按步长抽取,拆成独立的 U、V 平面。- U、V 顺序必须是 I420(先 U 后 V),NV21 的 VU 顺序需要交换。
Android420ToI420,自己写循环容易在 stride/pixelStride 组合上出错。
5. 持续送帧
线程与节奏:
- 不要在主线程送帧。
inputData是同步调用,内部会做拷贝、必要的对齐缩放并提交编码,1080p 下单帧耗时不可忽略。建议用一条独立的HandlerThread。 - 按预设帧率节流。超过
maxFps的高频送帧不会提升画质,只会白白增加 CPU 与内存带宽开销。 - 静态画面也要持续送帧。停止送帧远端会停在最后一帧;若画面长时间不变,可按最低帧率(如 1~2 fps)继续送同一帧以维持流的活性。
- 复用输出数组。
inputData返回后内部已完成数据拷贝,业务侧可立即复用同一个ByteArray,避免每帧新建对象造成 GC 抖动。
6. 停止与清理
- 先停送帧、再取消发布,顺序反了会有若干帧被丢弃(无害,但日志里会有无效调用)。
- 与摄像头一致,快速连续
publish/unPublish时,中间被合并的调用可能不回调,请以最后一次调用的回调或最终状态为准。 leave()会释放流媒体引擎,轨道实例仍在但发布状态已失效,重新入会后必须重新publishLocalVideo。releaseSDK()还会清空本地轨道缓存,之后需重新getLocalCustomVideoTrack。
本地预览:LocalCustomVideoTrack继承了addPlayView等渲染方法,但 SDK 不会把inputData送入的帧回显到这些控件上(本地回显只对摄像头轨道生效)。自定义推流的本地预览请直接显示你自己的数据源(如白板 View 本身),不需要给本轨道添加渲染控件。
7. 完整示例
一个把Bitmap 按固定帧率推流的最小封装:
8. 排查对照表
相关文档
- LocalCustomVideoTrack:接口与参数完整定义
- 自定义视频流预设:
PreOptionCustomVideo字段与内置预设 - RTCEngine:
getLocalCustomVideoTrack/publishLocalVideo/unPublishLocalVideo - 快速开始:入会、发布、订阅的最小主线流程