RK3588 libmedia VO:把视频帧送到 HDMI、DSI 显示屏
公开示例:https://github.com/libmedia886/rk3588-vo-demo
VO 是视频链路的显示出口。VI、VDEC、VPSS、RGA、OSD 等模块产生的视频帧,可以通过 libmedia pipeline 送进 VO,再由 DRM/KMS plane 显示到 HDMI、DP、eDP 或 MIPI DSI 屏幕。
下图用于说明 VO 的数据路径,不是开发板运行截图。真实效果仍要以目标板卡、connector、plane 和显示器上的画面为准。
VO 解决什么问题
VO 负责三件事:
- 选择物理显示输出,例如
HDMI-A-1或DSI-1。 - 为视频画面创建一个 DRM plane,确定位置、尺寸、stride、队列深度和像素格式。
- 接收上游视频帧并持续提交到显示平面。
一条常见显示链路是:
VI / VDEC -> VPSS / RGA -> OSD -> VO input0 -> DRM/KMS -> HDMI / DSI 屏幕VO 不负责采集、解码或图像增强。它接收已经准备好的视频帧,并把这些帧放到指定显示平面上。
先分清 output 和 chn
VO 接入中最容易混淆的是output_id和chn。
output_id表示一个显示输出实例,关联物理 connector 和显示时序。chn表示这个输出上的一个显示平面。- 通道
chn = 0对应 VO 的输入端口input0,chn = 1对应input1。
因此,下面这条绑定中的目标 id 是output_id,目标端口才用于选择通道:
MEDIA_SYS_Bind("OSD",0,"output0","VO",output_id,"input0");端口名必须显式填写,不能把output0或input0省略成NULL或空字符串。
当前 VO API 主流程
配置阶段固定为:
MEDIA_VO_ProbeConnectors:查询当前 DRM connector。MEDIA_VO_SetAttr:设置 connector、显示时序和 plane 数量。MEDIA_VO_CreateChn:创建显示平面和对应的inputN端口。
送帧有两种方式:
- 绑定模式:先
MEDIA_SYS_Bind,再MEDIA_VO_Start,由上游模块持续输出。 - 手动模式:先
MEDIA_VO_Start,再用MEDIA_SYS_SendFrame把 buffer 送到inputN。
退出时先停止上游送帧,再停止 VO、解绑并销毁通道,最后调用MEDIA_SYS_Exit。
1. 查询可用 connector
当前MEDIA_VO_ATTR.connector是必填字段。不要只设置MEDIA_VO_INTF_HDMI就假定目标一定是某个 HDMI 口。
MEDIA_VO_CONNECTOR_INFO connectors[8]={0};intconnector_count=0;if(MEDIA_SYS_Init()!=0){return-1;}if(MEDIA_VO_ProbeConnectors(connectors,8,&connector_count)!=0){return-1;}for(inti=0;i<connector_count&&i<8;++i){printf("connector=%s connected=%d mode=%dx%d@%d\n",connectors[i].name,connectors[i].connected,connectors[i].width,connectors[i].height,connectors[i].fps);}应用应从查询结果中选择目标 connector。常见名称包括HDMI-A-1、HDMI-A-2和DSI-1,实际名称以当前设备返回值为准。
2. 配置显示输出
下面以HDMI-A-1、1920x1080@60 为例:
MEDIA_VO_ATTR vo={0};vo.intf=MEDIA_VO_INTF_HDMI;vo.connector="HDMI-A-1";vo.width=1920;vo.height=1080;vo.plane_count=1;vo.fps=60;vo.policy=MEDIA_VO_MODE_NEGOTIATE;if(MEDIA_VO_SetAttr(0,&vo)!=0){fprintf(stderr,"MEDIA_VO_SetAttr failed\n");return-1;}MEDIA_VO_MODE_NEGOTIATE要求显示设备支持请求的时序,不会自动把任意分辨率强行设置到屏幕上。接入前可以用MEDIA_VO_GetSinkCaps查询支持的 mode,设置后用MEDIA_VO_GetActualMode或MEDIA_VO_GetOutputInfo检查实际结果。
只有明确知道目标硬件允许强制时序时,才使用MEDIA_VO_MODE_FORCE。
3. 创建显示平面
MEDIA_VO_CreateChn配置的是画面在屏幕上的位置和输入格式,不是物理输出时序。
intoutput_id=0;intchn=0;intview_x=0;intview_y=0;intview_w=1920;intview_h=1080;intstride=1920;intdepth=4;if(MEDIA_VO_CreateChn(output_id,chn,view_x,view_y,view_w,view_h,stride,depth,MEDIA_VO_PLANE_TYPE_AUTO,MEDIA_FORMAT_NV12,0)!=0){fprintf(stderr,"MEDIA_VO_CreateChn failed\n");return-1;}关键参数含义:
x、y、width、height:画面在屏幕上的窗口。stride:输入视频帧每行的实际跨度,必须与上游 buffer 一致。depth:VO 输入队列深度。plane_type:AUTO会优先选择合适的 overlay plane,必要时再考虑 primary plane。format:输入视频帧的像素格式,这里使用 NV12。- 最后一个参数小于等于 0 时不强制指定 DRM plane id。
分辨率是 1920 并不代表 stride 一定也是 1920。应使用上游模块或 buffer 的真实 stride,不能只按可见宽度猜测。
4. 绑定上游模块
实际工程更常用 pipeline 绑定,让上游模块持续把帧送进 VO:
if(MEDIA_SYS_Bind("OSD",0,"output0","VO",output_id,"input0")!=0){fprintf(stderr,"bind OSD to VO failed\n");return-1;}if(MEDIA_VO_Start(output_id,chn)!=0){fprintf(stderr,"MEDIA_VO_Start failed\n");return-1;}通常先创建并绑定整条链路,再启动下游 VO,最后启动上游采集或解码模块。这样可以减少刚启动时的空队列和黑屏时间。
5. 手动发送视频帧
如果应用已经持有一个格式和尺寸匹配的MEDIA_BUFFER,也可以直接送到 VO:
if(MEDIA_VO_Start(output_id,chn)!=0){return-1;}if(MEDIA_SYS_SendFrame("VO",output_id,"input0",frame,1000)!=0){fprintf(stderr,"send frame to VO failed\n");}手动送帧时,buffer 的尺寸、stride、格式和 metadata 必须与MEDIA_VO_CreateChn的配置一致。buffer 的取得、失败归还和成功发送后的所有权处理,要遵守产生该 buffer 的 Pool/API 约定。
6. 冻结和隐藏画面
VO 可以对指定 plane 做冻结和隐藏:
MEDIA_VO_FreezePlane(output_id,chn,1);/* 保持当前画面 */MEDIA_VO_FreezePlane(output_id,chn,0);/* 恢复刷新 */MEDIA_VO_HidePlane(output_id,chn,1);/* 隐藏画面 */MEDIA_VO_HidePlane(output_id,chn,0);/* 恢复显示 */冻结是保持最后一帧,隐藏是让这个 plane 不再可见,两者用途不同。
较早的示例可能还在调用MEDIA_VO_FreezeMain和MEDIA_VO_UnfreezeMain。当前公开头文件已经统一使用带output_id、plane_id的MEDIA_VO_FreezePlane,接入当前 SDK 时不要继续依赖旧接口。
7. 停止和清理
绑定链路的典型退出顺序如下:
/* 先停止上游模块继续产生视频帧 */MEDIA_VO_FreezePlane(output_id,chn,0);MEDIA_VO_HidePlane(output_id,chn,0);MEDIA_VO_Stop(output_id,chn);MEDIA_SYS_UnBind("OSD",0,"output0","VO",output_id,"input0");MEDIA_VO_DestroyChn(output_id,chn);MEDIA_SYS_Exit();如果应用还有其它模块或线程在调用MEDIA_*API,必须先停止并等待这些线程退出,再执行进程级的MEDIA_SYS_Exit。
正常显示时不需要 GetFrame
MEDIA_VO_GetFrame不是普通上屏链路的必要步骤。正常显示使用 pipeline 绑定或MEDIA_SYS_SendFrame,VO 内部完成 DRM plane commit。
MEDIA_VO_GetFrame是从 VO 的输入端口拉取一帧的特殊用法。只有确实需要这种输入端口取帧语义时才调用,并且成功后必须配对MEDIA_VO_ReleaseFrame。
如何判断接入成功
原理示意图只能解释结构,不能证明板端显示成功。真实设备上至少检查以下结果:
MEDIA_VO_ProbeConnectors能找到目标 connector,并且connected = 1。MEDIA_VO_GetActualMode返回的宽、高和帧率符合预期。- 屏幕持续显示上游最新帧,没有黑屏、撕裂、颜色异常或明显错位。
- 冻结时画面保持不动,解冻后继续刷新。
- 隐藏后 plane 消失,恢复后画面继续显示。
- 停止链路后可以正常解绑、销毁通道和退出,没有 buffer 或 plane 占用残留。
常见问题
MEDIA_VO_SetAttr 直接失败
先检查connector是否填写,以及名称是否来自MEDIA_VO_ProbeConnectors的当前查询结果。只设置intf、宽和高已经不够。
提示 mode 不支持
用MEDIA_VO_GetSinkCaps查看屏幕支持的时序,再调整宽、高和 fps。默认NEGOTIATE策略不会自动接受屏幕不支持的 mode。
屏幕黑屏
依次检查目标 connector 是否已连接、VO 是否已经Start、绑定端口是否写成正确的inputN、上游模块是否真的在出帧,以及输入格式和 stride 是否匹配。
画面颜色异常或位置错乱
重点核对format、stride、输入 buffer metadata 和窗口尺寸。NV12、RGB 和 ARGB 的内存布局不能混用。
Bind 找不到目标端口
VO 的端口名由通道号决定。chn = 0使用input0,chn = 1使用input1。绑定时的 VO id 是output_id,不要把通道号和输出 id 混为一谈。
与 X11 或桌面系统冲突
同一个显示输出只能有一个 modeset master。如果分辨率由 X11 等外部系统管理,应使用 VO 的 external display owner 路径,让外部系统管理 mode,VO 只负责 plane commit。
版本兼容提示
配套公开示例便于理解项目结构,但较早版本存在两处需要按当前 SDK 调整的写法:
MEDIA_VO_ATTR必须填写connector。MEDIA_VO_FreezeMain、MEDIA_VO_UnfreezeMain已由MEDIA_VO_FreezePlane统一替代。
编译前应以所使用 SDK 中的media_api.h为准,不要只照搬旧示例。
小结
VO 最小接入可以记成下面这条主线:
查询 connector -> 配置 output -> 创建 plane -> Bind 或 SendFrame -> Start / 持续显示 -> 停止上游 -> Stop -> UnBind -> DestroyChn真正决定能否稳定上屏的关键点有四个:connector 选对、mode 可用、inputN端口写对,以及输入格式和 stride 与 buffer 完全一致。
联系方式
如有技术交流或合作需求,请联系:allinjetson@gmail.com