news 2026/9/19 19:34:50

RK3588视频编码实战:基于MPP的H.264/H.265硬件编码全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RK3588视频编码实战:基于MPP的H.264/H.265硬件编码全解析

有人问过我:“RK3588做视频项目,能不能不碰MPP,直接用CPU软编?”如果只编一路720p玩一玩,当然可以;但你要是做1080p60或者4K的编码,我劝你趁早断了这个念想。RK3588的八核CPU虽然不弱,可软编视频是一个典型的“吃满核还嫌不够”的重负载活,跑起来之后系统里其他任务全得靠边站。我自己的方案从一开始就定在MPP(Rockchip Media Process Platform)上,也就是瑞芯微官方的媒体处理平台,用它来调VPU硬件编码单元,把H.264/H.265的编码压力从CPU上彻底卸掉。

这篇文章把最近做RK3588视频编码的完整过程写一遍:从环境搭建、交叉编译,到MPP的MPI接口怎么去调,再到把编码器吐出来的裸流封装成MP4、FLV或者推RTSP流。网上的资料大多停在“能跑通demo”的层面,真正到自己做项目时,每一步都有让你卡一下午的细节。我自己踩过的坑、调过的参数、排查过的内存问题,都会写出来,希望能让后面做这块的人少走点弯路。

1. 效率不是玄学:为什么RK3588的视频编码必须走MPP这条路

1.1 RK3588的VPU到底能干什么

先看硬件底子。RK3588集成的VPU,公共资料里写得很明确:视频编码支持H.264、H.265、VP8、JPEG,最大到8K@30fps;视频解码支持的格式更多,H.265、H.264、VP9、AV1、AVS2都在列。这个编码能力放在单板产品里是非常能打的,8K编码意味着4K@60fps这种需求对它来说根本不算极限,真正干活时甚至可以开多路1080p并发编码。

但硬件能力摆在芯片里是一回事,能不能把它用起来是另一回事。直接用寄存器去操作VPU,不现实,瑞芯微也没有对应用户这么干。它提供的方案就是MPP,一套运行在用户态、基于Linux的媒体处理平台,把VPU的buffer管理、任务调度、编码参数配置这些繁杂细节全部封装掉。你只需要调用它的MPI(Media Process Interface)接口,把一帧YUV数据送进去,然后从另一边把编码后的码流包拿出来。

所以做RK3588视频编码,本质上是两件事:第一,理解MPP的接口怎么调用;第二,把编码器输出的裸流变成业务能用的文件或网络流。前者解决“能不能编出来”,后者解决“能不能交付出货”。这篇文章就是围绕这两条主线展开的。

1.2 取流、编码、封装的链路中MPP处在哪个位置

做视频方案时,完整的数据链路通常是这样的:摄像头或者ISP单元输出YUV原始帧,经过V4L2、RKISP或者直接内存导入的方式,把图像数据交给编码器;编码器压缩成H.264/H.265码流;码流再经过封装层变成MP4文件,或者打包成FLV推RTMP,或者走RTP推RTSP。

MPP在链路中间充当的就是那个“编码引擎”。前面取流是什么方式它不管,后面封装成什么容器它也不管,它只负责高效地把YUV帧变成H.264/H.265码流包。这个边界划分非常重要,很多初学者以为用了MPP就等于把推流也搞定了,其实不是。你从MPP拿到的是一包一包的裸流数据,要存文件还是推网络,还得自己再处理一层。

我建议在设计模块时就把这条链路拆开:采集线程只负责取帧,编码线程只负责和MPP交互,封装/推流线程只负责消费码流包。三个线程之间用队列解耦,帧数据用引用计数管理,避免深拷贝。这样即使后面要换采集方式,或者从MP4切换成RTSP,代价都很小。

1.3 哪些场景其实没必要硬上MPP

写到这里可能有人会觉得,是不是所有RK3588的视频场景都无脑上MPP?也不是。至少有两种情况你可以重新考虑:

第一种是极低分辨率、极低帧率的场景,比如只编一路320x240、5fps的预览画面,CPU软编完全扛得住,系统负载也很低。此时为了一个MPP引入额外模块,反而增加了开发和调试成本。

