文章目录
x265的入口函数main
x265的入口函数是cli\Source Files\x265.cpp
中的main
函数,该函数的执行流程:
- 解析并设置参数,
cliopt.parse()
和cliopt.output->setParam
;根据参数创建编码器encoder
;参数初始化 - 编码主循环,判断输入帧数
inFrameCount
是否超过要编码的帧数framesToBeEncoded
,如果超过设置要读取的帧数pic_in
等于NULL
,并跳出循环,否则读入第inFrameCount
帧,参数初始化后调用api->encoder_encode
进行编码,outFrameCount
保存当前已编码并输出的帧数; - Flush the Encoder,如果跳出了上个大循环,但是没有按
Ctrl + C
,则会把大循环中未编码的帧进行编码,同样outFrameCount
保存已编码完成并输出的帧数; - 编码器关闭,编码结束。
main
调用的部分函数
main
函数调用的部分函数:
- 获取控制台窗口的函数
GetConsoleTitle
,传入两个参数,分别是orgConsoleTitle
指向一个缓冲区指针以接收包含控制台标题的字符串,CONSOLE_TITLE_SIZE
为orgConsoleTitle
所指向的缓冲区的大小。如果函数成功,返回以字符为单位长度的控制台窗口标题,否则返回0。 - 解析参数的函数
cliopt.parse(argc,argv)
,直接调用x265.cpp中的bool CLIOptions::parse()
函数,会打印输入视频的分辨率、帧率、视频格式、所要编码的帧的数目、输出文件名称等等。 - 打印编码器配置信息的函数
encoder_open(param)
,直接调用x265_encoder* x265_encoder_open(x265_param* p)
- 编码函数
x265_encoder_encode
- 关闭编码器的函数
encoder_close
编码函数api->encoder_encode
可见,我们想要了解的就是在api->encoder_encode()
里面,我们看一下它的声明:int x265_encoder_encode(x265_encoder *encoder, x265_nal **pp_nal, uint32_t *pi_nal, x265_picture *pic_in, x265_picture *pic_out);
官方还有一段关于该函数的注释:
x265_encoder_encode:
编码一张图片,输入参数中
*pi_nal
是输出的pp_nal
的NAL单元数;返回负数代表错误,返回1代表一张图片或者输入单元被输出,返回0代表编码器pipeline在flushing之后仍然是满的或者是空的。所有输出NAL的payloads在内存中保证是连续的。为了刷新(flush)编码器并延迟输出图片,将
pic_in
作为NALL
传入,一旦刷新开始,所有的后续调用必须将pic_in
作为NALL
传入。
该函数位于encoder\Source Files\api.cpp
中,一共203行,但是去掉SVT-HEVC部分,结构上就很明了了:
int x265_encoder_encode(x265_encoder *enc, x265_nal **pp_nal, uint32_t *pi_nal, x265_picture *pic_in, x265_picture *pic_out)
{
if (!enc)
return -1;
Encoder *encoder = static_cast<Encoder*>(enc);// 强制类型转换
int numEncoded;
#ifdef SVT_HEVC
...
#endif
// While flushing, we cannot return 0 until the entire stream is flushed
do
{
numEncoded = encoder->encode(pic_in, pic_out);
}
while ((numEncoded == 0 && !pic_in && encoder->m_numDelayedPic && !encoder->m_latestParam->forceFlush) && !encoder->m_externalFlush);
if (numEncoded)
encoder->m_externalFlush = false;
// do not allow reuse of these buffers for more than one picture. The
// encoder now owns these analysisData buffers.
if (pic_in)
{
pic_in->analysisData.wt = NULL;
pic_in->analysisData.intraData = NULL;
pic_in->analysisData.interData = NULL;
pic_in->analysisData.distortionData = NULL;
}
if (pp_nal && numEncoded > 0 && encoder->m_outputCount >= encoder->m_latestParam->chunkStart)
{
*pp_nal = &encoder->m_nalList.m_nal[0];
if (pi_nal) *pi_nal = encoder->m_nalList.m_numNal;
}
else if (pi_nal)
*pi_nal = 0;
if (numEncoded && encoder->m_param->csvLogLevel && encoder->m_outputCount >= encoder->m_latestParam->chunkStart)
x265_csvlog_frame(encoder->m_param, pic_out);
if (numEncoded < 0)
encoder->m_aborted = true;
return numEncoded;
}
可以看到api->encoder_encode
也是一个架子,实质上是调用项目encoder
下面的int Encode::encode
函数,下面我们再来看看这个函数的实现。
Encoder::encode
看一下Encoder::encode()
的注释:
/**
* Feed one new input frame into the encoder, get one frame out. If pic_in is NULL, a flush condition is implied and pic_in must be NULL for all subsequent calls for this encoder instance.
*
* pic_in input original YUV picture or NULL
* pic_out pointer to reconstructed picture struct
*
* returns 0 if no frames are currently available for output
* 1 if frame was output, m_nalList contains access unit
* negative on malloc error or abort */
/**
* 输入为一帧图像,输出为一帧图像
* 如果输入图像pic_in为NULL,则要修改flush条件,后续调用的pic_in也必须是NULL
* pic_in 原始YUV输入或者NULL
* pic_out 指向重建图像结构体的指针
* 返回结果为0,则没有输出帧,返回结果为1,则有帧输出,而且m_nalList包含进单元,返回结果为负数,则代表发生了malloc error或者中止*/
int Encoder::encode(const x265_picture* pic_in, x265_picture* pic_out){...}
Encoder::encode
函数主要包括三个部分:
- 分析是否是错误造成的代码终止;
- 判断是否有输入帧,如果有则判断该输入帧的像素深度、颜色空间是否支持,并判断List是否为空,如果为空的话则创建List;初始化ret=0,如果没有可供输出的重构帧则返回ret=0,否则返回ret=1;
- 使用do-while循环判断是否有输出,如果有则ret=1,且调用
startCompressFrame()
函数,该函数的主要目的是触发线程,为进一步编码做准备。
Encoder::encode()
的代码框架为:
int Encoder::encode(const x265_picture* pic_in, x265_picture* pic_out)
{
// 首先检查中止错误
#if CHECKED_BUILD || _DEBUG
if (g_checkFailures) {...}
#endif
if (m_aborted)
return -1;
if (m_exportedPic){...}
// 如果输入pic_in不为空,首先判断颜色空间、像素深度是否支持,再检查List
if (pic_in && (!m_param->chunkEnd || (m_encodedFrameNum < m_param->chunkEnd))) {...}
else if (m_latestParam->forceFlush == 2)
m_lookahead->m_filled = true;
else
m_lookahead->flush();
// 创建编码器 curEncoder
FrameEncoder *curEncoder = m_frameEncoder[m_curEncoder];
m_curEncoder = (m_curEncoder + 1) % m_param->frameNumThreads;
int ret = 0;
// do-while 循环调用`startCompressFrame`进行编码
Frame* outFrame = NULL;
Frame* frameEnc = NULL;
int pass = 0;
do {...}
while (m_bZeroLatency && ++pass < 2);
return ret;
}
关于这里的do-while循环,官方的解释是
/* Normal operation is to wait for the current frame encoder to complete its current frame
* and then to give it a new frame to work on. In zero-latency mode, we must encode this
* input picture before returning so the order must be reversed. This do/while() loop allows
* us to alternate the order of the calls without ugly code replication */
/*正常操作是等待当前帧编码器完成其当前帧,然后给它一个新的帧进行处理。在零延迟(zero-latency)模式下,我们必须在返回之前对输入图片进行编码,因此顺序必须颠倒。这个do/while()循环允许我们交替调用顺序,而无需进行丑陋的代码复制*/
这里,我们对do-while循环里的操作很感兴趣,这里主要做了以下操作:
/*
* 初始化outFrame=NULL,grameEnc=NULL
* do-while start
** 对于非zero-latency的情况,如果当前帧还没有处理完毕,getEncodedPicture()会进入阻塞状态;对于zero-latency的情况,可以直接执行后续操作
** if outFrame!=NULL
*** 初始化slice实例和frameData
*** 如果存在输出pic_out, 将分析数据保存在pic_in->analysisData中
*** 判断帧类型-I/P/B
*** 如果出现中断错误,即m_aborted==True 返回-1
*** 在多通道编码中写入帧级别的码率控制信息
*** 如果帧编码器没有使用此帧作为参考,则允许回收此帧
*** 如果有输出,ret=1
** endif outFrame!=NULL
** 在决定列表中弹出单个帧frameEnc,配置Encoder信息后, 调用线程函数`curEncoder->startCompressFrame(frameEnc)`对frameEnc进行编码,并设置m_aborted=True,用于中断错误的检查
可见,这里重点是最后调用了curEncoder->startCompressFrame(frameEnc)
对frameEnc
进行编码,下面重点来看这个函数。
FrameEncoder::startCompressFrame
该函数位于encoder\x265_NS::FrameEncoder
中,代码很简单:
bool FrameEncoder::startCompressFrame(Frame* curFrame)
{
m_slicetypeWaitTime = x265_mdate() - m_prevOutputTime;
m_frame = curFrame;
m_sliceType = curFrame->m_lowres.sliceType;
curFrame->m_encData->m_frameEncoderID = m_jpId;
curFrame->m_encData->m_jobProvider = this;
curFrame->m_encData->m_slice->m_mref = m_mref;
if (!m_cuGeoms)
{
if (!initializeGeoms())
return false;
}
m_enable.trigger();
return true;
}
主要是调用了一个事件的开关函数m_enable.trigger()
,可见如果想要探究如何编码的,还需要多线程的知识,我们需要先学习多线程的内容,然后才能深刻理解这个事件背后的含义,这也是x265相比HM要更加复杂的关键所在。
函数调用关系
main
|
api->encoder_encode
|
Encoder::encode
|
FrameEncoder::startCompressFrame
|
m_enable.trigger