目录
一、前言
上一章节,利用V4L2框架完成了对摄像头数据的采集,然后通过opencv进行视频显示。这一章节我们完成使用采集到的数据,通过MPP组件进行H265编码,然后进行推流。
二、H265编码格式
在我zlmediakit组件详解(四)里讲解了H264的文件结构,以及在推流分包的时候需要注意哪些要点。所以这里我说明一下H265和H264文件之间的结构差异。
2.1 起始码:一致
-
H.264:
00 00 01或00 00 00 01 -
H.265:完全一样,也是使用 Annex-B 格式起始码
2.2 NALU 头格式:不同
H.264 NALU Header(1字节):
|F|NRI| Type | --> 共8位
-
F: forbidden_zero_bit (1)
-
NRI: nal_ref_idc (2)
-
Type: nal_unit_type (5)
H.265 NALU Header(2字节):
|F| Type(6bit) | LayerId(6bit) | TID(3bit) |
也就是说,H.265 的 NALU 头是 2 字节,并且类型字段的位置发生了变化!
2.3 RSTP协议中的sdp
m=video 0 RTP/AVP 96
a=rtpmap:96 H265/90000
a=control:track0
三、MPP组件使用
对于MPP组件的使用,rk官方平台有对应相关的介绍,官方github地址。我这里就讲解在应用过程中需要注意的点。
3.1下面是编码器的初始化
主要写入的是原始摄像头的宽高和填充的宽高、图像数据的格式、还有帧率和码率。
MppEncoderParams mpp_encoder_params;
memset(&mpp_encoder_params, 0, sizeof(MppEncoderParams));
mpp_encoder_params.width = WIDTH;
mpp_encoder_params.height = HEIGHT;
mpp_encoder_params.hor_stride = 832;//MPP_ALIGN(WIDTH, 16);
mpp_encoder_params.ver_stride = 512;//MPP_ALIGN(HEIGHT, 16);
mpp_encoder_params.fmt = MPP_FMT_YUV420SP;
mpp_encoder_params.rc_mode=MPP_ENC_RC_MODE_CBR;
mpp_encoder_params.fps_in_flex=0;
mpp_encoder_params.fps_in_num=30;
mpp_encoder_params.fps_in_den=1;
mpp_encoder_params.fps_out_num=30;
mpp_encoder_params.fps_out_den=1;
mpp_encoder_params.bps=2000000;
mpp_encoder_params.bps_max=4000000;
mpp_encoder_params.type=MPP_VIDEO_CodingHEVC;
if(mpp_encoder.Init(mpp_encoder_params,NULL))
{
fprintf(stderr, "mpp_encoder init failed\n");
}
上面需要额外注意的点就是水平和垂直的对齐长度,需要是32的整数倍,还要看原始图像的排列方式。我的视频流宽高是800X500。
编码器的输入图像宽高配置需要与图像数据在内存中的排布一致。以1920x1080大小的YUV420图像编码为例,假设有两种情况如下:

上图图情况:亮度分量的宽度为1920,高度为1080,亮度数据与色度数据不直接相接,中间有8行的空行。
这种情况下,水平stride为1920,垂直stride为1088,应用需要以1920*1088*3/2的大小分配空间并写入数据,使用宽1920,高1080,水平stride 1920,垂直stride 1088的配置即可以正常进行编码。
下图情况:亮度分量的宽度为1920,高度为1080,亮度数据与色度数据直接相接,中间没有空行。
这种情况下,水平stride为1920,垂直stride为1080,但由于编码器对数据的访问是16对齐的,在读取亮度下边缘数据时会读取到色度部分,读取色度下边缘数据时会读取到色度数据之外的部分,需要用户开出额外的空间,这里的空间为1920*1080*3/2+1920*4的填充,才能保证编码器不出现访问未分配空间的情况。
如果没有正确对齐的话,视频就会出现花屏的现象。
3.2下面代码初始化编码器头信息,然后是申请数据缓冲区
//初始化编码器信息头
char header_buf[1024];
int header_size = mpp_encoder.GetHeader(header_buf, 1024);
if(header_size>0)
{
queue_buf_deal(consumer_queue,header_buf,header_size);
printf("header wirte suc %d\n",header_size);
}
else
{
fprintf(stderr, "get header failed\n");
return -1;
}
MppBuffer frm_buf=(MppBuffer)mpp_encoder.GetInputFrameBuffer();//申请buf缓冲区
void * frm_ptr=mpp_encoder.GetInputFrameBufferAddr(frm_buf);
if(frm_ptr)
{
int frm_size=mpp_encoder.GetFrameSize();
printf("frame size %d\n",frm_size);
}
else
{
fprintf(stderr, "get input frame buffer failed\n");
return -1;
}
3.3 接下来就是encoder视频数据了,大致流程我是从队列中获取摄像头数据然后进行编码,然后将编码后的数据让放入另一个队列供后续的RTSP推流使用。
这里需要的核心点是对原始NV12摄像头数据的处理,因为原始数据宽高是800X500,但是经过填充后是832X512,所以要按照上面提到的地址对齐。
Video_frame_context frame_context;
size_t frame_count = 0;
const int hor_stride = 832;
const int ver_stride = 512;
while(1)
{
if(consumer_queue.pop_cam_data(frame_context))//摄像头数据队列
{
//printf("copy frame data to input frame buffer index %d\n",i);
uint8_t* dst_y = (uint8_t*)frm_ptr;
uint8_t* dst_uv = dst_y + hor_stride * ver_stride;
uint8_t* src_y = frame_context.frame_data;
uint8_t* src_uv = frame_context.frame_data + 800 * 500;
// Y
for (int i = 0; i < 500; i++)
memcpy(dst_y + i * hor_stride, src_y + i * 800, 800);
// UV
for (int i = 0; i < 250; i++)
memcpy(dst_uv + i * hor_stride, src_uv + i * 800, 800);
char enc_buf[2 * 1024 * 1024]; // 每帧最大2MB
int enc_size = mpp_encoder.Encode(frm_buf, enc_buf, sizeof(enc_buf));
if (enc_size > 0)
{
//编码器数据队列
queue_buf_deal(consumer_queue,enc_buf,enc_size);
//printf("Encoded frame %d size: %d\n", frame_count, enc_size);
}
}
else
{
perror("pop_cam_data failed\n");
break;
}
}
三、zlmediakit组件应用
关于zlmediakit的编译和应用,在我其他文章有介绍。zlmediakit组件详解。上面我们已经将摄像头数据进行H265的编码,并且将编码的数据放入了队列当中,接下来我们只需要从队列当中取数据然后进行推流即可。
对于zlmediakit的使用,我们就可以参照demo的方式,只不过将读取文件的流程,改成从队列中读取即可。直接贴上代码,这里需要注意的就是没帧发送的时间,还有就是目前存在两个队列,需要注意两个队列的缓存大小,要做内存防溢出处理。
static int exit_flag = 0;
static void s_on_exit(int sig) {
exit_flag = 1;
}
static void on_h264_frame(void *user_data, mk_h264_splitter splitter, const char *data, int size) {
#ifdef _WIN32
Sleep(40);
#else
// usleep(10 * 1000);
#endif
uint64_t dts = mk_util_get_current_millisecond();
mk_frame frame = mk_frame_create(MKCodecH265, dts, dts, data, size, NULL, NULL);
mk_media_input_frame((mk_media) user_data, frame);
mk_frame_unref(frame);
}
int rtsp_server(ProducerConsumerQueue &consumerQueue)
{
/* code */
char *ini_path = mk_util_get_exe_dir("config.ini");
mk_config config = {
.thread_num = 0,
.log_level = 0,
.log_mask = LOG_CONSOLE,
.log_file_path = NULL,
.log_file_days = 0,
.ini_is_path = 1,
.ini = ini_path,
.ssl_is_path = 1,
.ssl = NULL,
.ssl_pwd = NULL,
};
mk_env_init(&config);
mk_free(ini_path);
mk_rtsp_server_start(554, 0);
printf("rtsp server start\n");
mk_events events = {
.on_mk_media_changed = NULL,
.on_mk_media_publish = NULL,
.on_mk_media_play = NULL,
.on_mk_media_not_found = NULL,
.on_mk_media_no_reader = NULL,
.on_mk_http_request = NULL,
.on_mk_http_access = NULL,
.on_mk_http_before_access = NULL,
.on_mk_rtsp_get_realm = NULL,
.on_mk_rtsp_auth = NULL,
.on_mk_record_mp4 = NULL,
.on_mk_shell_login = NULL,
.on_mk_flow_report = NULL
};
mk_events_listen(&events);
mk_media media = mk_media_create("__defaultVhost__", "live", "test", 0, 0, 0);
// h264的codec [AUTO-TRANSLATED:940c6a32]
// h264 codec
//mk_media_init_video(media, 0, 0, 0, 0, 2 * 104 * 1024);
codec_args v_args = {0};
mk_track v_track = mk_track_create(MKCodecH265, &v_args);
mk_media_init_track(media, v_track);
mk_media_init_complete(media);
mk_track_unref(v_track);
// 创建h264分帧器 [AUTO-TRANSLATED:5775837d]
// Create h264 frame splitter
mk_h264_splitter splitter = mk_h264_splitter_create(on_h264_frame, media,1);
signal(SIGINT, s_on_exit);// 设置退出信号
char buf[1024];
while (!exit_flag) {
mpp_encoder_context item={0};
consumerQueue.pop_encoder_data(item) ;
printf(" pop Encoded frame size: %d\n", item.data_size);
if (item.data_size > 0) {
mk_h264_splitter_input_data(splitter, (char *)item.encoder_data, item.data_size );
}
}
mk_h264_splitter_release(splitter);
mk_stop_all_server();
return 0;
至此我们就完成过了利用V4L2框架读取摄像头数据,使用opencv在本地屏幕显示,使用mpp组件进行H265编码,使用zlmediakit推流,其中涉及的技术框架还是挺多的,需要细细的去熟悉理解。整个工程文件代码已上传。
418

被折叠的 条评论
为什么被折叠?