第二种是项目周期非常紧,对底层的掌控要求又不高,只求快速交差。这种情况下,直接使用FFmpeg的h264_rkmpp/hevc_rkmpp编码器,或者GStreamer的mpph264enc插件,底层都是MPP,但对外有更高层的封装,开发效率更高。我自己在项目初期做原型验证时就是这么干的,等确认方案可行后再回头用原生MPP接口精雕细节。

原生MPP接口的价值在于:它省掉了FFmpeg/GStreamer那一层的开销和不确定性,让你能精确控制编码参数、buffer生命周期和时间戳,对延迟、码率、内存占用都能抠到极致。如果你的产品是视频类设备,最终大概率还是要走原生MPP这条路。

2. 环境搭建:在目标板子上跑通第一个H.264裸流

2.1 先检查系统里是不是已经有MPP可以白嫖

RK3588的开发板,大多数Linux发行版固件(比如Debian、Ubuntu桌面包)里已经预装了rockchip-mpp的动态库和头文件。动手编译之前,先上板子执行几行命令确认一下,很多时候环境根本不用从头搭。

ldconfig -p | grep rockchip_mpp ls /usr/lib/aarch64-linux-gnu/librockchip_mpp* ls /usr/include/rockchip/

如果能看到librockchip_mpp.so、librockchip_mpp_rc.so之类的动态库,以及rk_mpi.h、mpp_enc.h、mpp_frame.h这些头文件,那么恭喜,系统自带的MPP可以直接拿来用了。这里要留意的是,固件自带版本可能和你从GitHub拉下来的源码版本有差异,接口定义偶有调整,但核心的MPI接口保持兼容,不用太担心。

如果你用的固件比较精简,或者干脆是自己用Buildroot/Yocto做的系统,里面没有MPP,那就走源码编译这条路。还有一种情况:固件里虽然带库,但不带头文件,那你最好也自己编译一份,开发时以自己编译的头文件为准,避免对着旧头文件写代码、链接新库时行为对不上。

2.2 从源码编译MPP的两种方式和分支选择

MPP源码在GitHub上,仓库名是rockchip-linux/mpp,主要分支有release和develop。release分支偏向稳定,适合在产品上使用;develop分支更新更快,可能包含新特性,但也可能有新引入的问题。我的习惯是:做产品用release分支,做新功能调研时才会切到develop看。

在RK3588板子上编译MPP有两种常见方式。

第一种是在板子上直接原生编译。MPP是一个cmake工程,体积不大,板子上跑编译完全可行。SSH登录到板子,执行:

git clone -b release https://github.com/rockchip-linux/mpp.git cd mpp mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release .. make -j6

编译完成后,动态库和测试工具会在build目录里生成。

第二种是在PC上交叉编译,适合公司里集中管理工具链、或者板子磁盘空间紧张的情况。交叉编译时,确保你已经安装了aarch64-linux-gnu交叉工具链,然后:

cd mpp mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \ -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \ .. make -j8

交叉编译完成后,把编译出来的librockchip_mpp.so和test目录下的可执行文件用scp推到板子上。需要提醒的是,如果板子系统里已经装了旧的MPP库,新编译的库要放到单独的目录,比如/opt/mpp/lib,然后用LD_LIBRARY_PATH指过来,否则测试程序默认加载系统库,你辛苦编出来的新库根本没生效。这个问题我第一次就踩了,测试半天发现跑的还是老版本。

2.3 mpi_enc_test快速验证VPU有没有正常工作

装好MPP之后,先别急着写代码,用官方测试工具验证一下VPU是不是好的。MPP源码里自带一堆测试程序,编码相关的在编译输出的test目录下,常见的有mpi_enc_test,有些版本叫mpp_enc_test,本质一样。

跑一个H.264编码测试,1080p分辨率编100帧:

LD_LIBRARY_PATH=./lib ./test/mpi_enc_test -t 7 -w 1920 -h 1080 -n 100 -o /tmp/test.h264

这里的-t 7表示H.264编码,-t 8则是H.265/HEVC。MPP测试工具支持自动生成YUV输入数据,不需要外部喂文件,所以跑起来非常方便。如果VPU工作正常,命令执行结束后/tmp/test.h264就是一份可以直接播放的H.264裸流。

