学习Jetson视频编码之前,先看以下文章:
本文对Jetpack 5.0.2的03_video_cuda_enc编码流程进行详细介绍,代码路径:/usr/src/jetson_multimedia_api/samples/03_video_cuda_enc/。之前的文章介绍了Jetson视频解码,地址:Jetson视频解码 编码和解码除了具体定义不一样,过程基本相似。同样理解了03_video_cuda_enc编码流程,samples中的其他编码demo就都理解了,如下图所示,为Jetson视频编码流程(03_video_cuda_enc):
下面对编码流程进行详细介绍:
1、打开编码器
ctx.enc = NvVideoEncoder::createVideoEncoder("enc0");
打开编码器实际就是用open()函数打开编码设备,类似于open()打开/dev/video0。
2、设置输出缓冲区
ctx.enc->setCapturePlaneFormat(ctx.encoder_pixfmt, ctx.width,ctx.height, 2 * 1024 * 1024);
设置输出缓冲区格式,输出缓冲区类型对应v4l2的type就是V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,encoder_pixfmt->H264:V4L2_PIX_FMT_H264;H265:V4L2_PIX_FMT_H265。
3、设置输入缓冲区
ctx.enc->setOutputPlaneFormat(V4L2_PIX_FMT_YUV420M, ctx.width, ctx.height)
设置输入缓冲区格式,输出缓冲区类型对应v4l2的type就是V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE,输入数据格式为V4L2_PIX_FMT_YUV420M,就是YUV420P。
4、设置H264/H265编码参数
ret = ctx.enc->setBitrate(ctx.bitrate);
TEST_ERROR(ret < 0, "Could not set bitrate", cleanup);
if (ctx.encoder_pixfmt == V4L2_PIX_FMT_H264)
{
ret = ctx.enc->setProfile(V4L2_MPEG_VIDEO_H264_PROFILE_HIGH);
}
else
{
ret = ctx.enc->setProfile(V4L2_MPEG_VIDEO_H265_PROFILE_MAIN);
}
TEST_ERROR(ret < 0, "Could not set encoder profile", cleanup);
if (ctx.encoder_pixfmt == V4L2_PIX_FMT_H264)
{
ret = ctx.enc->setLevel(V4L2_MPEG_VIDEO_H264_LEVEL_5_0);
TEST_ERROR(ret < 0, "Could not set encoder level", cleanup);
}
ret = ctx.enc->setFrameRate(ctx.fps_n, ctx.fps_d);
TEST_ERROR(ret < 0, "Could not set framerate", cleanup);
设置H264/H265编码格式,码率、Profile、Level、帧率等。
5、请求分配输入缓冲区
ctx.enc->output_plane.setupPlane(V4L2_MEMORY_MMAP, 10, true, false);
请求分配输入缓冲区(v4l2_requestbuffers),type为V4L2_MEMORY_MMAP表示内存映射缓冲区。
6、请求分配输出缓冲区
ctx.enc->capture_plane.setupPlane(V4L2_MEMORY_MMAP, 6, true, false);
请求分配输出缓冲区(v4l2_requestbuffers),type为V4L2_MEMORY_MMAP表示内存映射缓冲区。
7、编码器输入流开启
ctx.enc->output_plane.setStreamStatus(true);
就是调用ioctl(),请求类型为VIDIOC_STREAMON。
8、编码器输出流开启
ctx.enc->capture_plane.setStreamStatus(true);
就是调用ioctl(),请求类型为VIDIOC_STREAMON。
9、设置编码器输出回调函数
ctx.enc->capture_plane.setDQThreadCallback(encoder_capture_plane_dq_callback);
这里是设置回调函数,后面会开启一个线程用于获取编码器输出缓冲buffer,之后就会调用这里设置的回调函数,回调函数的工作就是获取buffer中已编码好的视频。
10、开启输出线程
ctx.enc->capture_plane.startDQThread(&ctx);
开启一个线程,用于从编码器输出缓冲队列capture_plane中获取准备好的缓冲buffer。
11、输出缓冲区入队
for (uint32_t i = 0; i < ctx.enc->capture_plane.getNumBuffers(); i++)
{
struct v4l2_buffer v4l2_buf;
struct v4l2_plane planes[MAX_PLANES];
memset(&v4l2_buf, 0, sizeof(v4l2_buf));
memset(planes, 0, MAX_PLANES * sizeof(struct v4l2_plane));
v4l2_buf.index = i;
v4l2_buf.m.planes = planes;
ret = ctx.enc->capture_plane.qBuffer(v4l2_buf, NULL);
if (ret < 0)
{
cerr << "Error while queueing buffer at capture plane" << endl;
abort(&ctx);
goto cleanup;
}
}
编码器输出缓冲区capture_plane全部送入编码器输出队列,准备接收编码后的视频数据。
12、获取输入的所有缓冲buffer(output_plane)、填充视频帧、缓冲buffer送入输入队列
for (uint32_t i = 0; i < ctx.enc->output_plane.getNumBuffers() &&
!ctx.got_error; i++)
{
struct v4l2_buffer v4l2_buf;
struct v4l2_plane planes[MAX_PLANES];
NvBuffer *buffer = ctx.enc->output_plane.getNthBuffer(i);
memset(&v4l2_buf, 0, sizeof(v4l2_buf));
memset(planes, 0, MAX_PLANES * sizeof(struct v4l2_plane));
v4l2_buf.index = i;
v4l2_buf.m.planes = planes;
if (read_video_frame(ctx.in_file, *buffer) < 0)
{
cerr << "Could not read complete frame from input file" << endl;
v4l2_buf.m.planes[0].bytesused = 0;
}
/**
* buffer is touched by CPU in read_video_frame(), so NvBufSurfaceSyncForDevice()
* is needed to flash cached data to memory.
*/
ret = sync_buf (buffer);
if (ret < 0)
{
cerr << "Error while sync_buf" << endl;
abort(&ctx);
goto cleanup;
}
/* render rectangle by CUDA */
ret = render_rect (&ctx, buffer);
if (ret < 0)
{
cerr << "Error while render_rect" << endl;
abort(&ctx);
goto cleanup;
}
ret = ctx.enc->output_plane.qBuffer(v4l2_buf, NULL);
if (ret < 0)
{
cerr << "Error while queueing buffer at output plane" << endl;
abort(&ctx);
goto cleanup;
}
if (v4l2_buf.m.planes[0].bytesused == 0)
{
cerr << "File read complete." << endl;
eos = true;
break;
}
}
这里的目的是询缓冲buffer信息,填充 YUV数据到buffer中,并把buffer送入编码器输入队列(output_plane)。
13、不断的从输入队列output_plane中获取空闲缓冲buffer、填充视频帧、重新送入编码器
while (!ctx.got_error && !ctx.enc->isInError() && !eos)
{
struct v4l2_buffer v4l2_buf;
struct v4l2_plane planes[MAX_PLANES];
NvBuffer *buffer;
memset(&v4l2_buf, 0, sizeof(v4l2_buf));
memset(planes, 0, sizeof(planes));
v4l2_buf.m.planes = planes;
if (ctx.enc->output_plane.dqBuffer(v4l2_buf, &buffer, NULL, 10) < 0)
{
cerr << "ERROR while DQing buffer at output plane" << endl;
abort(&ctx);
goto cleanup;
}
if (read_video_frame(ctx.in_file, *buffer) < 0)
{
cerr << "Could not read complete frame from input file" << endl;
v4l2_buf.m.planes[0].bytesused = 0;
}
/**
* buffer is touched by CPU in read_video_frame(), so NvBufSurfaceSyncForDevice()
* is needed to flash cached data to memory.
*/
ret = sync_buf (buffer);
if (ret < 0)
{
cerr << "Error while sync_buf" << endl;
abort(&ctx);
goto cleanup;
}
/* render rectangle by CUDA */
ret = render_rect (&ctx, buffer);
if (ret < 0)
{
cerr << "Error while render_rect" << endl;
abort(&ctx);
goto cleanup;
}
ret = ctx.enc->output_plane.qBuffer(v4l2_buf, NULL);
if (ret < 0)
{
cerr << "Error while queueing buffer at output plane" << endl;
abort(&ctx);
goto cleanup;
}
if (v4l2_buf.m.planes[0].bytesused == 0)
{
cerr << "File read complete." << endl;
eos = true;
break;
}
}
这里是不断地从输入队列output_plane中获取缓冲buffer、填充YUV、把缓冲buffer重新送入输入队列,这里就是开启正式的编码循环。
14、编码结束、释放资源
首先结束输出工作线程:
ctx.enc->capture_plane.waitForDQThread(2000)
之后就是释放资源、关闭编码设备。
15、encoder_capture_plane_dq_callback回调函数
static bool
encoder_capture_plane_dq_callback(struct v4l2_buffer *v4l2_buf, NvBuffer * buffer,
NvBuffer * shared_buffer, void *arg)
{
context_t *ctx = (context_t *) arg;
NvVideoEncoder *enc = ctx->enc;
if (!v4l2_buf)
{
cerr << "Failed to dequeue buffer from encoder capture plane" << endl;
abort(ctx);
return false;
}
// 把buffer中已编码的视频保存到文件中
write_encoder_output_frame(ctx->out_file, buffer);
/* qBuffer on the capture plane */
// 把buffer重新入队
if (enc->capture_plane.qBuffer(*v4l2_buf, NULL) < 0)
{
cerr << "Error while Qing buffer at capture plane" << endl;
abort(ctx);
return false;
}
/* GOT EOS from encoder. Stop dqthread. */
if (buffer->planes[0].bytesused == 0)
{
return false;
}
return true;
}
回调函数的作用把输出队列工作线程capture_plane获取的buffer中保存的编码后视频保存到文件。capture_plane工作线程从队列中获取缓冲buffer,需要在回调函数里面重新入队。
我的开源:
1、Nvidia视频硬解码、渲染、软/硬编码并写入MP4文件。项目地址:https://github.com/BreakingY/Nvidia-Video-Codec
2、Jetson Jetpack5.x视频编解码。项目地址:https://github.com/BreakingY/jetpack-dec-enc
3、ffmpeg音视频(H264/H265/AAC)封装、解封装、编解码pipeline,支持NVIDIA硬编解码。项目地址:https://github.com/BreakingY/FFmpeg-Media-Codec-Pipeline
4、simple rtsp server,小而高效的rtsp服务器,支持H264、H265、AAC、PCMA;支持TCP、UDP;支持鉴权。项目地址:https://github.com/BreakingY/simple-rtsp-server
5、simple rtsp client,rtsp客户端,支持TCP、UDP、H264、H265、AAC、PCMA,支持鉴权。项目地址:https://github.com/BreakingY/simple-rtsp-client
6、libflv,flv muxer/demuxer,支持H264/H265、AAC。项目地址:GitHub - BreakingY/libflv: flv muxer/demuxer,support H264/H265、AAC。