这篇文章,详细讲解了用FFmpeg的API代码的方式,如何把摄像头的录制的视频,保存为MP4、YUV格式,会详细介绍视频的相关知识,已经遇到的问题文章目录YUVYUV采样格式与RGB比较YUV格式间的转换视频的比特率(bit_rate)、帧率(framerate)、分辨率I、B、P帧time_base三种时间基 tbr、tbn 和 tbcPTS、DTSPTS的转换对frame的pts,进行解码器-编码器的时间基转换编码后,对packet,进行编码器-容器的时间基转换录制视频的代码:YUVYUV中 “Y” 表示明亮度( Luminance 或 Luma ),也就是灰阶值;而 “U” 和 “V” 表示的则是色度( Chrominance 或 Chroma ),作用是描述影像色彩及饱和度,用于指定像素的颜色。早期技术不发达的时候,照片打印和电视播放都只能实现黑白的灰度显示,所以Y数据就成了标准。之后才有了彩色打印和彩色电视,为了兼容之前的黑白数据,发明了UV数据。YUV一起就可以实现彩色,只用Y数据,可实现黑白画面,这样一套数据格式就同时兼容了黑白设备和彩色设备。人眼对亮点信息更敏感。也就是说,可以压缩UV数据,而人眼难以发现。把RGB数据转换成YUV数据。对Y少压缩一点,对UV多压缩一点,以平衡图像效果和压缩率。YUV采样格式#NV12、NV21的存储格式为Y平面,UV打包一个平面,共两个平面,即:先连续存储Y,然后连续交叉存储UV# 不同点在于UV的排列顺序,SP为Semi-Planar的缩写 #NV12:先是 w*h 长度的Y,后面跟 w*h*0.5长度的UV(交叉存储),总长度为 w*h*1.5#NV21:先是 w*h 长度的Y,后面跟 w*h*0.5长度的VU(交叉存储),总长度为 w*h*1.5NV12:YYYYYYYYUVUV=YUV420SP# iOS 平台常用NV12NV21:YYYYYYYYVUVU=YUV420SP#Android平台常用NV21#I420、YV12三个分量均为平面格式,共三个平面,即:先连续存储Y,然后连续存储U,最后连续存储V# 不同点在于UV的排列顺序,P为Planar的缩写 #I420:先是 w*h 长度的Y,后面跟 w*h*0.25长度的U, 最后是 w*h*0.25长度的V,总长度为 w*h*1.5#YV12:先是 w*h 长度的Y,后面跟 w*h*0.25长度的V, 最后是 w*h*0.25长度的U,总长度为 w*h*1.5I420:YYYYYYYYUUVV=YUV420P#Android平台常用I420YV12:YYYYYYYYVVUU=YUV420P按照颜色可找到Y对应的UV值,以上图的NV12为例, 每个像素点都有一个Y值,像素1、2、7、8位置的Y值是Y1、Y2,、Y7、Y8(蓝色) 对应的UV是U1,V1(蓝色),像素1=Y1U1V1, 像素2=Y2U1V1,以此类推。知道了存储格式,就可以把上面二维的YUV值(上图只是为了方便理解),连续存储了,如下图:与RGB比较一帧width*height的图像RGB格式,R/G/B各需要1byte(8bit),总存储空间=width*height*3byte,YUV420P格式,Y存储空间 = width*height*1byte ,一个U用于4个像素,平均到单个像素用到1/4个U,U存储空间 = width*height*1/4byte,同理V存储空间 = width*height*1/4byte,总存储空间= width*height*(1byte + 1/4byte + 1/4byte),占用空间比RGB减少了一半。YUV格式间的转换例如:NV12 转 YUV420P#includestdint.h#includestring.hvoidnv12_to_yuv420p(uint8_t*src_yuv,uint8_t*dst_yuv,intwidth,intheight){inty_size=width*height;intuv_size=y_size/2;// NV12 UV plane size (interleaved)intu_size=y_size/4;// YUV420P U or V plane sizeinti;// Copy Y plane directly (identical in both formats)memcpy(dst_yuv,src_yuv,y_size);// Pointers to U and V planes in YUV420Puint8_t*dst_u=dst_yuv+y_size;// U plane starts after Yuint8_t*dst_v=dst_yuv+y_size+u_size;// V plane starts after U// Pointer to NV12 UV planeuint8_t*src_uv=src_yuv+y_size;// Deinterleave NV12 UV plane into separate U and V planesfor(i=0;iuv_size/2;i++){dst_u[i]=src_uv[2*i];// U values are at even indicesdst_v[i]=src_uv[2*i+1];// V values are at odd indices}}从转换代码可以看出,YUV只有像素的数据,所有YUV格式保存到文件,没有包装头,读取后可直接对YUV操作,所以视频的原始数据就是YUV视频的比特率(bit_rate)、帧率(framerate)、分辨率帧率每秒中视频帧数量 ,单位为 fps(frame per second), 帧率越高 , 视频越流畅 ;在ffmpeg中,为编码器设置帧率// 设置帧率encoder_codec_ctx-time_base=(AVRational){1,30};encoder_codec_ctx-framerate=(AVRational){30,1};framerate 和 time_base 值互为倒数,time_base 是每一帧视频的持续时间,此处是1/30s,framerate 是30fps。 后续需要根据解码器 time_base和编码器的time_base,来计算pts分辨率分辨率 = 宽度的像素点数* 高度的像素点数比特率每秒的bit数量,单位为 bps(bit per second)。 一个文件的比特率 =文件大小(字节)X8/ 文件时长(秒))分辨率越大、帧率越大,都会使比特率增大。为了保证视频的观看体验,这三者关系如下:I、B、P帧想起了大学时做的一款桌面远程控制软件,把桌面一张一张截图,发送过来,那个延时,适合树獭用。视频也是一个个帧,连续播放的效果,其中两帧之间,大部分时间差异很小,所以可以通过算法来提取一下相邻两帧之间的不变的画面,减少重复数据量。这个算法比较流行的就是H.264,经过压缩后,帧分为 I、B、P帧通俗理解:I帧 就是完整的一帧图片,P帧 是与它前面I帧或P帧的差异部分,可以与前帧结合,还原出一帧图。B帧 需要与前后的I帧或P帧,进行解析,还原出一帧图。I帧:帧内编码帧(intra picture),又称全帧压缩编码帧。I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,作为随机访问的参考点,可以当成静态图像。P帧:前向预测编码帧(predictive-frame),通常将图像序列中前面已经编码帧的冗余信息去除,达到压缩传输数据量的效果,也称为预测帧。B帧:双向预测内插编码帧(bi-directional interpolated prediction frame),既考虑源图像序列前面的已编码帧,又顾及源图像序列后面的已编码帧之间的冗余信息,来压缩传输数据量的编码图像,也称为双向预测帧。以图中“B[1]”帧为例进行说明,“B[1]”帧解码时需要参考“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。采集顺序指图像传感器采集原始信号得到图像帧的顺序。编码顺序指编码器编码后图像帧的顺序。存储到磁盘的本地视频文件中图像帧的顺序与编码顺序相同。传输顺序指编码后的流在网络中传输过程中图像帧的顺序。解码顺序指解码器解码图像帧的顺序。显示顺序指图像帧在显示器上显示的顺序。采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。直播时,不使用B帧,因为解析B帧,需要先接收到更后面的I/P帧,降低了实时性。直播秒开,也可以通过设置更小的GOP(一般是5),能更快的找到I帧,显示出画面在没有B帧的情况下,存放帧的顺序和显示帧的顺序就是一样的,PTS和DTS的值也是一样的。time_base在 FFmpeg 中,时间基(time_base)是时间戳(timestamp)的单位,时间戳值乘以时间基,可以得到实际的时刻值(以秒等为单位)。例如,如果一个视频帧的 dts 是 40,pts 是 160,其 time_base 是 1/1000 秒,那么可以计算出此视频帧的解码时刻是 40 毫秒(40/1000),显示时刻是 160 毫秒(160/1000)。三种时间基 tbr、tbn 和 tbc使用ffmpeg 、ffplay 操作视频时,经常会看到视频的这些参数,它们是不同的时间基不同的封装格式具有不同的时间基。在 FFmpeg 处理音视频过程中的不同阶段,也会采用不同的时间基。FFmepg 中有三种时间基,命令行中 tbr、tbn 和 tbc 的打印值就是这三种时间基的倒数:tbn:对应容器中的时间基。值是 AVStream.time_base 的倒数tbc:对应编解码器中的时间基。值是 AVCodecContext.time_base 的倒数tbr:从视频流中猜算得到,可能是帧率或场率(帧率的 2 倍)PTS、DTSDTS(Decoding Time Stamp, 解码时间戳),表示压缩帧的解码时间。PTS(Presentation Time Stamp, 显示时间戳),表示将压缩帧解码后得到的原始帧的显示时间。音频中 DTS 和 PTS 是相同的。视频中由于 B 帧需要双向预测,B 帧依赖于其前和其后的帧,因此含 B 帧的视频解码顺序与显示顺序不同,即 DTS 与 PTS 不同。不含 B 帧的视频,其 DTS 和 PTS 是相同的。PTS的转换编码器、解码器、要写入的容器(例如写到MP4的容器) 都有自己的time_base,上面说过,pts、dts的值的单位是time_base,那么如果time_base不相同,pts还是原样复制过去,播放时就会有问题。如果是录制视频,pts的起点不是从0开始,如不处理,会导致录制完的视频时间特别长。下面以摄像头录制文件为例,介绍pts的处理:对frame的pts,进行解码器-编码器的时间基转换staticvoidprocess_frame_use_decode(AVPacket*pkt,AVFrame*decoded_frame,AVFrame*converted_frame,AVCodecContext*av_decoder_ctx,AVCodecContext*h264_encoder_ctx,AVFormatContext*ofmtCtx,structSwsContext*sws_ctx,intframeIndex){//av_read_frame 接收到的是原始数据,也可以解码后再发送给编码器。如果接收到是编码后的数据packet,需要解码后再发送给编码器intret=avcodec_send_packet(av_decoder_ctx,pkt);if(ret0){printf("Decode error.\n");return;}if(avcodec_receive_frame(av_decoder_ctx,decoded_frame)=0){// frame的格式,转换成YUV420Psws_scale(sws_ctx,decoded_frame-data,decoded_frame-linesize,0,av_decoder_ctx-height,converted_frame-data,converted_frame-linesize);// Store the first PTS valueif(first_pts==AV_NOPTS_VALUE){first_pts=decoded_frame-pts;}//打印av_decoder_ctx-time_base、 h264_encoder_ctx-time_baseprintf("av_decoder_ctx time_base: %d/%d\n",av_decoder_ctx-time_base.num,av_decoder_ctx-time_base.den);printf("h264_encoder_ctx time_base: %d/%d\n",h264_encoder_ctx-time_base.num,h264_encoder_ctx-time_base.den);// 得到相对于起始时间first_pts的PTS,pts从0开始,保证了视频时长的正确性converted_frame-pts=decoded_frame-pts-first_pts;// 将 decoded_frame 的 PTS 进行时间基转换,传递给 converted_frameconverted_frame-pts=av_rescale_q(converted_frame-pts,av_decoder_ctx-time_base,h264_encoder_ctx-time_base);// 打印pts,pkt_dts,// frame 的pkt_dts 是取自packet的dts,此处打印pkt_dts,意义不大,只是想看看// 经过h264编码后,会自动设置packet的dtsprintf("decoded_frame pts: %ld, pkt_dts: %ld\n",decoded_frame-pts,decoded_frame-pkt_dts);printf("converted_frame pts: %ld, pkt_dts: %ld\n",converted_frame-pts,converted_frame-pkt_dts);encode_and_write_frame(h264_encoder_ctx,converted_frame,ofmtCtx,pkt,frameIndex);}}转换后的一系列 converted_frame-pts,是0、1、2、3、、、,说明转换对了编码后,对packet