FFmpeg流程

- 从本地读取YUV数据编码为h264格式的数据,然后再存⼊到本地,编码后的数据有带
startcode
- 与FFmpeg 示例⾳频编码的流程基本⼀致。
函数说明
- avcodec_find_encoder_by_name:根据指定的编码器名称查找注册的编码器。
- avcodec_alloc_context3:为AVCodecContext分配内存。
- avcodec_open2:打开编解码器。
- avcodec_send_frame:将AVFrame⾮压缩数据给编码器。。
- avcodec_receive_packet:获取到编码后的AVPacket数据。
- av_frame_get_buffer: 为⾳频或视频数据分配新的buffer。在调⽤这个函数之前,必须在AVFame上设置好以下属性:format(视频为像素格式,⾳频为样本格式)、nb_samples(样本个数,针对⾳频)、channel_layout(通道类型,针对⾳频)、width/height(宽⾼,针对视频)。
- av_frame_make_writable:确保AVFrame是可写的,尽可能避免数据的复制。如果AVFrame不是是可写的,将分配新的buffer和复制数据。
- av_image_fill_arrays: 存储⼀帧像素数据存储到AVFrame对应的data buffer。
编码出来的h264数据可以直接使⽤ffplay播放,也可以使⽤VLC播放。
av_image_get_buffer_size
c
int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align);
函数的作⽤是通过指定像素格式、图像宽、图像⾼来计算所需的内存⼤⼩,重点说明⼀个参数align:此参数是设定内存对⻬的对⻬数,也就是按多⼤的字节进⾏内存对⻬:
- ⽐如设置为1,表示按1字节对⻬,那么得到的结果就是与实际的内存⼤⼩⼀样。3
- 再⽐如设置为4,表示按4字节对⻬。也就是内存的起始地址必须是4的整倍数。
av_image_alloc
c
int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt,int align);
av_image_alloc()是这样定义的。此函数的功能是按照指定的宽、⾼、像素格式来分配图像内存。
pointers[4]
:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址。第四个指针保留不⽤linesizes[4]
:保存图像每个通道的内存对⻬的步⻓,即⼀⾏的对⻬内存的宽度,此值⼤⼩等于图像宽度。- w: 要申请内存的图像宽度。
- h: 要申请内存的图像⾼度。
- pix_fmt: 要申请内存的图像的像素格式。
- align: ⽤于内存对⻬的值。
- 返回值:所申请的内存空间的总⼤⼩。如果是负值,表示申请失败。
av_image_fill_arrays
c
int av_image_fill_arrays(uint8_t *dst_data[4], int dst_linesize[4], const uint8_t *src, enum AVPixelFormat pix_fmt, int width, int height, int align);
av_image_fill_arrays()函数⾃身不具备内存申请的功能,此函数类似于格式化已经申请的内存,即通过av_malloc()函数申请的内存空间,或者av_frame_get_buffer()函数申请的内存空间。
再者,av_image_fill_arrays()中参数具体说明:
dst_data[4]
:[out]
对申请的内存格式化为三个通道后,分别保存其地址dst_linesize[4]
:[out]
格式化的内存的步⻓(即内存对⻬后的宽度)*src: [in]av_alloc()
函数申请的内存地址。pix_fmt: [in]
申请 src内存时的像素格式width: [in]
申请src内存时指定的宽度height: [in]
申请scr内存时指定的⾼度align: [in]
申请src内存时指定的对⻬字节数。
H.264 码率设置
1 什么是视频码率
视频码率是视频数据(包含视频⾊彩量、亮度量、像素量)每秒输出的位数。⼀般⽤的单位是kbps。
2 设置视频码率的必要性
-
在⽹络视频应⽤中,视频质量和⽹络带宽占⽤是相⽭盾的。通常情况下,视频流占⽤的带宽越⾼则视频质量也越⾼,需要的⽹络带宽也越⼤,解决这⼀⽭盾的钥匙当然是视频编解码技术。评判⼀种视频编解码技术的优劣,是⽐较在相同的带宽条件下,哪个视频质量更好;在相同的视频质量条件下,哪个占⽤的⽹络带宽更少(⽂件体积⼩)。
-
是不是视频码率越⾼,质量越好呢?理论上是这样的。然⽽在我们⾁眼分辨的范围内,当码率⾼到⼀定程度时,就没有什么差别了。所以码率设置有它的最优值,H.264(也叫AVC或X264)的⽂件中,视频的建议码率如下:
视频大小 | 分辨率 | 推荐码率 |
---|---|---|
480P | 720x480 | 1800Kbps |
720P | 1280x720 | 3500Kbps |
1080P | 1920x1080 | 8500Kbps |
3 ⼿机设置码率建议
项目 | 计算公式 | 192X144 | 320X240 | 480X360 | 640X480 | 1280X720 | 1920X1080 |
---|---|---|---|---|---|---|---|
极低码率 | (宽X高X3)/4 | 30kbps | 60kbps | 120kbps | 250kbps | 500kbps | 1000kbps |
低码率 | (宽X高X3)/2 | 60kbps | 120kbps | 250kbps | 500kbps | 1000kbps | 2000kbps |
中码率 | (宽X高X3) | 120kbps | 250kbps | 500kbps | 1000kbps | 2000kbps | 4000kbps |
高码率 | (宽X高X3)X2 | 250kbps | 500kbps | 1000kbps | 2000kbps | 4000kbps | 8000kbps |
极高码率 | (宽X高X3)X4 | 500kbps | 1000kbps | 2000kbps | 4000kbps | 8000kbps | 16000kbps |
FFmpeg与H264编码指南
鉴于x264的参数众多,各种参数的配合复杂,为了使⽤者⽅便,x264建议如⽆特别需要可使⽤preset和tune设置。这套开发者推荐的参数较为合理,可在此基础上在调整⼀些具体参数以
符合⾃⼰需要,⼿动设定的参数会覆盖preset和tune⾥的参数。
使⽤ffmpeg -h encoder=libx264 命令查询相关⽀持的参数
shell
libx264 AVOptions:
-preset <string> E..V..... Set the encoding preset (cf. x264 --fullhelp) (default "medium")
-tune <string> E..V..... Tune the encoding params (cf. x264 --fullhelp)
-profile <string> E..V..... Set profile restrictions (cf. x264 --fullhelp)
-fastfirstpass <boolean> E..V..... Use fast settings when encoding first pass (default true)
-level <string> E..V..... Specify level (as defined by Annex A)
-passlogfile <string> E..V..... Filename for 2 pass stats
-wpredp <string> E..V..... Weighted prediction for P-frames
-a53cc <boolean> E..V..... Use A53 Closed Captions (if available) (default true)
-x264opts <string> E..V..... x264 options
-crf <float> E..V..... Select the quality for constant quality mode (from -1 to FLT_MAX) (default -1)
-crf_max <float> E..V..... In CRF mode, prevents VBV from lowering quality beyond this point. (from -1 to FLT_MAX) (default -1)
-qp <int> E..V..... Constant quantization parameter rate control method (from -1 to INT_MAX) (default -1)
-aq-mode <int> E..V..... AQ method (from -1 to INT_MAX) (default -1)
none E..V.....
variance E..V..... Variance AQ (complexity mask)
autovariance E..V..... Auto-variance AQ
autovariance-biased E..V..... Auto-variance AQ with bias to dark scenes
-aq-strength <float> E..V..... AQ strength. Reduces blocking and blurring in flat and textured areas. (from -1 to FLT_MAX) (default -1)
-psy <boolean> E..V..... Use psychovisual optimizations. (default auto)
-psy-rd <string> E..V..... Strength of psychovisual optimization, in <psy-rd>:<psy-trellis> format.
-rc-lookahead <int> E..V..... Number of frames to look ahead for frametype and ratecontrol (from -1 to INT_MAX) (default -1)
-weightb <boolean> E..V..... Weighted prediction for B-frames. (default auto)
-weightp <int> E..V..... Weighted prediction analysis method. (from -1 to INT_MAX) (default -1)
none E..V.....
simple E..V.....
smart E..V.....
-ssim <boolean> E..V..... Calculate and print SSIM stats. (default auto)
-intra-refresh <boolean> E..V..... Use Periodic Intra Refresh instead of IDR frames. (default auto)
-bluray-compat <boolean> E..V..... Bluray compatibility workarounds. (default auto)
-b-bias <int> E..V..... Influences how often B-frames are used (from INT_MIN to INT_MAX) (default INT_MIN)
-b-pyramid <int> E..V..... Keep some B-frames as references. (from -1 to INT_MAX) (default -1)
none E..V.....
strict E..V..... Strictly hierarchical pyramid
normal E..V..... Non-strict (not Blu-ray compatible)
-mixed-refs <boolean> E..V..... One reference per partition, as opposed to one reference per macroblock (default auto)
-8x8dct <boolean> E..V..... High profile 8x8 transform. (default auto)
-fast-pskip <boolean> E..V..... (default auto)
-aud <boolean> E..V..... Use access unit delimiters. (default auto)
-mbtree <boolean> E..V..... Use macroblock tree ratecontrol. (default auto)
-deblock <string> E..V..... Loop filter parameters, in <alpha:beta> form.
-cplxblur <float> E..V..... Reduce fluctuations in QP (before curve compression) (from -1 to FLT_MAX) (default -1)
-partitions <string> E..V..... A comma-separated list of partitions to consider. Possible values: p8x8, p4x4, b8x8, i8x8, i4x4, none, all
-direct-pred <int> E..V..... Direct MV prediction mode (from -1 to INT_MAX) (default -1)
none E..V.....
spatial E..V.....
temporal E..V.....
auto E..V.....
-slice-max-size <int> E..V..... Limit the size of each slice in bytes (from -1 to INT_MAX) (default -1)
-stats <string> E..V..... Filename for 2 pass stats
-nal-hrd <int> E..V..... Signal HRD information (requires vbv-bufsize; cbr not allowed in .mp4) (from -1 to INT_MAX) (default -1)
none E..V.....
vbr E..V.....
cbr E..V.....
-avcintra-class <int> E..V..... AVC-Intra class 50/100/200 (from -1 to 200) (default -1)
-me_method <int> E..V..... Set motion estimation method (from -1 to 4) (default -1)
dia E..V.....
hex E..V.....
umh E..V.....
esa E..V.....
tesa E..V.....
-motion-est <int> E..V..... Set motion estimation method (from -1 to 4) (default -1)
dia E..V.....
hex E..V.....
umh E..V.....
esa E..V.....
tesa E..V.....
-forced-idr <boolean> E..V..... If forcing keyframes, force them as IDR frames. (default false)
-coder <int> E..V..... Coder type (from -1 to 1) (default default)
default E..V.....
cavlc E..V.....
cabac E..V.....
vlc E..V.....
ac E..V.....
-b_strategy <int> E..V..... Strategy to choose between I/P/B-frames (from -1 to 2) (default -1)
-chromaoffset <int> E..V..... QP difference between chroma and luma (from INT_MIN to INT_MAX) (default -1)
-sc_threshold <int> E..V..... Scene change threshold (from INT_MIN to INT_MAX) (default -1)
-noise_reduction <int> E..V..... Noise reduction (from INT_MIN to INT_MAX) (default -1)
-x264-params <string> E..V..... Override the x264 configuration using a :-separated list of key=value parameters
x264是⼀个 H.264/MPEG4 AVC 编码器,本指南将指导新⼿如何创建⾼质量的H.264视频。
对于普通⽤户通常有两种码率控制模式:CRF(Constant Rate Factor)和Two pass ABR。码率控制是⼀种决定为每⼀个视频帧分配多少⽐特数的⽅法,它将决定⽂件的⼤⼩和质量的分配。
CRF(Constant Rate Factor):
1 选择⼀个CRF值
-
量化⽐例的范围为051,其中0为⽆损模式,**23为缺省值**,51可能是最差的。该数字越⼩,图像质量越好。从主观上讲,1828是⼀个合理的范围。18往往被认为从视觉上看是⽆损的,它的输出视频质量和输⼊视频⼀模⼀样或者说相差⽆⼏。但从技术的⻆度来讲,它依然是有损压缩。
-
若CRF值加6,输出码率⼤概减少⼀半;若CRF值减6,输出码率翻倍。通常是在保证可接受视频质量的前提下选择⼀个最⼤的CRF值,如果输出视频质量很好,那就尝试⼀个更⼤的值,如果看起来很糟,那就尝试⼀个⼩⼀点值。
注释:本⽂所提到的量化⽐例只适⽤于8-bit x264(10-bit x264的量化⽐例 为0~63),你可以使⽤x264 --help命令在Output bit depth选项查看输出位深,在各种版本中,8bit是最常⻅的。
2 选择⼀个preset和tune
preset
-
预设是⼀系列参数的集合,这个集合能够在编码速度和压缩率之间做出⼀个权衡。⼀个编码速度稍慢的预设会提供更⾼的压缩效率(压缩效率是以⽂件⼤⼩来衡量的)。这就是说,假如你想得到⼀个指定⼤⼩的⽂件或者采⽤恒定⽐特率编码模式,你可以采⽤⼀个较慢的预设来获得更好的质量。同样的,对于恒定质量编码模式,你可以通过选择⼀个较慢的预设轻松地节省⽐特率。
-
如果你很有耐⼼,通常的建议是使⽤最慢的预设。⽬前所有的预设按照编码速度降序排列为:
text
ultrafast
superfast
veryfast
faster
fast
medium -- default preset
slow
slower
veryslow
placebo - ignore this as it is not useful (see FAQ)
默认为medium级别。
你可以使⽤--preset来查看预设列表,也可以通过x264 --fullhelp来查看预设所采⽤的参数配置。
针对 libx264做过简单的各选项对⽐测试,结果如下图:

从图中可以看出,当其他参数固定时,选择不同的preset,对应的码率和编码时间都不⼀样
tune
tune是x264中重要性仅次于preset的选项,它是视觉优化的参数,tune可以理解为视频偏好(或者视频类型),tune不是⼀个单⼀的参数,⽽是由⼀组参数构成-tune来改变参数设置。当前的 tune包括:
-
film:电影类型,对视频的质量⾮常严格时使⽤该选项
-
animation:动画⽚,压缩的视频是动画⽚时使⽤该选项
-
grain:颗粒物很重,该选项适⽤于颗粒感很重的视频
-
stillimage:静态图像,该选项主要⽤于静⽌画⾯⽐较多的视频
-
psnr:提⾼psnr,该选项编码出来的视频psnr⽐较⾼
-
ssim:提⾼ssim,该选项编码出来的视频ssim⽐较⾼
-
fastdecode:快速解码,该选项有利于快速解码
-
zerolatency:零延迟,该选项主要⽤于视频直播
-
如果你不确定使⽤哪个选项或者说你的输⼊与所有的tune皆不匹配,你可以忽略--tune 选项。
-
你可以使⽤-tune来查看tune列表,也可以通过x264 --fullhelp来查看tune所采⽤的参数配置。
profile
另外⼀个可选的参数是-profile:v,它可以将你的输出限制到⼀个特定的 H.264 profile。⼀些⾮常⽼的或者要被淘汰的设备仅⽀持有限的选项,⽐如只⽀持baseline或者main。
所有的profile 包括:
- baseline profile:基本画质。⽀持I/P 帧,只⽀持⽆交错(Progressive)和CAVLC;
- extended profile:进阶画质。⽀持I/P/B/SP/SI 帧,只⽀持⽆交错(Progressive)和CAVLC;
- main profile:主流画质。提供I/P/B 帧,⽀持⽆交错(Progressive)和交错(Interlaced),也⽀持CAVLC 和CABAC 的⽀持;
- high profile:⾼级画质。在main Profile 的基础上增加了8x8内部预测、⾃定义量化、 ⽆损视频编码和更多的YUV 格式;
在相同配置情况下,High profile(HP)可以⽐Main profile(MP)节省10%的码流量,⽐MPEG-2MP节省60%的码流量,具有更好的编码性能。根据应⽤领域的不同:
- baseline profile多应⽤于实时通信领域;
- main profile多应⽤于流媒体领域;
- high profile则多应⽤于⼴电和存储领域。
低延迟
x264提供了⼀个 -tune zerolatency 选项。
兼容性
如果你想让你的视频最⼤化的和⽬标播放设备兼容(⽐如⽼版本的的ios或者所有的android 设备),那么你可以这做:
-profile:v baseline
- 这将会关闭很多⾼级特性,但是它会提供很好的兼容性。也许你可能不需要这些设置,因为⼀旦你⽤了这些设置,在同样的视频质量下与更⾼的编码档次相⽐会使⽐特率稍有增加。
- 关于profile列表和关于它们的描述,你可以运⾏x264 --fullhelp
- 要牢记apple quick time 对于x264编码的视频只⽀持 YUV 420颜⾊空间,⽽且不⽀持任何⾼于 main profile编码档次。这样对于quick time 只留下了两个兼容选项baseline和 main。其他的编码档次qucik time均不⽀持,虽然它们均可以在其它的播放设备上回放。
X264参数之zerolatency的分析
我们都知道,加⼊zerolatency的⽬的是为了降低在线转码的编码延迟,那么,该参数是如何影响到x264的转码性能了呢?
⾸先,先来看看代码中编码延迟的影响因素:
c
h->frames.i_delay = max(h->param.i_bframe, h->param.rc.i_lookahead)
h->i_thread_frames - 1
h->param.i_sync_lookahead
h->param.b_vfr_input
设置zerolatency后,相应的参数配置如下:
c
if(!strncasecmp(s, "zerolatency", 11 ))
{
param->rc.i_lookahead = 0;
param->i_sync_lookahead = 0;
param->i_bframe = 0;
param->b_sliced_threads = 1;
param->b_vfr_input = 0;
param->rc.b_mb_tree = 0;
}
下⾯我们来看⼀下zerolatency设置中各个参数的意义:
-
rc_lookahead: Set number of frames to look ahead for frametype and ratecontrol
-
该参数为mb-tree码率控制和vbv-lookahead设置可⽤的帧数量,最⼤值为250。对于mbi-tree来说,rc_lookahead值越⼤,会得到更准确的结果,但编码速度也会更慢,因为编码器需要缓存慢rc_lookahead帧数据后,才会开始第⼀帧编码,增加编码延时,因此在实时视频通信中将其设置为0。
-
sync_lookahead: 设置⽤于线程预测的帧缓存⼤⼩,最⼤值为250。在第⼆遍及更多遍编码或基于分⽚线程时⾃动关闭。sync_lookahead = 0为关闭线程预测,可减⼩延迟,但也会降低性能。
-
bframes: I帧和P帧或者两个P帧之间可⽤的最⼤连续B帧数量,默认值为3。B帧可使⽤双向预测,从⽽显著提⾼压缩率,但由于需要缓存更多的帧数以及重排序的原因,会降低编码速度,增加编码延迟,因此在实时编码时也建议将该值设置为0。
-
sliced_threads: 基于分⽚的线程,默认值为off,开启该⽅法在压缩率和编码效率上都略低于默认⽅法,但没有编码延时。除⾮在编码实时流或者对低延迟要求较⾼的场合开启该⽅法,⼀般情况下建议设为off。
-
vfr_input: 与force-cfr选项相对应:
c
OPT("force-cfr")
p->b_vfr_input = !atobool(value);
- vfr_input= 1时,为可变帧率,使⽤timebase和timestamps做码率控制;vfr_input = 0时,为固定帧率,使⽤fps做码率控制。
mb_tree: 基于宏块树的码率控制。对于每个MB,向前预测⼀定数量的帧(该数量由rc_lookahead和keyint中的较⼩值决定),计算该MB被引⽤的次数,根据被引⽤次数的多少决定为该MB分配的量化QP值。该⽅法会⽣成⼀个临时stats⽂件,记录了每个P帧中每个MB被参考的情况。使⽤mb_tree的⽅法能够节约⼤概30%的码率,但同时也会增加编码延迟,因此实时流编码时也将其关闭。
实现流程
准备文件
在build
路径下准备yuv420p
的文件,用于编码为h264
格式的文件

