前言
前段时间一直在搞视频编解码与传输的东西,现在基本上整个系统能够实现服务器实时编码打包传输给客户端,客户端实时解码并显示的功能,故将上一阶段的工作整理记录一下,将我使用x264进行h.264编码的过程,使用jrtplib进行h264数据包传输的过程以及使用ffmpeg进行解码的过程都记录下来,一方面是自己做一个总结,另外一方面也是想帮助那些跟我做同样工作的人更快的熟悉这些内容。
下面,首先开始我对x264这个库的进行h.264视频编码的介绍。在我的博客中之前我已经介绍过如何在windows下编译x264的库,所以这里不再叙述,其实我个人觉得windows下编译这个库没有意义,只是浪费时间,因为即使在windows下编译出来可以使用的库,依然是不能跟进库里面看程序的执行的(也可能是我水平不够,没有发现此方法,但是依然我博客中的那种方法是不能跟进程序内部的)。倘若能在网上直接找到一个编译好的库拿来直接使用则是最好的,所以我将我的编译好的库上传到我的github,需要的请戳这里(里面还包含已经编译好的ffmpeg库)。
在我使用这个库进行视频编码的时候,我发现网上能够找到的资源非常少,x264也并没有提供相应的api文档,从网上找到的x264学习笔记也是看着让我非常郁闷,时间比较久了不说,写的也是不清不楚,对于我这种现在仅仅需要知道怎样使用这个库的人,还是作用不大,于是自己将x264在Linux下编译了一下,跟进x264.c的运行终于把x264编码的流程搞懂了,虽然其中一些参数还是一知半解。
H264基本知识
想要使用x264进行h.264的视频编码,首先还是需要对h.264编码有一定的了解,下面我先对h.264编码的一些基本知识进行介绍,如果想深入了解,可以参考毕厚杰的《新一代视频压缩编码标准》这本书。
首先,我们必须知道h264支持三种不同的档次:
1、基本档次:主要用于“视频会话”,如会议电视,可视电话,远程医疗、远程教学等;
2、扩展档次:主要用于网络的视频流,如视频点播;
3、主要档次:主要用于消费电子应用,如数字电视广播,数字视频存储等。
这三种不同的档次导致其在编码过程中使用的编码的功能的差异,具体来说如下:
1)基本档次:利用I 片和P 片支持帧内和帧间编码,支持利用基于上下文的自适应的变长编码进行的熵编码(CAVLC)。主要用于可视电话、会议电视、无线通信等实时视频通信;
2)主要档次:支持隔行视频,采用B 片的帧间编码和采用加权预测的帧内编码;支持利用基于上下文的自适应的算术编码(CABAC)。主要用于数字广播电视与数字视频存储;
3)扩展档次:支持码流之间有效的切换(SP 和SI 片)、改进误码性能(数据分割),但不支持隔行视频和CABAC。
上面提到的I,P,B,SP,SI定义如下:一个编码图像通常划分成若干宏块组成,一个宏块由一个16×16 亮度像素和附加的一个8×8 Cb和一个8×8 Cr 彩色像素块组成。每个图象中,若干宏块被排列成片的形式。
I 片只包含I 宏块,P 片可包含P 和I 宏块,而B 片可包含B 和I 宏块。
I 宏块利用从当前片中已解码的像素作为参考进行帧内预测(不能取其它片中的已解码像素作为参考进行帧内预测)。
P 宏块利用前面已编码图象作为参考图象进行帧内预测,一个帧内编码的宏块可进一步作宏块的分割:即16×16、16×8、8×16 或8×8 亮度像素块(以及附带的彩色像素);如果选了8×8 的子宏块,则可再分成各种子宏块的分割,其尺寸为8×8、8×4、4×8 或4×4 亮度像素块(以及附带的彩色像素)。
B 宏块则利用双向的参考图象(当前和未来的已编码图象帧)进行帧内预测。
现在我们再由每种档次所能使用的编码功能我们就能很容易的理解每种档次所使用的场合了。比如说视频会话系统,我们只能得到当前帧或者当前帧之前的帧,所以我们只能采用基本档次。
然后,我们要了解H264编码的数据格式:
首先,我们必须知道H264编码输入的为YUV420格式的图像(关于YUV420,请戳这里)。如果为RGB32的图像,我们必须首先转换为YUV420格式。
然后,我们需要知道H264的功能分为两层,即视频编码层(VCL,Video coding layer )和网络提取层(NAL,Network Abstraction Layer)。VCL 数据即编码处理的输出,它表示被压缩编码后的视频数据序列。在VCL 数据传输或存储之前,这些编码的VCL 数据,先被映射或封装进NAL 单元中。每个NAL 单元包括一个原始字节序列负荷(RBSP)、一组对应于视频编码数据的NAL 头信息。NAL 单元序列的结构见图1.
图1,nal单元序列结构示意图
其中NAL header为0x0000 00 01或者0x00 00 01.
知道这些基本上我们就可以开始使用x264进行h264编码了.
使用X264编码视频
至于怎样用vs创建工程以及包含库文件我在这里就不说了,搜索之后一大堆。下面主要讲述下使用这个库的编码过程。主要的步骤都在注释中说明
- int main(int argc, char** argv)
- {
- int iResult = 0;
- x264_t* pX264Handle = NULL;
- x264_param_t* pX264Param = new x264_param_t;
- assert(pX264Param);
- //* 配置参数
- //* 使用默认参数,在这里因为我的是实时网络传输,所以我使用了zerolatency的选项,使用这个选项之后就不会有delayed_frames,如果你使用的不是这样的话,还需要在编码完成之后得到缓存的编码帧
- x264_param_default_preset(pX264Param, ”veryfast”, “zerolatency”);
- //* cpuFlags
- pX264Param->i_threads = X264_SYNC_LOOKAHEAD_AUTO;//* 取空缓冲区继续使用不死锁的保证.
- //* 视频选项
- pX264Param->i_width = 352; //* 要编码的图像宽度.
- pX264Param->i_height = 288; //* 要编码的图像高度
- pX264Param->i_frame_total = 0; //* 编码总帧数.不知道用0.
- pX264Param->i_keyint_max = 10;
- //* 流参数
- pX264Param->i_bframe = 5;
- pX264Param->b_open_gop = 0;
- pX264Param->i_bframe_pyramid = 0;
- pX264Param->i_bframe_adaptive = X264_B_ADAPT_TRELLIS;
- //* Log参数,不需要打印编码信息时直接注释掉就行
- pX264Param->i_log_level = X264_LOG_DEBUG;
- //* 速率控制参数
- pX264Param->rc.i_bitrate = 1024 * 10;//* 码率(比特率,单位Kbps)
- //* muxing parameters
- pX264Param->i_fps_den = 1; //* 帧率分母
- pX264Param->i_fps_num = 10;//* 帧率分子
- pX264Param->i_timebase_den = pX264Param->i_fps_num;
- pX264Param->i_timebase_num = pX264Param->i_fps_den;
- //* 设置Profile.使用Baseline profile
- x264_param_apply_profile(pX264Param, x264_profile_names[0]);
- //* 编码需要的辅助变量
- iNal = 0;
- pNals = NULL;
- x264_picture_t* pPicIn = new x264_picture_t;
- x264_picture_t* pPicOut = new x264_picture_t;
- x264_picture_init(pPicOut);
- x264_picture_alloc(pPicIn, X264_CSP_I420, pX264Param->i_width, pX264Param->i_height);
- pPicIn->img.i_csp = X264_CSP_I420;
- pPicIn->img.i_plane = 3;
- //* 打开编码器句柄,通过x264_encoder_parameters得到设置给X264
- //* 的参数.通过x264_encoder_reconfig更新X264的参数
- pX264Handle = x264_encoder_open(pX264Param);
- assert(pX264Handle);
- //* 创建文件,用于存储编码数据
- FILE* pFile = fopen(“test.264”, “wb”);
- assert(pFile);
- //设置y4m文件参数
- y4m_input_t *y4m_hnd = (y4m_input_t*)malloc(sizeof(y4m_input_t));
- //打开y4m文件
- iResult = open_file_y4m(”benchmark.y4m”,(hnd_t**)&y4m_hnd,pX264Param);
- if(iResult < 0 )
- {
- printf(”Failed to open file!\n”);
- return 0;
- }
- //得到文件总得帧数
- int nFrames = ::get_frame_total_y4m((hnd_t*)y4m_hnd);
- //开始编码
- for(int i = 0; i < nFrames ; i++ )
- {
- //读取一帧
- read_frame_y4m(pPicIn,(hnd_t*)y4m_hnd,i);
- if( i ==0 )
- pPicIn->i_pts = i;
- else
- pPicIn->i_pts = i - 1;
- //编码
- int frame_size = x264_encoder_encode(pX264Handle,&pNals,&iNal,pPicIn,pPicOut);
- if(frame_size >0)
- {
- for (int i = 0; i < iNal; ++i)
- {//将编码数据写入文件.
- fwrite(pNals[i].p_payload, 1, pNals[i].i_payload, pFile);
- }
- }
- }
- //* 清除图像区域
- x264_picture_clean(pPicIn);
- //* 关闭编码器句柄
- x264_encoder_close(pX264Handle);
- pX264Handle = NULL;
- delete pPicIn ;
- pPicIn = NULL;
- delete pPicOut;
- pPicOut = NULL;
- delete pX264Param;
- pX264Param = NULL;
- return 0;
- }
int main(int argc, char** argv)
{
int iResult = 0;
x264_t* pX264Handle = NULL;
x264_param_t* pX264Param = new x264_param_t;
assert(pX264Param);
//* 配置参数
//* 使用默认参数,在这里因为我的是实时网络传输,所以我使用了zerolatency的选项,使用这个选项之后就不会有delayed_frames,如果你使用的不是这样的话,还需要在编码完成之后得到缓存的编码帧
x264_param_default_preset(pX264Param, "veryfast", "zerolatency");
//* cpuFlags
pX264Param->i_threads = X264_SYNC_LOOKAHEAD_AUTO;//* 取空缓冲区继续使用不死锁的保证.
//* 视频选项
pX264Param->i_width = 352; //* 要编码的图像宽度.
pX264Param->i_height = 288; //* 要编码的图像高度
pX264Param->i_frame_total = 0; //* 编码总帧数.不知道用0.
pX264Param->i_keyint_max = 10;
//* 流参数
pX264Param->i_bframe = 5;
pX264Param->b_open_gop = 0;
pX264Param->i_bframe_pyramid = 0;
pX264Param->i_bframe_adaptive = X264_B_ADAPT_TRELLIS;
//* Log参数,不需要打印编码信息时直接注释掉就行
pX264Param->i_log_level = X264_LOG_DEBUG;
//* 速率控制参数
pX264Param->rc.i_bitrate = 1024 * 10;//* 码率(比特率,单位Kbps)
//* muxing parameters
pX264Param->i_fps_den = 1; //* 帧率分母
pX264Param->i_fps_num = 10;//* 帧率分子
pX264Param->i_timebase_den = pX264Param->i_fps_num;
pX264Param->i_timebase_num = pX264Param->i_fps_den;
//* 设置Profile.使用Baseline profile
x264_param_apply_profile(pX264Param, x264_profile_names[0]);
//* 编码需要的辅助变量
iNal = 0;
pNals = NULL;
x264_picture_t* pPicIn = new x264_picture_t;
x264_picture_t* pPicOut = new x264_picture_t;
x264_picture_init(pPicOut);
x264_picture_alloc(pPicIn, X264_CSP_I420, pX264Param->i_width, pX264Param->i_height);
pPicIn->img.i_csp = X264_CSP_I420;
pPicIn->img.i_plane = 3;
//* 打开编码器句柄,通过x264_encoder_parameters得到设置给X264
//* 的参数.通过x264_encoder_reconfig更新X264的参数
pX264Handle = x264_encoder_open(pX264Param);
assert(pX264Handle);
//* 创建文件,用于存储编码数据
FILE* pFile = fopen("test.264", "wb");
assert(pFile);
//设置y4m文件参数
y4m_input_t *y4m_hnd = (y4m_input_t*)malloc(sizeof(y4m_input_t));
//打开y4m文件
iResult = open_file_y4m("benchmark.y4m",(hnd_t**)&y4m_hnd,pX264Param);
if(iResult < 0 )
{
printf("Failed to open file!\n");
return 0;
}
//得到文件总得帧数
int nFrames = ::get_frame_total_y4m((hnd_t*)y4m_hnd);
//开始编码
for(int i = 0; i < nFrames ; i++ )
{
//读取一帧
read_frame_y4m(pPicIn,(hnd_t*)y4m_hnd,i);
if( i ==0 )
pPicIn->i_pts = i;
else
pPicIn->i_pts = i - 1;
//编码
int frame_size = x264_encoder_encode(pX264Handle,&pNals,&iNal,pPicIn,pPicOut);
if(frame_size >0)
{
for (int i = 0; i < iNal; ++i)
{//将编码数据写入文件.
fwrite(pNals[i].p_payload, 1, pNals[i].i_payload, pFile);
}
}
}
//* 清除图像区域
x264_picture_clean(pPicIn);
//* 关闭编码器句柄
x264_encoder_close(pX264Handle);
pX264Handle = NULL;
delete pPicIn ;
pPicIn = NULL;
delete pPicOut;
pPicOut = NULL;
delete pX264Param;
pX264Param = NULL;
return 0;
}
关于其中用到的benchmark.y4m与y4m.h的文件,请戳
这里下载.这样,保存下来的test.264就是编码好的文件,可以使用ffmpeg进行解码成为MP4、mkv等等使主流播放器能够播放。