x265笔记_2_整体调试和初步的源码分析

x265的入口函数main

x265的入口函数是cli\Source Files\x265.cpp中的main函数,该函数的执行流程:

  1. 解析并设置参数,cliopt.parse()cliopt.output->setParam;根据参数创建编码器encoder;参数初始化
  2. 编码主循环,判断输入帧数inFrameCount是否超过要编码的帧数framesToBeEncoded,如果超过设置要读取的帧数pic_in等于NULL,并跳出循环,否则读入第inFrameCount帧,参数初始化后调用api->encoder_encode进行编码,outFrameCount保存当前已编码并输出的帧数;
  3. Flush the Encoder,如果跳出了上个大循环,但是没有按Ctrl + C,则会把大循环中未编码的帧进行编码,同样outFrameCount保存已编码完成并输出的帧数;
  4. 编码器关闭,编码结束。

main调用的部分函数

main函数调用的部分函数:

  1. 获取控制台窗口的函数GetConsoleTitle,传入两个参数,分别是orgConsoleTitle指向一个缓冲区指针以接收包含控制台标题的字符串,CONSOLE_TITLE_SIZEorgConsoleTitle所指向的缓冲区的大小。如果函数成功,返回以字符为单位长度的控制台窗口标题,否则返回0。
  2. 解析参数的函数cliopt.parse(argc,argv),直接调用x265.cpp中的bool CLIOptions::parse()函数,会打印输入视频的分辨率、帧率、视频格式、所要编码的帧的数目、输出文件名称等等。
  3. 打印编码器配置信息的函数encoder_open(param),直接调用x265_encoder* x265_encoder_open(x265_param* p)
  4. 编码函数x265_encoder_encode
  5. 关闭编码器的函数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函数主要包括三个部分:

  1. 分析是否是错误造成的代码终止;
  2. 判断是否有输入帧,如果有则判断该输入帧的像素深度、颜色空间是否支持,并判断List是否为空,如果为空的话则创建List;初始化ret=0,如果没有可供输出的重构帧则返回ret=0,否则返回ret=1;
  3. 使用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

Reference

x265探索与研究_成长Bar的博客专栏-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值