把裸流拉回PC验证:

ffplay -f h264 /tmp/test.h264

如果播放出画面,说明VPU和MPP链路都是通的,环境搭建这一步就算完成了。这一步花不了几分钟,但能帮你把“环境问题”和“代码问题”隔离开:后面自己写代码跑不通时,至少可以确定MPP本身在板子上是好的。

3. 编码主流程:MPP的MPI接口就是一个“吃帧拉包”的工厂

3.1 初始化的关键参数:格式、对齐和码控

MPP的编码接口,本质就是一套“往里送帧、往外拉码流包”的工厂流水线。先把这条流水线建立起来。

#include "rk_mpi.h" #include "mpp_enc.h" #include "mpp_frame.h" #include "mpp_packet.h" MppCtx ctx = NULL; MppApi *mpi = NULL; MppEncCfg cfg = NULL; // 创建编码上下文 mpp_create(&ctx, MPP_CTX_ENC); // 初始化为H.264编码器,如果要编H.265就改成MPP_VIDEO_CodingHEVC mpp_init(ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC); // 拿到底层MPI接口 mpi = mpp_get_mpi(ctx);

接下来是配置编码器参数。MPP的配置项采用“键值对”风格,通过mpp_enc_cfg_set_s32等接口写入。这里最关键的几个参数是:宽高、水平/垂直对齐、像素格式、码控模式、码率和GOP大小。

mpp_enc_cfg_init(&cfg); mpp_enc_cfg_set_s32(cfg, "prep:width", 1920); mpp_enc_cfg_set_s32(cfg, "prep:height", 1080); mpp_enc_cfg_set_s32(cfg, "prep:hor_stride", 1920); mpp_enc_cfg_set_s32(cfg, "prep:ver_stride", 1088); mpp_enc_cfg_set_s32(cfg, "prep:format", MPP_FMT_YUV420SP); mpp_enc_cfg_set_s32(cfg, "rc:mode", MPP_ENC_RC_MODE_CBR); mpp_enc_cfg_set_s32(cfg, "rc:bps", 8000000); mpp_enc_cfg_set_s32(cfg, "rc:bps_max", 8000000); mpp_enc_cfg_set_s32(cfg, "rc:bps_min", 8000000); mpp_enc_cfg_set_s32(cfg, "rc:gop", 60); mpp_enc_cfg_set_s32(cfg, "codec:type", MPP_VIDEO_CodingAVC); mpi->control(ctx, MPP_ENC_SET_CFG, cfg);

解释一下那两个容易看懵的参数。“prep:width”和“prep:height”是你图像的实际尺寸,但硬件编码时内存通常需要对齐:1080这个高度对齐到16或者32的倍数之后是1088或者1080(RK3588上展开了不少对齐策略)。所以这里要区分“实际分辨率”和“stride对齐后的内存分辨率”。如果你送给编码器的buffer实际是按对齐后的stride分配的,就必须把hor_stride和ver_stride配置成对齐后的值,否则编码出来会花屏或者绿屏。

我在第一次做4K编码时就吃过这个亏:width和height填的3840x2160,但buffer是按3840x2176分配的对齐缓冲区,stride没告诉MPP,结果编码出来的画面底部有绿边,排查了整整半天。后来把hor_stride/ver_stride填成对齐值,画面立刻正常。这个细节,官方demo里通常会有注释,但很多照着抄代码的人注意不到。

3.2 帧输入:MppBuffer、NV12内存布局和PTS

编码器初始化完成后,接下来要准备输入帧。MPP推荐的做法是使用MppBufferGroup来管理buffer,这样buffer可以由MPP内部统一分配,编码器访问时效率更高。一个典型的做法如下:

MppBufferGroup frm_grp = NULL; MppBuffer frm_buf = NULL; MppFrame frame = NULL; // 创建一个DRM buffer group,VPU可以直接访问这种buffer mpp_buffer_group_get(&frm_grp, MPP_BUFFER_TYPE_DRM); // 按对齐后的stride计算buffer大小,NV12是Y平面加UV交错平面 size_t buf_size = hor_stride * ver_stride * 3 / 2; mpp_buffer_get(frm_grp, &frm_buf, buf_size); // 把采集到的图像数据拷入buffer void *buf_ptr = mpp_buffer_get_ptr(frm_buf); memcpy(buf_ptr, yuv_src, buf_size); // 组装MppFrame mpp_frame_init(&frame); mpp_frame_set_width(frame, width); mpp_frame_set_height(frame, height); mpp_frame_set_fmt(frame, MPP_FMT_YUV420SP); mpp_frame_set_buffer(frame, frm_buf); mpp_frame_set_pts(frame, pts);

这里的PTS是重中之重。MPP本身不会帮你生成时间戳,你送进去的MppFrame带什么pts,编码后的packet就带什么pts。如果你的时间戳是乱的,后面不管封装MP4还是推RTSP,播放端都会出现快进、卡顿、音画不同步。所以在上游采集阶段就一定要维护好线性递增的PTS,比如用单调时钟或者帧计数来生成。

像素格式也要提一下。MPP_FMT_YUV420SP对应的是NV12,也就是YYYYYYYYUVUV这种布局。RK3588的ISP输出或摄像头模组很多直接就是NV12,拿到就能用。如果输入是I420/YUV420P,也就是Y、U、V三个平面分立的格式,要么在进入编码器前转换为NV12,要么直接用MPP_FMT_YUV420P配置(MPP也支持,但需要确认你的固件版本对格式的支持完整)。我在实际项目里统一在采集端就转成NV12,后面所有环节都按NV12处理,省去很多麻烦。

3.3 编码循环:put_frame与get_packet怎么配合

编码器是异步工作的。你用encode_put_frame把帧送进去,不能马上从encode_get_packet拿到码流,中间需要等待VPU处理完成。一个基础的编码循环长这样:

