RK3588S具备强大的编解码能力,其编解码器最大支持 7680x4320@30fps 的解码和 7680x4320@30fps的编码能力。RK平台统一使用MPP库作为其编解码器的开发工具。因此,可以实现一次开发,多个平台之间互相移植。
由于风火轮科技已经开源了sdk,因此建议在PC端的SDK进行统一构建,这样可以保证构建环境一致。SDK里面自带的交叉编译环境和板上的环境是一致的。
MPP库源码获取有两种方式。
如果要保持和板上的动态库一致,则需要拉取风火轮科技提供的整个SDK。mpp源码在SDK根目录的
external/mpp
下,建议将这个源码拷贝到一个单独的工作目录使用
如果不想下载整个SDK的,可以从下方链接下载mpp的包,这个包提取自完整版sdk
如果想要体验最新版本的mpp,可以直接从rk的git上面拉取,新建一个工作目录,然后执行
git clone https://github.com/rockchip-linux/mpp.git
首先配置交叉编译环境。在 SDK根目录 下,执行下面几行内容
DIR=$(pwd)
export PATH=$PATH:${DIR}/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin
export CROSS_COMPILE=aarch64-none-linux-gnu-
export ARCH=aarch64
在mpp的工作目录下,打开 build/linux/aarch64 目录
然后修改 arm.linux.cross.cmake 文件中编译器,如下
SET(CMAKE_C_COMPILER "aarch64-none-linux-gnu-gcc")
SET(CMAKE_CXX_COMPILER "aarch64-none-linux-gnu-g++")
然后在 build/linux/aarch64 目录执行
./make-Makefiles.bash
make -j8
即可执行编译,编译完成的信息如下
编译完成后,可以在此目录下看到如下文件
然后要验证一下编译出来的mpp环境在板上是否能运行。
在mpp的工作目录的 build/linux/aarch64/test
下有编译好的程序。这个mpp_info_test可以用来验证当前板上的mpp环境是否正常。将此文件放到板上,然后执行
./mpp_info_test
查看输出:此程序把打印接口改为系统log
,需要输入下面命令查看mpp输出
tail -10f /var/log/messages
如果能看到正常的输出,说明mpp的运行环境是没有问题的
MPP的工程里面自带了cmake,只要配置cmake即可编译其自带的demo。但实际应用中,很多时候是要将mpp功能集成到自己的工程里面,这个时候就需要具备链接mpp库的功能。下面通过介绍如何单独编译某个mpp demo,来说明如何链接mpp库。
首先拷贝一份mpp的demo源码,比如拷贝到mpp 工程的 mytest 目录下(此目录需要自行新建,也可以改成其它目录
)。这里比较常用的就是编解码功能,其demo源码分别为
test/mpi_dec_test.c
test/mpi_enc_test.c
然后cd到mpp的工程目录下,分别执行
aarch64-none-linux-gnu-gcc mytest/mpi_enc_test.c build/linux/aarch64/utils/libutils.a build/linux/aarch64/osal/libosal.a -Lbuild/linux/aarch64/mpp -lrockchip_mpp -I./osal/inc -I./osal/linux -I./inc -I./utils -o mytest/mpi_enc_test -lpthread -lstdc++
aarch64-none-linux-gnu-gcc mytest/mpi_dec_test.c build/linux/aarch64/utils/libutils.a build/linux/aarch64/osal/libosal.a -Lbuild/linux/aarch64/mpp -lrockchip_mpp -I./osal/inc -I./osal/linux -I./inc -I./utils -o mytest/mpi_dec_test -lpthread -lstdc++
可以编译出 mpi_enc_test 和 mpi_dec_test,也就是编码和解码的demo
其中libutils是一些辅助性的函数,比如crc计算等,demo里面会用到,用户自己的程序用不到的可以不引用
libosal是一些操作系统api,比如内存分配等,同样用户自己的程序用不到的可以不引用
下面使用demo中的mpi_enc_test进行演示,这个是用来编码的。所谓的编码,就是把原始的RAW图像数据,比如YUV422 YUV420 NV12 NV16这些数据格式的,进行压缩,压缩方式通常有H264 H265等,压缩之后体积减小,便于存放,传输。
首先要抓取一段摄像头的原始视频,这个抓取原始视频的方法可以参考前面的章节。
这里按1632x1224的分辨率抓取10帧数据并存放到/tmp/isp.out下。抓取之后使用ls -ll命令可以看到其大小。可以看出这种数据格式的图像非常占空间
然后使用如下命令进行编码
./mpi_enc_test -w 1632 -h 1224 -t 7 -i /tmp/isp.out -o /tmp/isp.h264 -n 10 -f 0
其中-w为宽度 -h为高 -t指定输出格式,7代表H264,-i -o分别是输入输出文件名,-n是帧数,-f指定输入格式,0代表YUV420SP, NV12。 这个编码命令具体参数要跟原始图像的格式匹配,否则会报错。
编码之后输出文件为/tmp/isp.h264,可以看出其占用空间大幅度下降。
mpp的相关输出信息可以查看syslog信息
tail -20f /var/log/messages
将/tmp/isp.h264和/tmp/isp.out文件复制到PC端的虚拟机ubuntu里面,然后使用命令
W=1632; H=1224; mplayer isp.out -loop 0 -demuxer rawvideo -fps 30 -rawvideo w=${W}:h=${H}:size=$((${W}*${H}*3/2)):format=NV12
可以播放isp.out
mplayer isp.h264 -loop 0
可以播放isp.h264
可以看出,虽然占用空间大幅度下降,其画面质量差不多。而这个就是使用板端编码器的意义。
解码则是将经过h264压缩后的图像还原为RAW格式,命令为
./mpi_dec_test -w 1632 -h 1224 -t 7 -i /tmp/isp.h264 -o /tmp/isp.h264.out -n 10 -f 0
理论上来说,解码之后的内容和编码前的大小应当是一致的。由于经过了压缩,会丢失部分细节,内容不完全一致,但播放时显示的内容差别不大
下面以demo中的mpi_enc_test源码为例,介绍一下mpp动态库的使用方法。
此源文件位置在mpp工程的test目录下
首先看下main函数,它首先使用mpi_enc_test_cmd_update_by_args解析出了所有的参数,然后通过enc_test_multi函数执行编码
int main(int argc, char **argv)
{
RK_S32 ret = MPP_NOK;
MpiEncTestArgs* cmd = mpi_enc_test_cmd_get();
// parse the cmd option
ret = mpi_enc_test_cmd_update_by_args(cmd, argc, argv);
if (ret)
goto DONE;
mpi_enc_test_cmd_show_opt(cmd);
ret = enc_test_multi(cmd, argv[0]);
DONE:
mpi_enc_test_cmd_put(cmd);
return ret;
}
mpi_enc_test_cmd_update_by_args函数就是逐个参数解析,并检查参数,然后把不全的参数补齐。
其中
MPP_RET mpp_opt_parse(MppOpt opt, int argc, char **argv);
的作用就是把执行mpi_enc_test命令时候的参数解析出来,并放到MppOpt opt里面。
MPP_RET mpi_enc_test_cmd_update_by_args(MpiEncTestArgs* cmd, int argc, char **argv)
{
MppOpt opts = NULL;
RK_S32 ret = -1;
RK_U32 i;
...
/* should change node count when option increases */
mpp_opt_setup(opts, cmd, 67, enc_opt_cnt);
for (i = 0; i < enc_opt_cnt; i++)
mpp_opt_add(opts, &enc_opts[i]);
/* mark option end */
mpp_opt_add(opts, NULL);
ret = mpp_opt_parse(opts, argc, argv);
...
if (!cmd->hor_stride)
cmd->hor_stride = mpi_enc_width_default_stride(cmd->width, cmd->format);
if (!cmd->ver_stride)
cmd->ver_stride = cmd->height;
...
if (cmd->rc_mode == MPP_ENC_RC_MODE_FIXQP) {
if (!cmd->qp_init) {
if (cmd->type == MPP_VIDEO_CodingAVC ||
cmd->type == MPP_VIDEO_CodingHEVC)
cmd->qp_init = 26;
}
}
if (cmd->trace_fps) {
fps_calc_init(&cmd->fps);
mpp_assert(cmd->fps);
fps_calc_set_cb(cmd->fps, show_enc_fps);
}
...
return ret;
}
之后main函数调用
int enc_test_multi(MpiEncTestArgs* cmd, const char *name);
此函数创建了一个线程,并执行
void *enc_test(void *arg);
函数,此函数就是循环执行编码操作
void *enc_test(void *arg)
{
...
mpp_log_q(quiet, "%s start\n", info->name);
...
ret = test_ctx_init(info);
...
ret = mpp_buffer_group_get_internal(&p->buf_grp, MPP_BUFFER_TYPE_DRM);
...
ret = mpp_buffer_get(p->buf_grp, &p->frm_buf, p->frame_size + p->header_size);
...
ret = mpp_buffer_get(p->buf_grp, &p->pkt_buf, p->frame_size);
...
ret = mpp_buffer_get(p->buf_grp, &p->md_info, p->mdinfo_size);
...
// encoder demo
ret = mpp_create(&p->ctx, &p->mpi);
...
mpp_log_q(quiet, "%p encoder test start w %d h %d type %d\n",
p->ctx, p->width, p->height, p->type);
ret = p->mpi->control(p->ctx, MPP_SET_OUTPUT_TIMEOUT, &timeout);
...
ret = mpp_init(p->ctx, MPP_CTX_ENC, p->type);
...
ret = mpp_enc_cfg_init(&p->cfg);
...
ret = test_mpp_enc_cfg_setup(info);
...
t_s = mpp_time();
ret = test_mpp_run(info);
t_e = mpp_time();
ret = p->mpi->reset(p->ctx);
...
return NULL;
}
此函数先初始化一个buffer,然后将输入文件的info,data等信息放到这个buffer里面,再调用
mpp_create
实例化一个MppCtx
然后使用
mpp_init
初始化它,再使用
mpp_enc_cfg_init
设置一下,最后在
test_mpp_run
中调用
mpi->encode_put_frame(ctx, frame);
mpi->encode_get_packet(ctx, &packet);
将一个frame的原始数据发送给mpp,并从mpp获取一个packet的编码后数据
将多个packet循环执行此操作,即得到最终的编码后文件
具体的MPP 的API可以参考MPP说明文档,在docs/Linux/Multimedia/Rockchip_Developer_Guide_MPP_CN.pdf里面。