VENC视频编码
VENC(Video Encoder)将YUV420SP格式的图片编码成H264/H265格式的视频码流。关于VENC功能的详细介绍及约束请参见VENC功能及约束说明。
本节介绍VENC视频编码的接口调用流程,同时配合示例代码辅助理解该接口调用流程。在实现VENC视频编码功能时,可在创建通道时设置基本参数、或调用对应的set接口设置高级参数,优化视频编码质量,请参见优化视频编码质量。
Atlas 200/300/500 推理产品上,当前版本不支持该功能。
Atlas 训练系列产品上,当前版本不支持该功能。
Atlas A2训练系列产品上,不支持该功能。
接口调用流程
开发应用时,如果涉及视频编码,则应用程序中必须包含编码的代码逻辑,关于编码的接口调用流程,请先参见AscendCL接口调用流程了解整体流程,再查看本节中的流程说明。
- 调用hi_mpi_sys_init接口进行媒体数据处理系统初始化;
- 调用hi_mpi_venc_create_chn函数创建完通道;
成功创建通道之后,您可以根据实际需求设置编码的高级参数,例如场景模式、码流控制器的高级参数等,请参见hi_mpi_venc_set_jpeg_param~hi_mpi_venc_compact_jpeg_tables章节中的接口说明。
- 调用hi_mpi_venc_get_fd将通道ID转换为一个文件句柄;
- 调用hi_mpi_sys_create_epoll函数创建DVPP epoll实例;
- 调用hi_mpi_sys_ctl_epoll函数将编码通道的文件句柄添加到epoll实例中,由epoll实例处理;
select或者poll方式,不需要执行该步骤。
- 调用hi_mpi_venc_start_chn函数通知通道准备开始编码;
- 调用hi_mpi_dvpp_malloc接口申请存放Device上输入数据的内存。
- 启动一个用户态线程,调用hi_mpi_sys_wait_epoll函数等待编码完成;
- 之后用户就可以调用hi_mpi_venc_send_frame函数发送待编码的码流;
- 一旦编码完成,hi_mpi_sys_wait_epoll函数或select函数或poll函数就会返回,用户就可以调用hi_mpi_venc_query_status接口查询编码状态,再调用hi_mpi_venc_get_stream函数获取编码结果;
- 用户需要注意的是,编码结果数据使用完成之后,需要及时调用hi_mpi_venc_release_stream函数释放buffer。否则会因编码buffer用完导致后续编码无法进行;
- 调用hi_mpi_dvpp_free接口释放输入内存;
- 当用户不需发送图像到目的通道继续编码时,需要调用hi_mpi_venc_stop_chn函数通知该通道不再接收新的输入图片;
- 调用hi_mpi_sys_ctl_epoll函数从epoll实例中删除编码通道的文件句柄;
- 当用户完成所有编码之后,需要调用hi_mpi_venc_destroy_chn释放编码通道以及内部内存资源;
- 调用hi_mpi_sys_close_epoll函数销毁DVPP epoll实例;
- 调用hi_mpi_sys_exit接口进行媒体数据处理系统去初始化。
优化视频编码质量
在实现VENC视频编码功能时,可在创建通道时设置基本参数、或调用对应的set接口设置高级参数,优化视频编码质量,以下调整手段可以叠加使用,效果是叠加的,例如:
- H264视频数据获取场景,分辨率720P,gop = 60,帧率30fps,码率1M需要提升编码质量,可以使用如下优化手段组合:CBR模式、HI_VENC_SCENE_0、stats_time等于2、profile等于2、关闭宏块级码控。
- H265电影场景,分辨率1080P,gop=30,帧率25fps,码率2M需要提升编码质量,可以使用如下优化手段组合:CBR模式、HI_VENC_SCENE_1、stats_time等于1、关闭宏块级码控。
当前支持以下方式优化视频编码质量:
- 设置基本参数,优化视频编码质量
不同分辨率的视频,其编码质量与视频的帧率、GOP(Group of pictures)、码率有关,在调用hi_mpi_venc_create_chn接口创建通道时,可设置编码的等级、设置H.264/H.265协议编码场景下CBR/VBR/AVBR/CVBR/QVBR模式的帧率、GOP、码率等参数,来调整视频编码质量:
- 编码等级,通过hi_venc_chn_attr.venc_attr结构内的profile参数来设置;
- 帧率,通过hi_venc_chn_attr.rc_attr结构体内的src_frame_rate输入帧率参数、dst_frame_rate输出帧率参数来设置;
- GOP,通过hi_venc_chn_attr.rc_attr结构体内的gop参数来设置;
- 码率,通过hi_venc_chn_attr.rc_attr结构体内的bit_rate或max_bit_rate或target_bit_rate参数来设置。
表1 典型场景下帧率、GOP、码率的取值 画质/分辨率
帧率
GOP
码率(Mbps)
4K
3840*2160/4096*2160
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
2K
2560*1440
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
1080P(蓝光)
1920*1080
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
720P(高清)
1280*720
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
480P/D1_N(标清)
854*480/720*480
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
576P/D1(标清)
720*576
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
270P(流畅)
480*270
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
CIF P/N
352*288/320*240
25或30
建议GOP为帧率的整数倍,例如帧率为25时,GOP建议25或50。
- 设置高级参数,调整视频编码细节
您可以调用接口设置码控模式、宏块级码率控制参数、编码场景模式等,来调整视频编码的细节,进一步改善编码质量。
表2 高级配置项列表 配置项
接口
参数名
说明
码控模式
hi_venc_chn_attr.rc_attr结构体内的rc_mode参数
追求码率平稳或追求PSNR大且码率符合目标值,配置为CBR;
追求节省码率,对主观编码质量有一定要求,配置为VBR;
追求节省码率,对主观编码质量有一定要求,且场景中有较多静止画面,配置为AVBR;
追求PSNR且对码率上浮没有严格要求,配置为QVBR;
追求节省码率,对主观编码质量有一定要求,且可以根据带宽、存储空间要求进行更多调整,配置为CVBR;
码率控制模型统计时间
hi_venc_chn_attr.rc_attr内各模式属性值结构体内的stats_time参数
关注长期码率稳定,短期波动不在意的可以设置大一些,例:DVR存盘。设大可以提高重编码判决的门槛,重编码次数会减少,但是码率波动会加大。
宏块级码率控制参数
hi_venc_rc_param结构内的threshold_i、threshold_p、threshold_b、direction、row_qp_delta参数
如果图像内容复杂、细节较多或用户关注PSNR等客观指标时,需关闭宏块级码率控制。
第一帧的起始Qp值
hi_venc_rc_param结构内的first_frame_start_qp参数
典型场景下,用户配置的码率小于表1中给的参考值,且编码后的视频第一帧明显模糊,则建议配置first_frame_start_qp参数,参数值取[min_i_qp, max_i_qp]的中间值,例如,[min_i_qp, max_i_qp]为[30, 40],则first_frame_start_qp参数配置为35,同时将max_reencode_times参数配置为0,会获得较好的编码质量。
编码场景模式
hi_venc_scene_mode
安防场景配置为HI_VENC_SCENE_0;自动驾驶、直播、游戏、动画、电影配置为HI_VENC_SCENE_1。
示例代码
您可以从样例介绍中获取完整样例代码。
调用接口后,需增加异常处理的分支,并记录报错日志、提示日志,此处不一一列举。以下是关键步骤的代码示例,不可以直接拷贝编译运行,仅供参考。
// 1.获取软件栈的运行模式,不同运行模式影响后续的接口调用流程(例如是否进行数据传输等) aclrtRunMode runMode; aclError aclRet = aclrtGetRunMode(&runMode); // 2.AscendCL初始化 aclRet = aclInit(nullptr); // 3.运行管理资源申请(依次申请Device、Context) aclrtContext g_context; aclRet = aclrtSetDevice(0); aclRet = aclrtCreateContext(&g_context, 0); // 4.初始化媒体数据处理系统 int32_t ret = hi_mpi_sys_init(); // 5.创建通道 hi_venc_chn chn = 0; hi_venc_chn_attr attr{}; attr.venc_attr.type = HI_PT_H265; attr.venc_attr.profile = 0; attr.venc_attr.max_pic_width = 128; attr.venc_attr.max_pic_height = 128; attr.venc_attr.pic_width = 128; attr.venc_attr.pic_height = 128; attr.venc_attr.buf_size = 2 * 1024 * 1024; attr.venc_attr.is_by_frame = HI_TRUE; attr.rc_attr.rc_mode = HI_VENC_RC_MODE_H265_VBR; attr.rc_attr.h265_vbr.gop = 30; attr.rc_attr.h265_vbr.stats_time = 1; attr.rc_attr.h265_vbr.src_frame_rate = 30; attr.rc_attr.h265_vbr.dst_frame_rate = 30; attr.rc_attr.h265_vbr.max_bit_rate = 4000; attr.gop_attr.gop_mode = HI_VENC_GOP_MODE_NORMAL_P; attr.gop_attr.normal_p.ip_qp_delta = 3; ret = hi_mpi_venc_create_chn(chn, &attr); // 6.通知编码器启动接收输入数据 hi_venc_start_param recv_param{}; recv_param.recv_pic_num = -1; ret = hi_mpi_venc_start_chn(chn, &recv_param); // 7.发送输入数据 // 7.1 申请输入内存 uint8_t* inputAddr = nullptr; int32_t inputSize = 128 * 128 * 3 / 2; ret = hi_mpi_dvpp_malloc(0, &inputAddr, inputSize); // 如果运行模式为ACL_HOST,则需要申请Host内存,将输入数据读入Host内存,再通过aclrtMemcpy接口将Host的数据传输到Device,数据传输完成后,需及时释放Host内存;否则直接将输入数据读入Device内存 if (runMode == ACL_HOST) { void* hostInputAddr = nullptr; // 申请Host内存 aclRet = aclrtMallocHost(&hostInputAddr, inputSize); // 将输入数据读入内存中,该自定义函数VencReadYuvFile由用户实现 VencReadYuvFile(streamName, hostInputAddr, inputSize); // 数据传输 aclRet = aclrtMemcpy(inputAddr, inputSize, hostInputAddr, inputSize, ACL_MEMCPY_HOST_TO_DEVICE); // 完成数据传输后,需及时释放不使用的内存 aclrtFreeHost(hostInputAddr ); hostInputAddr = nullptr; } else { // 将输入数据读入内存中,该自定义函数VencReadYuvFile由用户实现 VencReadYuvFile(streamName, inputAddr, inputSize); } // 7.2 发送输入数据,开始编码 hi_video_frame_info frame{}; frame.mod_id = HI_ID_VENC; frame.v_frame.width = 128; frame.v_frame.height = 128; frame.v_frame.field = HI_VIDEO_FIELD_FRAME; frame.v_frame.pixel_format = HI_PIXEL_FORMAT_YUV_SEMIPLANAR_420; frame.v_frame.video_format = HI_VIDEO_FORMAT_LINEAR; frame.v_frame.compress_mode = HI_COMPRESS_MODE_NONE; frame.v_frame.dynamic_range = HI_DYNAMIC_RANGE_SDR8; frame.v_frame.color_gamut = HI_COLOR_GAMUT_BT709; frame.v_frame.width_stride[0] = 128; frame.v_frame.width_stride[1] = 128; frame.v_frame.width_stride[2] = 128; frame.v_frame.virt_addr[0] = inputAddr; frame.v_frame.virt_addr[1] = (hi_void *)((uintptr_t)frame.v_frame.virt_addr[0] + 128 * 128); frame.v_frame.frame_flag = 0; frame.v_frame.time_ref = 0; frame.v_frame.pts = 0; ret = hi_mpi_venc_send_frame(chn, &frame, 0); // 8.获取编码结果 // 8.1 创建EPOLL实例 int32_t epollFd = 0; int32_t fd = hi_mpi_venc_get_fd(chn); ret = hi_mpi_sys_create_epoll(10, &epollFd); hi_dvpp_epoll_event event; event.events = HI_DVPP_EPOLL_IN; event.data = (void*)(unsigned long)(fd); ret = hi_mpi_sys_ctl_epoll(epollFd, HI_DVPP_EPOLL_CTL_ADD, fd, &event); int32_t eventCount = 0; // 编码完成前,会超时阻塞在这里,一旦完成,才会往下执行 ret = hi_mpi_sys_wait_epoll(epollFd, event, 3, 1000, &eventCount); // 8.2 获取编码结果 hi_venc_chn_status stat; ret = hi_mpi_venc_query_status(chn, &stat); hi_venc_stream stream; stream.pack_cnt = stat.cur_packs; stream.pack = new hi_venc_pack[stream.pack_cnt]; ret = hi_mpi_venc_get_stream(chn, &stream, 1000); // 8.3 如果运行模式为ACL_HOST,且Host上需要使用编码输出的码流,则需要申请Host内存,通过aclrtMemcpy接口将Device的输出码流传输到Host if (g_runMode == ACL_HOST) { void* hostOutputAddr = nullptr; aclRet = aclrtMallocHost(&hostOutputAddr, outputSize); aclRet = aclrtMemcpy(hostOutputAddr, outputSize, stream.pack[0].addr, outputSize, ACL_MEMCPY_DEVICE_TO_HOST); // ...... // 数据使用完成后,需及时释放不使用的内存 aclrtFreeHost(hostOutputAddr); hostOutputAddr = nullptr; } else { // 可以直接使用编码输出码流数据,在stream.pack[0].addr指向的内存中 // TODO: 推理相关的代码逻辑 } // 9.释放输入内存和输出码流 ret = hi_mpi_dvpp_free(inputAddr); ret = hi_mpi_venc_release_stream(chn, &stream); delete[] stream.pack; // 10.通知编码器停止接收输入数据 ret = hi_mpi_venc_stop_chn(chn); ret = hi_mpi_sys_ctl_epoll(epollFd, HI_DVPP_EPOLL_CTL_DEL, fd, NULL); ret = hi_mpi_sys_close_epoll(epollFd); // 11.销毁通道 ret = hi_mpi_venc_destroy_chn(chn); // 12.媒体数据处理系统去初始化 ret = hi_mpi_sys_exit(); // 13.释放运行管理资源(依次释放Context、Device) aclRet = aclrtDestroyContext(g_context); aclRet = aclrtResetDevice(0); // 14.AscendCL去初始化 aclRet = aclFinalize(); // ....