【音视频】视频解码实战

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 包括:

  1. baseline profile:基本画质。⽀持I/P 帧,只⽀持⽆交错(Progressive)和CAVLC;
  2. extended profile:进阶画质。⽀持I/P/B/SP/SI 帧,只⽀持⽆交错(Progressive)和CAVLC;
  3. main profile:主流画质。提供I/P/B 帧,⽀持⽆交错(Progressive)和交错(Interlaced),也⽀持CAVLC 和CABAC 的⽀持;
  4. 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
  • 设置解码器高级参数,比如设置pretestprofiletune等参数
  • 设置的这些参数影响输出的视频质量与速度
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;
}

更多资料:https://github.com/0voice

相关推荐
Donvink1 小时前
【视频生成模型】通义万相Wan2.1模型本地部署和LoRA微调
人工智能·深度学习·aigc·音视频
Deepsleep.2 小时前
视频HLS分片与关键帧优化深度解析
音视频
数据与后端架构提升之路3 小时前
深度解析如何将图像帧和音频片段特征高效存储到向量数据库 Milvus
数据库·opencv·音视频
EQ-雪梨蛋花汤6 小时前
【Part 2安卓原生360°VR播放器开发实战】第二节|基于等距圆柱投影方式实现全景视频渲染
android·音视频·vr
Antonio9159 小时前
【音视频】SDL简介
音视频·sdl
薛瑄9 小时前
FFmpeg之三 录制音频并保存, API编解码从理论到实战
ffmpeg·音视频·xcode
算家云11 小时前
AI音频核爆!Kimi开源“六边形战士”Kimi-Audio,ChatGPT语音版?
人工智能·音视频·kimi·算家云·kimi-audio·租算力,到算家云
Everbrilliant8913 小时前
音视频之H.265/HEVC熵编码
音视频·h.265·算术编码·哈夫曼编码·熵编码·指数哥伦布编码·熵编码的基本原理
Panesle13 小时前
月之暗面开源-音频理解、生成和对话生成模型:Kimi-Audio-7B-Instruct
人工智能·音视频·语音生成