在main
函数参数中传入对应的输入文件、输出文件,以及对应的编码器

初始化解码器
- 根据解码器名字查找对应的解码器
- 初始化解码器上下文
c
codec_name = argv[3];
codec = avcodec_find_encoder_by_name(codec_name);
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
fprintf(stderr, "Could not allocate video codec context\n");
exit(1);
}
- 设置编码器基本参数,比如分辨率,帧率,时间基、视频格式
c
codec_ctx->width = 1280;
codec_ctx->height = 720;
/* 设置time base ,注意timebase的正确设置,会影响码率的输出, 即是AVFrame的pts的timebase需要和codec_ctx->time_base一致*/
codec_ctx->time_base = (AVRational){1, 25}; // 和AVFrame的pts相同,这样不需要做时间戳的转换
codec_ctx->framerate = (AVRational){25, 1};
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
- 设置编码器
GOP
参数,比如GOP
长度(I
帧间隔),B
帧间隔
c
codec_ctx->gop_size = 25; // I帧间隔
codec_ctx->max_b_frames = 2; // 如果不想包含B帧则设置为0
- 设置解码器高级参数,比如设置
pretest
、profile
、tune
等参数 - 设置的这些参数影响输出的视频质量与速度
c
ret = av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);
ret = av_opt_set(codec_ctx->priv_data, "profile", "main", 0); // 默认是high
ret = av_opt_set(codec_ctx->priv_data, "tune","film",0); // 画质film
- 设置输出比特率,可以有波动(最小值+最大值),也可以设置成固定的
- 可以设置缓存大小,波动的时候可以存在缓存中
c
codec_ctx->bit_rate = 3000000; //固定
codec_ctx->rc_max_rate = 3000000; //最大值
codec_ctx->rc_min_rate = 1000000;// 最小值
codec_ctx->rc_buffer_size = 2000000;//缓存大小
- 如果有需要,还可以设置多线程编码,不过多线程会带来延迟
c
codec_ctx->thread_count = 4; // 开了多线程后也会导致帧输出延迟, 需要缓存thread_count帧后再编程。
codec_ctx->thread_type = FF_THREAD_FRAME; // 并 设置为FF_THREAD_FRAME
libx264
编码器默认自动写入NALU
头部,如(SPS、PPS、StartCode)- 如果需要设置将这些信息将写入在
extra_data
中,则需要设置编码器标志或上AV_CODEC_FLAG_GLOBAL_HEADER
宏 - 设置后不会每一个
I
帧前都写入SPS/PPS
信息
c
codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 存本地文件时不要去设置
- 将编码器和编码器上下文关联起来
c
ret = avcodec_open2(codec_ctx, codec, NULL);
if (ret < 0) {
fprintf(stderr, "Could not open codec: %s\n", av_err2str(ret));
exit(1);
}
打开文件
- 打开输入输出文件,进行循环写入数据
c
infile = fopen(in_yuv_file, "rb");
if (!infile) {
fprintf(stderr, "Could not open %s\n", in_yuv_file);
exit(1);
}
outfile = fopen(out_h264_file, "wb");
if (!outfile) {
fprintf(stderr, "Could not open %s\n", out_h264_file);
exit(1);
}
分配frame和``packet`
frame
用于存储没有编码的yuv
裸数据packet
用于存储编码过的h264
数据
c
// 分配pkt和frame
pkt = av_packet_alloc();
if (!pkt) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
}
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
}
设置frame
属性
frame
中的参数需要从解码器上下文中拷贝过来- 根据
frame
中的参数分配对应的data
内存
c
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate the video frame data\n");
exit(1);
}
- 根据
frame
的参数,计算出缓存区需要设置的内存大小 - 分配对应大小的字节内存
c
int frame_bytes = av_image_get_buffer_size(frame->format, frame->width,frame->height, 1);
uint8_t *yuv_buf = (uint8_t *)malloc(frame_bytes);
循环编码
- 编码逻辑和音频类似,只是这里是将
yuv
数据根据相应参数拷贝到frame
中
c
int need_size = av_image_fill_arrays(frame->data, frame->linesize, yuv_buf,frame->format,frame->width, frame->height,1);
- 循环解码代码如下
c
for (;;) {
memset(yuv_buf, 0, frame_bytes);
size_t read_bytes = fread(yuv_buf, 1, frame_bytes, infile);
if(read_bytes <= 0) {
printf("read file finish\n");
break;
}
/* 确保该frame可写, 如果编码器内部保持了内存参考计数,则需要重新拷贝一个备份
目的是新写入的数据和编码器保存的数据不能产生冲突
*/
int frame_is_writable = 1;
if(av_frame_is_writable(frame) == 0) { // 这里只是用来测试
printf("the frame can't write, buf:%p\n", frame->buf[0]);
if(frame->buf && frame->buf[0]) // 打印referenc-counted,必须保证传入的是有效指针
printf("ref_count1(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
frame_is_writable = 0;
}
ret = av_frame_make_writable(frame);
if(frame_is_writable == 0) { // 这里只是用来测试
printf("av_frame_make_writable, buf:%p\n", frame->buf[0]);
if(frame->buf && frame->buf[0]) // 打印referenc-counted,必须保证传入的是有效指针
printf("ref_count2(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
}
if(ret != 0) {
printf("av_frame_make_writable failed, ret = %d\n", ret);
break;
}
int need_size = av_image_fill_arrays(frame->data, frame->linesize, yuv_buf,
frame->format,
frame->width, frame->height, 1);
if(need_size != frame_bytes) {
printf("av_image_fill_arrays failed, need_size:%d, frame_bytes:%d\n",
need_size, frame_bytes);
break;
}
pts += 40;
// 设置pts
frame->pts = pts; // 使用采样率作为pts的单位,具体换算成秒 pts*1/采样率
begin_time = get_time();
ret = encode(codec_ctx, frame, pkt, outfile);
end_time = get_time();
printf("encode time:%lldms\n", end_time - begin_time);
if(ret < 0) {
printf("encode failed\n");
break;
}
}
encode
函数用于将frame
编码为packet
,并写入到输出的h264
文件中- 同样注意
send
一次frame
,可能生成多个package
,因此需要循环接收
c
static int encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,
FILE *outfile)
{
int ret;
/* send the frame to the encoder */
if (frame)
printf("Send frame %3"PRId64"\n", frame->pts);
/* 通过查阅代码,使用x264进行编码时,具体缓存帧是在x264源码进行,
* 不会增加avframe对应buffer的reference*/
ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0)
{
fprintf(stderr, "Error sending a frame for encoding\n");
return -1;
}
while (ret >= 0)
{
ret = avcodec_receive_packet(enc_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) {
fprintf(stderr, "Error encoding audio frame\n");
return -1;
}
if(pkt->flags & AV_PKT_FLAG_KEY)
printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",
pkt->flags, pkt->pts, pkt->dts, pkt->size);
if(!pkt->flags)
printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",
pkt->flags, pkt->pts, pkt->dts, pkt->size);
fwrite(pkt->data, 1, pkt->size, outfile);
}
return 0;
}
冲刷编码器
- 编码结束后还需要冲刷一下编码器,写入剩余数据
c
encode(codec_ctx, NULL, pkt, outfile);
结束工作
- 结束后要关闭文件并释放相关内存
c
fclose(infile);
fclose(outfile);
// 释放内存
if(yuv_buf) {
free(yuv_buf);
}
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
整体代码
main.c
c
/**
* @projectName 08-02-encode_video
* @brief 视频编码,从本地读取YUV数据进行H264编码
* @author Liao Qingfu
* @date 2020-04-16
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavutil/time.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
int64_t get_time()
{
return av_gettime_relative() / 1000; // 换算成毫秒
}
static int encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,
FILE *outfile)
{
int ret;
/* send the frame to the encoder */
if (frame)
printf("Send frame %3"PRId64"\n", frame->pts);
/* 通过查阅代码,使用x264进行编码时,具体缓存帧是在x264源码进行,
* 不会增加avframe对应buffer的reference*/
ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0)
{
fprintf(stderr, "Error sending a frame for encoding\n");
return -1;
}
while (ret >= 0)
{
ret = avcodec_receive_packet(enc_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) {
fprintf(stderr, "Error encoding audio frame\n");
return -1;
}
if(pkt->flags & AV_PKT_FLAG_KEY)
printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",
pkt->flags, pkt->pts, pkt->dts, pkt->size);
if(!pkt->flags)
printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",
pkt->flags, pkt->pts, pkt->dts, pkt->size);
fwrite(pkt->data, 1, pkt->size, outfile);
}
return 0;
}
/**
* @brief 提取测试文件:ffmpeg -i test_1280x720.flv -t 5 -r 25 -pix_fmt yuv420p yuv420p_1280x720.yuv
* 参数输入: yuv420p_1280x720.yuv yuv420p_1280x720.h264 libx264
* @param argc
* @param argv
* @return
*/
int main(int argc, char **argv)
{
char *in_yuv_file = NULL;
char *out_h264_file = NULL;
FILE *infile = NULL;
FILE *outfile = NULL;
const char *codec_name = NULL;
const AVCodec *codec = NULL;
AVCodecContext *codec_ctx= NULL;
AVFrame *frame = NULL;
AVPacket *pkt = NULL;
int ret = 0;
if (argc < 4) {
fprintf(stderr, "Usage: %s <input_file out_file codec_name >, argc:%d\n",
argv[0], argc);
return 0;
}
in_yuv_file = argv[1]; // 输入YUV文件
out_h264_file = argv[2];
codec_name = argv[3];
/* 查找指定的编码器 */
codec = avcodec_find_encoder_by_name(codec_name);
if (!codec) {
fprintf(stderr, "Codec '%s' not found\n", codec_name);
exit(1);
}
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
fprintf(stderr, "Could not allocate video codec context\n");
exit(1);
}
/* 设置分辨率*/
codec_ctx->width = 1280;
codec_ctx->height = 720;
/* 设置time base ,注意timebase的正确设置,会影响码率的输出, 即是AVFrame的pts的timebase需要和codec_ctx->time_base一致*/
codec_ctx->time_base = (AVRational){1, 25}; // 和AVFrame的pts相同,这样不需要做时间戳的转换
codec_ctx->framerate = (AVRational){25, 1};
/* 设置I帧间隔
* 如果frame->pict_type设置为AV_PICTURE_TYPE_I, 则忽略gop_size的设置,一直当做I帧进行编码
*/
codec_ctx->gop_size = 25; // I帧间隔
codec_ctx->max_b_frames = 2; // 如果不想包含B帧则设置为0
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
//
if (codec->id == AV_CODEC_ID_H264) {
// 相关的参数可以参考libx264.c的 AVOption options
// ultrafast all encode time:2270ms
// medium all encode time:5815ms
// veryslow all encode time:19836ms
ret = av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);
if(ret != 0) {
printf("av_opt_set preset failed\n");
}
ret = av_opt_set(codec_ctx->priv_data, "profile", "main", 0); // 默认是high
if(ret != 0) {
printf("av_opt_set profile failed\n");
}
ret = av_opt_set(codec_ctx->priv_data, "tune","zerolatency",0); // 直播是才使用该设置
// ret = av_opt_set(codec_ctx->priv_data, "tune","film",0); // 画质film
if(ret != 0) {
printf("av_opt_set tune failed\n");
}
}
/*
* 设置编码器参数
*/
/* 设置bitrate */
codec_ctx->bit_rate = 3000000;
// codec_ctx->rc_max_rate = 3000000;
// codec_ctx->rc_min_rate = 1000000;
// codec_ctx->rc_buffer_size = 20000AA00;
// codec_ctx->thread_count = 4; // 开了多线程后也会导致帧输出延迟, 需要缓存thread_count帧后再编程。
codec_ctx->thread_type = FF_THREAD_FRAME; // 并 设置为FF_THREAD_FRAME
/* 对于H264 AV_CODEC_FLAG_GLOBAL_HEADER 设置则只包含I帧,此时sps pps需要从codec_ctx->extradata读取
* 不设置则每个I帧都带 sps pps sei
*/
// codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 存本地文件时不要去设置
/* 将codec_ctx和codec进行绑定 */
ret = avcodec_open2(codec_ctx, codec, NULL);
if (ret < 0) {
fprintf(stderr, "Could not open codec: %s\n", av_err2str(ret));
exit(1);
}
printf("thread_count: %d, thread_type:%d\n", codec_ctx->thread_count, codec_ctx->thread_type);
// 打开输入和输出文件
infile = fopen(in_yuv_file, "rb");
if (!infile) {
fprintf(stderr, "Could not open %s\n", in_yuv_file);
exit(1);
}
outfile = fopen(out_h264_file, "wb");
if (!outfile) {
fprintf(stderr, "Could not open %s\n", out_h264_file);
exit(1);
}
// 分配pkt和frame
pkt = av_packet_alloc();
if (!pkt) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
}
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
}
// 为frame分配buffer
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate the video frame data\n");
exit(1);
}
// 计算出每一帧的数据 像素格式 * 宽 * 高
// 1382400
int frame_bytes = av_image_get_buffer_size(frame->format, frame->width,
frame->height, 1);
printf("frame_bytes %d\n", frame_bytes);
uint8_t *yuv_buf = (uint8_t *)malloc(frame_bytes);
if(!yuv_buf) {
printf("yuv_buf malloc failed\n");
return 1;
}
int64_t begin_time = get_time();
int64_t end_time = begin_time;
int64_t all_begin_time = get_time();
int64_t all_end_time = all_begin_time;
int64_t pts = 0;
printf("start enode\n");
for (;;) {
memset(yuv_buf, 0, frame_bytes);
size_t read_bytes = fread(yuv_buf, 1, frame_bytes, infile);
if(read_bytes <= 0) {
printf("read file finish\n");
break;
}
/* 确保该frame可写, 如果编码器内部保持了内存参考计数,则需要重新拷贝一个备份
目的是新写入的数据和编码器保存的数据不能产生冲突
*/
int frame_is_writable = 1;
if(av_frame_is_writable(frame) == 0) { // 这里只是用来测试
printf("the frame can't write, buf:%p\n", frame->buf[0]);
if(frame->buf && frame->buf[0]) // 打印referenc-counted,必须保证传入的是有效指针
printf("ref_count1(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
frame_is_writable = 0;
}
ret = av_frame_make_writable(frame);
if(frame_is_writable == 0) { // 这里只是用来测试
printf("av_frame_make_writable, buf:%p\n", frame->buf[0]);
if(frame->buf && frame->buf[0]) // 打印referenc-counted,必须保证传入的是有效指针
printf("ref_count2(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
}
if(ret != 0) {
printf("av_frame_make_writable failed, ret = %d\n", ret);
break;
}
int need_size = av_image_fill_arrays(frame->data, frame->linesize, yuv_buf,
frame->format,
frame->width, frame->height, 1);
if(need_size != frame_bytes) {
printf("av_image_fill_arrays failed, need_size:%d, frame_bytes:%d\n",
need_size, frame_bytes);
break;
}
pts += 40;
// 设置pts
frame->pts = pts; // 使用采样率作为pts的单位,具体换算成秒 pts*1/采样率
begin_time = get_time();
ret = encode(codec_ctx, frame, pkt, outfile);
end_time = get_time();
printf("encode time:%lldms\n", end_time - begin_time);
if(ret < 0) {
printf("encode failed\n");
break;
}
}
/* 冲刷编码器 */
encode(codec_ctx, NULL, pkt, outfile);
all_end_time = get_time();
printf("all encode time:%lldms\n", all_end_time - all_begin_time);
// 关闭文件
fclose(infile);
fclose(outfile);
// 释放内存
if(yuv_buf) {
free(yuv_buf);
}
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
printf("main finish, please enter Enter and exit\n");
getchar();
return 0;
}