while (get_yuv_frame(&yuv_src, &pts)) { // 拷入帧数据 memcpy(mpp_buffer_get_ptr(frm_buf), yuv_src, buf_size); mpp_frame_set_pts(frame, pts); // 送帧进编码器 ret = mpi->encode_put_frame(ctx, frame); if (ret != MPP_OK) { // 处理错误 break; } // 拉取编码后的码流包 MppPacket packet = NULL; while (1) { ret = mpi->encode_get_packet(ctx, &packet); if (ret == MPP_OK && packet) { void *data = mpp_packet_get_data(packet); size_t size = mpp_packet_get_size(packet); int64_t p_pts = mpp_packet_get_pts(packet); // 把码流数据写入文件或交给封装模块 write_packet(data, size, p_pts); mpp_packet_deinit(&packet); } else { break; } } }

这里有个容易困惑的地方:encode_get_packet返回MPP_OK但packet为NULL是合法情况,意思是“当前没有可输出的码流”,你继续循环等下一轮就行。如果等到超时仍然没有输出,那就要排查是不是编码器出错了。实际项目中我通常使用带超时控制的循环,避免在异常状态下死循环卡住线程。

还有一个细节是EOS处理。如果你要结束编码,官方接口里会送一个带EOS标记的空帧给编码器,编码器会把内部缓冲的码流全部吐出来。这个操作在处理“最后一帧”时非常关键,漏掉它会导致MP4文件末尾缺帧或者文件损坏。具体做法是:用mpp_frame_set_eos(frame, 1)设置一个空帧,送入编码器后,继续拉packet,直到拿到带EOS标记的packet为止。

4. 高效封装:从裸流到MP4/FLV/RTSP的落地姿势

4.1 先定容器再谈封装:交付场景决定方案

MPP吐出来的H.264/H.265码流是裸流字节流,直接存成.h264文件也能播放,但没有任何容器结构,无法做随机定位、音视频同步,也不能用于流媒体传输。真正要交付产品,必须把它封装进容器格式。选哪种容器,取决于你的业务场景:

场景推荐容器理由
本地录像回放MP4兼容性好,支持快进快退和音视频同步
录像留存、多音轨需求MKV封装灵活,容错强
RTMP直播推流FLVRTMP协议天然基于FLV tag结构
局域网摄像头预览RTSP/RTP实时性好,延迟低,播放端普遍支持
浏览器直接播fMP4/WebRTC分段mp4或走WebRTC

我在项目里用得最多的是MP4和RTSP两条线。录像功能走MP4,实时预览走RTSP。两条线共用同一个编码模块,只是编码参数不同:录像用VBR追求画质,预览用CBR保证码率稳定。

4.2 SPS/PPS和关键帧识别:封装的命门所在

无论做哪种封装,都绕不开SPS/PPS的处理。H.264裸流是Annex-B格式,用起始码00 00 00 01分隔NAL单元。IDR关键帧前面通常会跟着SPS(NAL type 7)和PPS(NAL type 8)。做封装时,需要从码流中识别出这些参数集。

识别NAL类型的方法很简单:起始码之后读第一个字节,取低5位。7是SPS,8是PPS,5是IDR帧,1是普通非IDR帧。MPP输出的H.264关键帧packet,通常会把SPS/PPS和IDR数据一起放在同一个packet里,这给裸流直接写文件带来了方便,但在封装MP4时,需要把SPS/PPS单独提取出来,生成AVCDecoderConfigurationRecord(也就是avcC box),并且写入MP4的extradata。

H.265稍有不同:对应的参数集是VPS(type 32)、SPS(type 33)、PPS(type 34),封装HEVC时需要生成hvcC box。如果你同时支持H.264和H.265,建议封装层把两套参数集提取逻辑都实现好,后面切换编码格式时就只是换一个函数的问题。

4.3 手写MP4 box和用FFmpeg接力:两条路怎么选

封装MP4这件事,有两条路线:一条是纯手写box,另一条是借用FFmpeg的libavformat。

手写box适合那种“我不想引入FFmpeg这么大一个依赖”的场景。MP4核心就是若干个box:ftyp、moov、mdat。mdat存的是码流数据,moov里面包含mvhd、trak、stts、stss、stsz等一系列索引信息。你只需要实现一个精简的MP4写入器,支持H.264/H.265的单一视频轨。我自己早期做过一个极简版,核心逻辑就几百行,但非常麻烦的点在于:如果写的是普通MP4,moov默认在文件末尾,播放器必须下载完整个文件才能播放。要在网页上边下边播,还得做“moov前置”,也就是写完后修正文件头,或者一开始就预留moov空间。这些细节很折磨人。

如果你产品里本来就用到了FFmpeg,或者你愿意链接libavformat,那用FFmpeg封装是最省力的。一个典型的流程是把MPP输出的packet直接组装成AVPacket,然后交给av_interleaved_write_frame:

AVFormatContext *ofmt = NULL; avformat_alloc_output_context2(&ofmt, NULL, "mp4", "record.mp4"); AVStream *st = avformat_new_stream(ofmt, NULL); st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; st->codecpar->codec_id = AV_CODEC_ID_H264; st->codecpar->width = 1920; st->codecpar->height = 1080; // 把从裸流里提取的SPS/PPS拼接成extradata st->codecpar->extradata = (uint8_t *)av_mallocz(extradata_size); memcpy(st->codecpar->extradata, extradata, extradata_size); st->codecpar->extradata_size = extradata_size; avformat_write_header(ofmt, NULL); AVPacket pkt; av_init_packet(&pkt); pkt.pts = p_pts; pkt.dts = p_pts; pkt.data = packet_data; pkt.size = packet_size; pkt.stream_index = st->index; // 这个flag要自己判断关键帧 if (is_idr) { pkt.flags |= AV_PKT_FLAG_KEY; } av_interleaved_write_frame(ofmt, &pkt); av_write_trailer(ofmt);

用FFmpeg封装时,最关键的是外部DTS。MPP的H.264输出,大多数配置下帧顺序就是显示顺序(B帧较少用,嵌入式编码通常关闭B帧以降低延迟),所以DTS可以直接等于PTS。如果你为了画质开了B帧,那么DTS和PTS就会出现偏移,封装层必须正确处理,否则播放时就会出现“卡顿后面的画面先出来”的错乱。我的经验是:实时视频场景一律关B帧,封装逻辑就能保持简单,延迟也能稳定压住。

4.4 直播场景下的GOP与延迟控制

做RTSP或RTMP直播时,GOP(关键帧间隔)的设置直接影响延迟和协议行为。GOP越大,关键帧越少,客户端从拉流到首帧显示的时间越不确定;GOP越小,码流中I帧变多,同样码率下画质会被摊薄。我一般把GOP设成帧率的整数倍:比如30fps下GOP设60,即2秒一个IDR。这个配置兼顾了“进流后快速出图”和“画质不过分损失”。

RTP封装H.264时,还有一个MTU分片的问题。一个IDR帧的单个NAL单元可能超过1400字节,RTP包需要按RTP payload格式进行FU-A分片。如果你是自己实现RTSP推流,这个分片逻辑省不了;如果是用GStreamer或FFmpeg的RTSP muxer,它们会自己处理。我的建议是:直播链路在原型阶段直接用GStreamer的rtspsrc/rtspclientsink组合验证,量产阶段再决定要不要自研RTSP server,避免一上来就被RTP分片、SDP生成这些细节拖住进度。

5. 实测排障记录:帧率、码控和buffer分配三个坑

5.1 1080p60与4K30的实测数据

先给一组我自己在RK3588板子上实测的数据,供参考。需要说明的是,同一颗芯片、不同板卡散热、不同固件版本、不同输入源,数据都会有差异,不要拿我的数字去怼供应商,但数量级可以参考。

编码配置实际帧率CPU占用(编码线程)备注
1080p60 H.264 CBR 8Mbps60fps稳定5%-10%最常用的预览/录像组合
4K30 H.265 CBR 20Mbps30fps稳定8%-15%高分辨率录像场景
4K60 H.265 CBR 25Mbps接近60fps15%-25%对BNB显存/内存带宽要求高

CPU占用低是硬件编码最大的优势。软编1080p60在这颗芯片上CPU占用会直接飙到70%以上,还伴随着掉帧。用MPP后,编码线程的CPU占用可以忽略不计,剩下的算力全部留给算法、协议栈和应用逻辑。这也是我在方案选型时坚持MPP的核心原因。

5.2 帧率抖动:先怀疑输入源,再怀疑码控

有一次做1080p60录像,编码器输出帧率在55到60fps之间来回跳,怎么看都不对劲。首先排查的方向是输入源。我先用mpi_enc_test的YUV自生成模式跑了一遍,发现编码器本身能稳定跑满60fps,问题显然出在采集链路。

接着查采集线程,发现它从V4L2拿到buffer之后,在memcpy之前做了一次图像旋转,用的是普通CPU逐像素复制,这一步吃掉不少时间,导致喂帧速率不稳定。把这个旋转操作换成基于DMA的实现,或者干脆去掉旋转、让显示端旋转,帧率就稳定了。

排查这类问题,我总结了一条原则:先隔离编码器,再怀疑输入源,最后才回头看码控。MPP自带的测试工具就是最好的隔离手段,它能帮你快速判断“到底是谁拖慢了谁”。

5.3 mpp_buffer分配失败:CMA内存耗尽怎么查

MPP的buffer大多来自DRM/CMA内存。系统跑久了,如果buffer没有正确释放,CMA内存会被耗尽,然后出现mpp_buffer_get失败、编码卡死、甚至VPU任务不提交的现象。排查方法:

cat /proc/meminfo | grep -i cma

看到CmaTotal和CmaFree之后,如果CmaFree长期为一个很小的值,基本可以确定是buffer泄漏。常见的泄漏点有几个:

第一个,packet拿到后忘记调mpp_packet_deinit。这个是最容易犯的,每帧泄漏一个packet,跑个几万帧系统就废了。

第二个,输出packet处理后,没有及时把packet的数据指针归还。MPP的packet内部引用的是编码器内部buffer,deinit操作会回收,但如果你把packet的data指针存到另一个队列里迟迟不释放,底层buffer一样被占着。

第三个,MppBuffer本身没有正确put。MppBuffer的上游是MppBufferGroup,buffer get之后用完必须调用mpp_buffer_put归还给group,否则group里的内存越占越多。我自己的模块里,输入帧buffer用的是循环池,固定几个buffer轮转,避免反复get/put带来的额外开销和碎片。

5.4 码控模式选不对,画质和码率两头受气

MPP的码控模式(rc:mode)主要有CBR、VBR和FIXQP,各有适用场景。

CBR模式下编码器会严格控制输出码率,瞬时码率波动很小,适合RTSP/RTMP这类需要带宽可控的传输场景。但代价是:画面运动剧烈时,为了压低码率会降低画面质量,出现模糊或马赛克。所以我做CBR时,bps_max不会紧贴着bps设,而是留出1.5到2倍的上浮空间,让编码器在复杂场景下有喘息余地。

VBR模式的码率波动大,但画质稳定,适合本地存储。这个模式在运动场景下能明显看出画质比同规格CBR好。缺点是如果突发码率过高,存储系统写入压力会增大,需要评估SD卡或硬盘的持续写能力。

FIXQP适合做算法调试,比如你想对比不同分辨率下的编码画质,固定QP后输出结果更纯粹。我自己在测试VPU能力边界时会用FIXQP,但在产品代码里,录像用VBR,预览用CBR,这个搭配比较无脑也不会出错。

关于码控,还有一个坑是“码率设置太小导致掉帧”。有一次我把1080p的bps设成1Mbps,结果编码器明显开始丢帧。原因是极限低码率下编码器需要做复杂的码率控制决策,处理不过来。它不是不能编到1Mbps,而是需要更多时间决定量化参数,输入帧率一高就会落后。解决方法是提高bps,或者降低输入帧率。所以配置码率时,不要拍脑袋,要结合分辨率和帧率估算一个合理区间:1080p30起步4Mbps,1080p60起步8Mbps,4K30起步16Mbps,这只是经验值,具体还要看画面复杂度。

5.5 关于时间戳和多路并发的两个补充点

最后补充两个容易被忽视的点。

时间戳基准:如果同时做音视频录制,音频和视频必须共用一个时钟基准。我通常使用CLOCK_MONOTONIC作为系统时间基准,采样到时间后统一换算成微秒,再转成对应容器的时间戳。这样即使系统时间被NTP调了,音视频同步也不受影响。很多项目的音画不同步问题,根源不是封装层,而是时间戳源头就乱了。

多路并发:RK3588的VPU可以同时跑多路编码,但总资源是有限的。多路并发时,每路的编码参数要提前做整体规划,比如两路1080p60加一路4K30,就需要估算总码率和总内存占用,给各路分配独立的MppBufferGroup,避免互相争抢。我自己做过四路1080p30同时编码,VPU资源还剩富余,但内存带宽明显吃紧,后来通过降低某些路的帧率优先级,整体才算稳定。

综合这些实测和排障经验,我的感受是:RK3588的MPP编码本身是成熟可靠的,项目的难点基本都在“你以为配置对了其实没有”的细节上,比如stride对齐、buffer生命周期、时间戳一致性。把这三个基础问题打牢,后面的封装、推流、存储都会顺畅很多。希望这篇实战记录能帮你把这段路走得更顺一点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 19:30:15

OpenCode + NanoBanana MCP:终端里的AI图像编辑实战

前段时间一个做UI的朋友给我丢过来一张产品截图,让我把背景抠掉、换个深色底,再把右上角的Logo文案改成中文。搁以前这种活我大概率会打开网页工具或者切到设计软件手动折腾十分钟。但这次我动了点心思:既然OpenCode已经能把写代码、读文档这…

作者头像 李华
网站建设 2026/9/19 19:27:43

如何系统规划一篇App框架开发技术文章?从选型到上架的完整路线

写技术文章最怕的不是没内容,而是内容太多。我最近准备整理一篇关于app框架开发的技术文章,原本打算直接动笔写正文,结果越列素材越觉得事情没那么简单:框架选型要写,架构分层要写,状态管理、路由、网络请求…

作者头像 李华
网站建设 2026/9/19 19:26:22

GitHub Copilot 的 Token 想拿去做别的用途,走 TaoToken 行不行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 19:23:59

鸿蒙Flutter稳定性排查:黑屏白屏OOM与内存泄漏的DFX实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华