征程 6E/M 快速上手实战 Sample-Codec
01 Codec 模块简述
Codec(Coder-Decoder)是指编解码器,用于压缩或解压缩视频、图像、音频等媒体数据;J6 Soc 中存在两种硬件编解码单元,分别是 VPU(Video process unit)和 JPU(Jpeg process unit),可提供 4K@90fps 的视频编解码能力和 4K@90fps 的图像编解码能力。
1.1 硬件特性
1.1.1 JPU 硬件特性:
1.1.2 VPU 硬件特性:
1.2 软件功能
MediaCodec 子系统会提供音视频和图像的编解码组件,原始流封装和视频录像等功能。该系统主要会封装底层 codec 硬件资源和软件编解码库,为上层提供编解码能力。开发者可以基于提供的编解码接口实现 H265 和 H264 视频的编解码功能,也可以使用 JPEG 编码功能将摄像头数据存成 JPEG 图片,还可以使用视频录像功能实现摄像头数据的录制。
1.2.1 整体框架:
1.2.2 控制接口:
hb_s32 hb_mm_mc_initialize(media_codec_context_t *context):初始化编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_INITIALIZED 状态。
hb_s32 hb_mm_mc_configure(media_codec_context_t *context):配置编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_CONFIGURED 状态。
hb_s32 hb_mm_mc_start(media_codec_context_t *context, const mc_av_codec_startup_params_t *info):启动编码/解码流程,MediaCodec 将创建编解码实例、设置序列或解析数据流、注册 Framebuffer、编码头信息等,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_STARTED 状态。
hb_s32 hb_mm_mc_stop(media_codec_context_t *context):停止编码/解码流程,退出所有子线程并释放相关资源,调用成功后 MediaCodec 回到 MEDIA_CODEC_STATE_INITIALIZED 状态。
hb_s32 hb_mm_mc_release(media_codec_context_t *context):释放 MediaCodec 内部所有资源,用户需要在调用该函数前调用 hb_mm_mc_stop 来停止编解码,操作成功后 MediaCodec 进入 MEDIA_CODEC_STATE_UNINITIALIZED 状态。
hb_s32 hb_mm_mc_queue_input_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, hb_s32 timeout):填充需要处理的 buffer 到 MediaCodec 中。
hb_s32 hb_mm_mc_dequeue_input_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, hb_s32 timeout):获取输入的 buffer。
hb_s32 hb_mm_mc_queue_output_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, hb_s32 timeout):返还处理完的 output buffer 到 MediaCodec 中。
hb_s32 hb_mm_mc_dequeue_output_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, media_codec_output_buffer_info_t *info, hb_s32 timeout):获取输出的 buffer。
1.2.3 码率控制模式:
MediaCodec 支持对 H264/H265 和 MJPEG 协议的码率控制,分别支持 H264/H265 编码通道的 CBR、VBR、AVBR、FixQp 和 QpMap 五种码率控制方式,以及支持 MJPGE 编码通道的 FixQp 码率控制方式。
*1.2.3.1 CBR 说明:*
CBR 表示恒定码率,能够保证整体的编码码率稳定。下面是 CBR 模式下各个参数含义:
*1.2.3.2 VBR 说明:*
VBR 表示可变码率,简单场景分配比较大的 qp,压缩率小,质量高。复杂场景分配较小 qp,可以保证编码图像的质量稳定。下面是 VBR 模式下各个参数含义:
1.2.3.3 AVBR 说明:
ABR 表示恒定平均目标码率,简单场景分配较低码率,复杂场景分配足够码率,使得有限的码率能够在不同场景下合理分配,这类似 VBR。同时一定时间内,平均码率又接近设置的目标码率,这样可以控制输出文件的大小,这又类似 CBR。可以认为是 CBR 和 VBR 的折中方案,产生码率和图像质量相对稳定的码流。下面是 AVBR 模式下各个参数含义:
1.2.3.4 FixQp 说明:
FixQp 表示固定每一个 I 帧、P 帧的 QP 值,对于 I/P 帧可以分别设值。下面是 FixQp 模式下各个参数含义:
1.2.3.5 QPMAP 说明:
1.2.4 编码效果:
根据当前客户使用 codec 进行视频编码的场景,多将码率模式设置为 CBR,当编码的场景较为复杂时,为了保证视频质量,硬件会自动提高码率值,导致输出的视频较预期更大。因此为了兼顾视频质量和实际码率,需要统筹 bit_rate 和 max_qp_I/P 值的设置。下面给出了全 I 帧模式下,不同复杂场景下,码率设置为 15000kbps 时,不同 max_qp_I 下实际码率和 qp 的情况(不同场景复杂程度不同,下列数据仅供参考):
H264/H265 编码支持 GOP 结构的设置,用户可从预置的 3 种 GOP 结构种选择,也可自定义 GOP 结构。
GOP 结构表可定义一组周期性的 GOP 结构,该 GOP 结构将用于整个编码过程。单个结构表中的元素如下表所示,其中可以指定该图像的参考帧,如果 IDR 帧后的其他帧指定的参考帧为 IDR 帧前的数据帧,编码器内部会自动处理这种情况使其不参考其他帧,用户无需关心这种情况。用户在自定义 GOP 结构时需要指明结构表的数量,最多可定义 3 个结构表,结构表的顺序需要按照解码顺序排列。下面表示了结构表中各个元素的含义:
征程6 中一共支持设置九种 GOP 预置结构:
02 Codec-Sample 使用
编码 yuv 图像, 生成 h264/h265 视频或 jpg 图片。
2.1 encoder
** **
2.1.1 调用流程
采用 MediaCodec 的 poll 模式来解耦输入和输出,可使编码帧率性能达到最优。在主线程中灌 YUV 数据:取出一个空的 input buffer,配置 YUV 数据的地址信息(如 phys addr),再 queue input buffer 并通知编码器处理该帧数据;另一个线程取输出码流:通过 select 接收硬件编码完成通知,取出一个硬件填满输出码流的 output buffer,将编码结果写到文件中后归还 output buffer。
check_and_init_test:打开输入文件(yuv),并打开内存管理模块申请内存缓冲;
hb_mm_mc_initialize:初始化编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_INITIALIZED 状态;
hb_mm_mc_configure:配置编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_CONFIGURED 状态;
hb_mm_mc_start:启动编码/解码流程,MediaCodec 将创建编解码实例、设置序列或解析数据流、注册 Framebuffer、编码头信息等,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_STARTED 状态;
hb_mm_mc_dequeue_input_buffer:获取输入的 buffer;
read_input_frames:从输入文件(yuv)中读取视频帧数据,并将其刷新到 buffer 中;
hb_mm_mc_queue_input_buffer:填充需要处理的 buffer 到 MediaCodec 中;
hb_mm_mc_dequeue_output_buffer:获取输出的 buffer;
write_output_streams:outputbuffer 写入 outFile 中;
hb_mm_mc_queue_output_buffer:返还处理完的 output buffer 到 MediaCodec 中;
hb_mm_mc_stop:停止编码/解码流程,退出所有子线程并释放相关资源,调用成功后 MediaCodec 回到 MEDIA_CODEC_STATE_INITIALIZED 状态;
hb_mm_mc_release:释放 MediaCodec 内部所有资源,操作成功后 MediaCodec 进入 MEDIA_CODEC_STATE_UNINITIALIZED 状态。
2.1.2 源码主干:
#Sample源码路径
/test/samples/platform_samples/source/S83_Sample/S83E04_Module/codec_sample
Encoder:
void do_sync_encoding(void *arg)
{hb_s32 ret = 0;MediaCodecTestContext *ctx = (MediaCodecTestContext *)arg;media_codec_context_t *context = NULL;media_codec_buffer_t inputBuffer;media_codec_buffer_t outputBuffer;media_codec_output_buffer_info_t info;ret = check_and_init_test(ctx);if (ret != 0) {printf("check_and_init_test failed(%d).\n", ret);return;}context = ctx->context;ret = hb_mm_mc_initialize(context);if (ret != 0) {printf("hb_mm_mc_initialize failed(%d).\n", ret);return;}// set_message(ctx);ret = hb_mm_mc_configure(context);if (ret != 0) {printf("hb_mm_mc_configure failed(%d).\n", ret);hb_mm_mc_release(context);return;}mc_av_codec_startup_params_t startup_params;startup_params.video_enc_startup_params.receive_frame_number = 0;ret = hb_mm_mc_start(context, &startup_params);if (ret != 0) {printf("hb_mm_mc_start failed(%d).\n", ret);hb_mm_mc_release(context);return;}do {// process input buffersmemset(&inputBuffer, 0x00, sizeof(media_codec_buffer_t));ret = hb_mm_mc_dequeue_input_buffer(context, &inputBuffer, 3000);if (ret == 0) {ret = read_input_frames(ctx, &inputBuffer);if (ret <= 0) {printf("There is no more input data(ret=%d)!\n", ret);inputBuffer.vframe_buf.size = 0;inputBuffer.vframe_buf.frame_end = TRUE;}ctx->input_num++;// ASSERT_EQ(do_encode_params_setting(ctx, &inputBuffer), 0);ret = hb_mm_mc_queue_input_buffer(context, &inputBuffer, 100);if (ret != 0) {break;}} else {printf("dequeue input buffer fail(%d).\n", ret);if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {break;}}// process output buffersmemset(&outputBuffer, 0x00, sizeof(media_codec_buffer_t));memset(&info, 0x00, sizeof(media_codec_output_buffer_info_t));ret = hb_mm_mc_dequeue_output_buffer(context, &outputBuffer, &info, 3000);if (ret == 0) {write_output_streams(ctx, &outputBuffer);ret = hb_mm_mc_queue_output_buffer(context, &outputBuffer, 100);if (outputBuffer.vstream_buf.stream_end) {printf("There is no more output data!\n");break;}if (ret) {break;}} else {printf("dequeue output buffer fail(ret=0x%x).\n", ret);if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {break;}}} while(TRUE);ret = hb_mm_mc_stop(context);if (ret != 0) {printf("hb_mm_mc_stop failed(%d).\n", ret);}ret = hb_mm_mc_release(context);if (ret != 0) {printf("hb_mm_mc_release failed(%d).\n", ret);}check_and_release_test(ctx);}
2.2 decoder
VPU/JPU 从 DDR 中获得 H265/H264/JPG 输入源,经过硬件解码后生成 yuv 图像
2.2.1 调用流程
check_and_init_test:打开输入文件(h265),并打开内存管理模块申请内存缓冲;
hb_mm_mc_initialize:初始化编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_INITIALIZED 状态;
hb_mm_mc_configure:配置编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_CONFIGURED 状态;
hb_mm_mc_start:启动编码/解码流程,MediaCodec 将创建编解码实例、设置序列或解析数据流、注册 Framebuffer、编码头信息等,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_STARTED 状态;
hb_mm_mc_dequeue_input_buffer:获取输入的 buffer;
hb_mm_mc_dequeue_output_buffer:获取输出的 buffer;
write_output_streams:outputbuffer 写入 outFile 中;
hb_mm_mc_queue_output_buffer:返还处理完的 output buffer 到 MediaCodec 中;
hb_mm_mc_stop:停止编码/解码流程,退出所有子线程并释放相关资源,调用成功后 MediaCodec 回到 MEDIA_CODEC_STATE_INITIALIZED 状态;
hb_mm_mc_release:释放 MediaCodec 内部所有资源,操作成功后 MediaCodec 进入 MEDIA_CODEC_STATE_UNINITIALIZED 状态。
** **
2.2.2 源码主干:
void do_sync_decoding(void *arg)
{int ret = 0;MediaCodecTestContext *ctx = (MediaCodecTestContext *)arg;media_codec_context_t *context = NULL;media_codec_buffer_t inputBuffer;media_codec_buffer_t outputBuffer;media_codec_output_buffer_info_t info;ret = check_and_init_test(ctx);if (ret != 0) {printf("check_and_init_test failed(%d).\n", ret);return;} context = ctx->context;context = ctx->context;ret = hb_mm_mc_initialize(context);if (ret != 0) {printf("hb_mm_mc_initialize failed(%d).\n", ret);return;}ret = hb_mm_mc_configure(context);if (ret != 0) {printf("hb_mm_mc_configure failed(%d).\n", ret);hb_mm_mc_release(context);return;}mc_av_codec_startup_params_t startup_params;startup_params.video_enc_startup_params.receive_frame_number = 0;ret = hb_mm_mc_start(context, &startup_params);if (ret != 0) {printf("hb_mm_mc_start failed(%d).\n", ret);hb_mm_mc_release(context);return;}do {// process input buffersret = hb_mm_mc_dequeue_input_buffer(context, &inputBuffer, 3000);if (ret == 0) {ret = read_input_streams(ctx, &inputBuffer);if (ret <= 0) {printf("There is no more input data(ret=%d)!\n", ret);inputBuffer.vstream_buf.stream_end = TRUE;inputBuffer.vstream_buf.size = 0;ctx->lastStream = 1;}ret = hb_mm_mc_queue_input_buffer(context, &inputBuffer, 100);if (ret != 0) {break;}} else {if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {char info[256];hb_mm_strerror(ret, info, 256);printf("dequeue input buffer fail.(%s)\n", info);break;}}// process output buffersmemset(&outputBuffer, 0x00, sizeof(media_codec_buffer_t));memset(&info, 0x00, sizeof(media_codec_output_buffer_info_t));ret = hb_mm_mc_dequeue_output_buffer(context, &outputBuffer, &info, 100);if (ret == 0) {printf("info.video_frame_info.nalu_type %d\n", info.video_frame_info.nalu_type);write_output_frames(ctx, &outputBuffer);if (ctx->enable_get_userdata) {mc_user_data_buffer_t userdata = {0};ret = hb_mm_mc_get_user_data(context, &userdata, 0);if (!ret) {printf("Get userdata %d:\n", userdata.size);for (uint32_t i = 0; i < userdata.size; i++) {if (i < 16) {printf("userdata[i]:%x\n", userdata.virt_addr[i]);} else {printf("userdata[i]:%c\n", userdata.virt_addr[i]);}}ret = hb_mm_mc_release_user_data(context, &userdata);} else {ret = 0;}}ret = hb_mm_mc_queue_output_buffer(context, &outputBuffer, 100);if (outputBuffer.vframe_buf.frame_end) {printf("There is no more output data!\n");break;}if (ret) {break;}} else {char info[256];hb_mm_strerror(ret, info, 256);printf("dequeue output buffer fail.(%s)\n", info);if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {break;}}} while (TRUE);ret = hb_mm_mc_stop(context);if (ret != 0) {printf("hb_mm_mc_stop failed(%d).\n", ret);}ret = hb_mm_mc_release(context);if (ret != 0) {printf("hb_mm_mc_release failed(%d).\n", ret);}check_and_release_test(ctx);
}
2.3 编译&运行
获取 AppSDK 包后,进入 appuser 执行:
*其中 hbrootfs-sdk_0.0.1.XXX_all.deb 是地平线自己的库和头文件,rootfs-sdk-focal_0.0.1.XXX_all.deb 是系统库,aarch64-linux-hb-gcc_12.2.0_amd64.deb 是 gcc 12.2.0 工具链,目前在 ubuntu22.04 非 docker 环境下运行正常。其它环境不能保证。
dpkg-deb -x rootfs-sdk*.deb ./sdk
dpkg-deb -x hbrootfs-sdk*.deb ./sdk
##移动sdk库路径,本文档放入/usr/lib中
sudo mv sdk/ /usr/lib
进入 toolchain 执行:
dpkg -x aarch64-linux-hb-gcc_12.2.0_amd64.deb ./arm-gnu-toolchain
##移动toolchain库路径,本文档放入/usr/lib中
sudo mv arm-gnu-toolchain/ /usr/lib
nano ~/.bashrc
##添加系统路径
export PATH="/usr/lib/arm-gnu-toolchain/bin:$PATH"
export LD_LIBRARY_PATH="/usr/lib/arm-gnu-toolchain/lib:$LD_LIBRARY_PATH"
##
source ~/.bashrc
Sample 代码路径:
#Sample源码路径
/test/samples/platform_samples/source/S83_Sample/S83E04_Module/codec_sample
运行参数说明:
复制/src 源码到新建文件夹 codec 并构建新 Makefile:
codec
├── Makefile
└── src├── sample.c├── sample_common.c├── sample.h├── sample_vdec.c└── sample_venc.c
Makefile:
CROSS_COMPILE = aarch64-none-linux-gnu-
OUTPUT_HBROOTFS_DIR = /usr/lib/sdkCXX := ${CROSS_COMPILE}gccINC_DIR := ${OUTPUT_HBROOTFS_DIR}/usr/hobot/include
INC_DIR += ${OUTPUT_HBROOTFS_DIR}/include
LIB_DIR := ${OUTPUT_HBROOTFS_DIR}/usr/hobot/lib
LIB_DIR += ${OUTPUT_HBROOTFS_DIR}/usr/lib/aarch64-linux-gnu
LIBS += -lpthread -ldl -lhbmem -lalog -lmultimedia
LIBS += -lavformat -lavcodec -lavutil -lswresample
CXXFLAGS := -Wall -O2 $(foreach dir,$(INC_DIR),-I$(dir))
LDFLAGS := $(addprefix -L, $(LIB_DIR)) $(LIBS)SRC_DIR := src
TARGET := program
SRCS := $(wildcard $(SRC_DIR)/*.c)OBJS := $(SRCS:.c=.o)$(TARGET): $(OBJS)$(CXX) $(CXXFLAGS) $(LDFLAGS) $^ -o $@
%.o: %.c$(CXX) $(CXXFLAGS) -c $< -o $@
clean:rm -f $(OBJS) $(TARGET)
执行 make 完成编译,生成的文件为。/program
使用 WinScp 将 program 传输到单板上。*需要 encode 的 yuv 文件请用户自行准备,本文档使用 PYM 生成的 yuv 文件。
*WinScp 使用方法请参考征程 6E/M 底软开发 Sample-IPC 2.1.2
通过 ssh 或串口进入/home/hobot/执行:*w 和 h 需要进行 16 字节对齐,如原始 yuv 不支持则会出现数据丢失
chmod +x program
#encode
#代码可参考源码目录中的codec_sample.sh
./program -m 0 -c 3 -w 3840 -h 2160 -p 1 -i 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv -o ./3840x2160.jpg
./program -m 0 -c 1 -w 3840 -h 2160 -p 1 -i 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv -o ./3840x2160.h265
Sample 运行时日志:
g_samplemode = 0
g_codecid = 3
g_width = 3840
g_height = 2160
g_pixfmt = 1
g_pixfmt = 1
g_inputfilename = 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv
g_outputfilename = ./3840x2160.jpg
InputFileName = 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv
OutputFileName = ./3840x2160.jpg
Thread use internal buffer mode, 0 rc mode
Failed to read input file (size=12441600)
There is no more input data(ret=0)!
There is no more output data!
生成的缩放 jpg 文件存放在指定-o 目录下。
生成的 Jpeg 效果如下:
*有关 jpg 工具查看 1080p 图片时出现绿边问题:
问题说明:这是因为当前 ip 进行编码时按照 16 位对齐进行,假如到最后如果是 8 位对齐而不是 16 位对齐,那么编码器就会在后面补齐,这部分补齐的数据是随机产生的,不属于有效数据;
#decode
#使用上述生成的3840x2160.h265为例
./program -m 1 -c 1 -w 3840 -h 2160 -p 1 -i 3840x2160.h265 -o ./3840x2160.yuv
使用 YUView 打开生成的 3840x2160.yuv:
*注意配置 offset:
附件:
1、[codec.7z]