Skip to main content
当内置的摄像头、屏幕共享采集不能满足需求时,可以用 LocalCustomVideoTrack业务侧自己生成的画面发布到频道。分工是固定的:
  • 你负责产出帧:持续提供未编码的原始 YUV(I420)数据,无需自行编码。
  • SDK 负责编码与传输:按预设参数编码、发布,远端像普通视频流一样订阅播放。
典型场景:
  • 白板 / 画布 / 自绘内容推流
  • 本地播放器解码后的画面转推
  • 第三方 SDK(美颜、AR、AI 生成)处理后的输出
  • 非标准采集设备(外接采集卡、USB 设备)的帧数据

1. 整体流程

三个必须遵守的顺序约束:
  1. 入会之后才能发布(未入会 publishLocalVideo 回调 onFail(102202),频道未启动)。
  2. 发布成功之后才能送帧。轨道未发布时 inputData 会被静默丢弃,没有任何提示 —— 这是最常见的“远端看不到画面”的原因。
  3. 观众身份不能发布onFail(102207))。可先用 rtcEngine.isAudience() 判断。

2. 选择预设

预设决定编码分辨率、帧率、码率以及轨道描述(desc),完整字段见 自定义视频流预设
需要自定义参数时直接构造。采集与推送两侧的分辨率保持一致,可以避免 SDK 做额外缩放:
选参数的建议:
  • 静态内容为主(白板、文档、PPT):帧率取 5~10 fps 就够,分辨率优先(文字清晰度靠分辨率而不是帧率)。
  • 动态内容(视频转推、游戏画面):帧率取 15~25 fps,同时把码率相应提高,否则运动画面会明显发糊。
  • 宽高必须是偶数(I420 的色度平面按 width/2 × height/2 计算)。

3. 获取轨道并发布

关于轨道实例,有两点需要注意:
  • 它在 SDK 内部按单例缓存。重复调用 getLocalCustomVideoTrack(preOpt) 返回同一个对象,并用新传入的 preOpt 覆盖旧值。因此不要用两套 desc 交替送帧来模拟“两路自定义流”,同一时刻只应有一套生效的预设。
  • 若在发布时通过 PublishCustomOptions(desc = ...) 覆盖了 desc,SDK 会把它写回 preOpt.publish.descinputData 自动按最新的 desc 定位轨道,业务侧无需额外处理。

4. 准备帧数据(关键)

inputData 只接受紧凑排布的 I420。这一节的约定不满足,表现就是远端花屏、错位或绿边。

4.1 内存布局

三段必须连续、无空洞,并且 stride 参数按紧凑值传:
⚠️ SDK 当前按紧凑布局计算平面偏移,传入带 padding 的行跨度不会被用于寻址。若上游数据每行带 padding(如 Camera2 的 rowStride > width),必须先按有效像素逐行拷贝成紧凑数组,再送入。
数据长度不足时,SDK 会在送编码前抛出 IllegalArgumentException("Invalid I420 size: ..."),可据此快速定位布局问题。

4.2 从 Bitmap / Canvas 生成 I420

白板、自绘内容通常先画到 BitmapARGB_8888),再转 I420:
调用方式:
上面是纯 Kotlin 实现,便于理解与自测,但 1080p 逐像素循环在中低端机上开销明显。生产环境建议改用 libyuvARGBToI420)或 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 顺序需要交换。
这部分同样推荐直接用 libyuv 的 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. 排查对照表

相关文档