ffmpeg入门教程之YUV编码成H264详解(翻译)

文章目录
ffmpeg入门教程https://www.jianshu.com/p/042c7847bd8a
视频播放器原理
本文示例基于官方版本ffmpeg-20190926-87ddf9f-win64-dev编写
将YUV视频序列文件编码成H264文件
RGB
YUV
YUV存储格式
如何获取YUV图像序列文件
mp4--->h264(ffmpeg命令行)
h264--->yuv(ffmpeg命令行)
已然迫不及待开始敲代码
fopen()
初始化AVFormatContext
创建AVStream
获取AVCodec
配置AVCodecContext
YUV420P
AVRational time_base
打开编码器
avcodec_open2()
初始化AVFrame
计算存储一张图片所需要的内存大小
av_image_get_buffer_size()
分配存储图像数据的内存块
为AVFrame指定data大小和linesize大小
av_image_fill_arrays()
AVFrame uint8_t *data[AV_NUM_DATA_POINTERS]
AVFrame int linesize[AV_NUM_DATA_POINTERS]
将流头写入输出文件
初始化AVPacket
YUV内存占用
读取YUV图像数据
fread()
time_base转换
YUV编码成H264过程
avcodec_send_frame()
avcodec_receive_packet()
fwrite()
写文件尾
end
完整代码如下
GitHub:https://github.com/AnJiaoDe/FFmpegDemo
欢迎分享、转载、联系、指正、批评、撕逼

ffmpeg入门教程https://www.jianshu.com/p/042c7847bd8a

视频播放器原理

———————————————— 版权声明

此处摘抄部分为CSDN博主「雷霄骅」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/leixiaohua1020/article/details/18893769

视音频技术主要包含以下几点:封装技术,视频压缩编码技术以及音频压缩编码技术。如果考虑到网络传输的话,还包括流媒体协议技术。

视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地文件则不需要解协议,为以下几个步骤:解封装,解码视音频,视音频同步。他们的过程如图所示。

ffmpeg入门教程之YUV编码成H264详解(翻译)_第1张图片
在这里插入图片描述

解协议的作用

就是将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如HTTP,RTMP,或是MMS等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。

解封装的作用

就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。

解码的作用

就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3,AC-3等等,视频的压缩编码标准则包含H.264,MPEG2,VC-1等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P,RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。

视音频同步的作用

就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。

本文示例基于官方版本ffmpeg-20190926-87ddf9f-win64-dev编写

获取途径参考 ffmpeg入门教程https://www.jianshu.com/p/042c7847bd8a

ffmpeg入门教程之YUV编码成H264详解(翻译)_第2张图片
在这里插入图片描述

官方示例是自定义了YUV等颜色数据,然后生成编码文件,下面我将实现

将YUV视频序列文件编码成H264文件

编写程序之前,我们必须先了解一些概念

RGB

RGB色彩模式是工业界的一种颜色标准,是通过对红色(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。

目前的显示器大都是采用了RGB颜色标准,在显示器上,是通过电子枪打在屏幕的红、绿、蓝三色发光极上来产生色彩的,目前的电脑一般都能显示32位颜色,有一千万种以上的颜色。

来几张图片想象一下:

ffmpeg入门教程之YUV编码成H264详解(翻译)_第3张图片
在这里插入图片描述

ffmpeg入门教程之YUV编码成H264详解(翻译)_第4张图片
在这里插入图片描述

ffmpeg入门教程之YUV编码成H264详解(翻译)_第5张图片
在这里插入图片描述

ffmpeg入门教程之YUV编码成H264详解(翻译)_第6张图片
20191031153142863.jpg

此处转载自百度百科RGB:https://baike.baidu.com/item/RGB/342517?fr=aladdin
写得特别详细

YUV

YUV,是一种颜色编码方法。常使用在各个视频处理组件中。 YUV在对照片或视频编码时,考虑到人类的感知能力,允许降低色度的带宽。

YUV是编译true-color颜色空间(color space)的种类,Y'UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance或Luma),也就是灰阶值,“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。

Y′UV,YUV,YCbCr,YPbPr所指涉的范围,常有混淆或重叠的情况。从历史的演变来说,其中YUV和Y'UV通常用来编码电视的模拟信号,而YCbCr则是用来描述数字的视频信号,适合视频与图片压缩以及传输,例如MPEG、JPEG。但在现今,YUV通常已经在电脑系统上广泛使用。

Y'代表明亮度(luma;brightness)而U与V存储色度(色讯;chrominance;color)部分;亮度(luminance)记作Y,而Y'的prime符号记作伽玛校正。

YUVFormats分成两个格式:
紧缩格式(packedformats):将Y、U、V值存储成MacroPixels数组,和RGB的存放方式类似。
平面格式(planarformats):将Y、U、V的三个分量分别存放在不同的矩阵中。

紧缩格式(packedformat)中的YUV是混合在一起的,对于YUV4:4:4格式而言,用紧缩格式很合适的,因此就有了UYVY、YUYV等。平面格式(planarformats)是指每Y分量,U分量和V分量都是以独立的平面组织的,也就是说所有的U分量必须在Y分量后面,而V分量在所有的U分量后面,此一格式适用于采样(subsample)。平面格式(planarformat)有I420(4:2:0)、YV12、IYUV等。

Y'UV的发明是由于彩色电视与黑白电视的过渡时期。黑白视频只有Y(Luma,Luminance)视频,也就是灰阶值。到了彩色电视规格的制定,是以YUV/YIQ的格式来处理彩色电视图像,把UV视作表示彩度的C(Chrominance或Chroma),如果忽略C信号,那么剩下的Y(Luma)信号就跟之前的黑白电视频号相同,这样一来便解决彩色电视机与黑白电视机的兼容问题。Y'UV最大的优点在于只需占用极少的带宽。

因为UV分别代表不同颜色信号,所以直接使用R与B信号表示色度的UV。也就是说UV信号告诉了电视要偏移某象素的的颜色,而不改变其亮度。或者UV信号告诉了显示器使得某个颜色亮度依某个基准偏移。UV的值越高,代表该像素会有更饱和的颜色。
彩色图像记录的格式,常见的有RGB、YUV、CMYK等。彩色电视最早的构想是使用RGB三原色来同时传输。这种设计方式是原来黑白带宽的3倍,在当时并不是很好的设计。RGB诉求于人眼对色彩的感应,YUV则着重于视觉对于亮度的敏感程度,Y代表的是亮度,UV代表的是彩度(因此黑白电影可省略UV,相近于RGB),分别用Cr和Cb来表示,因此YUV的记录通常以Y:UV的格式呈现。

此处转载自百度百科YUV:https://baike.baidu.com/item/YUV/3430784?fr=aladdin

YUV存储格式

YUV 4:4:4采样

每一个Y对应一组UV分量8+8+8 = 24bits,3个字节。
比如四个像素为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]
存放的码流为: Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
内存大小:表示色度值(UV)没有减少采样。即Y,U,V各占一个字节,加上Alpha通道一个字节,总共占4字节.这个格式其实就是24bpp的RGB格式了

YUV 4:2:2采样

每两个Y共用一组UV分量,一个YUV占8+4+4 = 16bits 2个字节。
比如四个像素为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]
存放的码流为: Y0 U0 Y1 V1 Y2 U2 Y3 V3
映射出像素点为:[Y0 U0 V1] [Y1 U0 V1] [Y2 U2 V3] [Y3 U2 V3]
内存大小:w * h * 2

YUV 4:2:0采样

每四个Y共用一组UV分量一个YUV占8+2+2 = 12bits 1.5个字节。
比如八个像素为:[Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]
[Y5 U5 V5] [Y6 U6 V6] [Y7U7 V7] [Y8 U8 V8]
存放的码流为:Y0 U0 Y1 Y2 U2 Y3
Y5 V5 Y6 Y7 V7 Y8
映射出的像素点为:[Y0 U0 V5] [Y1 U0 V5] [Y2 U2 V7] [Y3 U2 V7]
[Y5 U0 V5] [Y6 U0 V5] [Y7U2 V7] [Y8 U2 V7]
内存则是:yyyyyyyyuuvv
需要占用的内存:w * h * 3 / 2

YUV 4:1:1采样
是在水平方向上对色度进行4:1抽样。对于低端用户和消费类产品这仍然是可以接受的。对非压缩的8比特量化的视频来说,每个由4个水平方向相邻的像素组成的宏像素需要占用6字节内存

比如四个像素为: [Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]
存放的码流为: Y0 U0 Y1 Y2 V2 Y3
映射出像素点为:[Y0 U0 V2] [Y1 U0 V2] [Y2 U0 V2] [Y3 U0 V2]
内存大小:可以参考4:2:2分量,是进一步压缩,每隔四个点才采一次U和V分量。一般是第1点采Y,U,第2点采Y,第3点采YV,第4点采Y,依次类推。

除了4:4:4采样,其余采样后信号重新还原显示后,会丢失部分UV数据,只能用相临的数据补齐,但人眼对UV不敏感,因此总体感觉损失不大。

YUV格式有两大类:planar和packed

planar:YUV的存储中与RGB格式最大不同在于,RGB格式每个点的数据是连继保存在一起的。即R,G,B是前后不间隔的保存在2-4byte空间中。而YUV的数据中为了节约空间,U,V分量空间会减小。每一个点的Y分量独立保存,但连续几个点的U,V分量是保存在一起的.这几个点合起来称为macro-pixel, 这种存储格式称为Packed(打包)格式。对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。

packed:对于packed的YUV格式,每个像素点的Y,U,V是连续交*存储的

YUV420p:又叫planer平面模式,Y ,U,V分别再不同平面,也就是有三个平面。
I420:又叫YU12,安卓的模式。存储顺序是先存Y,再存U,最后存V。YYYYUUUVVV
YV12:存储顺序是先存Y,再存V,最后存U。YYYVVVUUU

YUV420sp:又叫bi-planer或two-planer双平面,Y一个平面,UV在同一个平面交叉存储
NV12:IOS只有这一种模式。存储顺序是先存Y,再UV交替存储。YYYYUVUVUV
NV21:安卓的模式。存储顺序是先存Y,再存U,再VU交替存储。YYYYVUVUVU

版权声明:
作者:DramaScript
链接:https://www.jianshu.com/p/96366af0a8e9
来源:
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

用手机录制视频,就是采集了YUV图像数据然后编码再封装为视频文件如xxx.mp4。
为了实现YUV编码为h264的程序,需要先得到YUV图像序列文件。

如何获取YUV图像序列文件

这里我们不通过摄像机采集YUV图像数据,直接利用ffmpeg提供的命令行从mp4视频文件解封装得到h264文件,再从h264文件解码得到YUV图像序列文件。

mp4--->h264(ffmpeg命令行)

获取途径参考 ffmpeg入门教程https://www.jianshu.com/p/042c7847bd8a

注意:mp4文件不要太大,有个5S左右即可,不然测试会浪费贼多时间,可以使用Adobe Premiere,或者"剪映APP"剪辑视频,如果用ffmpeg命令行剪辑视频,必须找到关键帧,否则剪辑出来的视频会卡帧。

ffmpeg入门教程之YUV编码成H264详解(翻译)_第7张图片
在这里插入图片描述

步骤如下:
1.打开CMD,进入shared/bin目录
2.输入如下代码:

ffmpeg -i C:\Users\Administrator\Desktop\video.mp4 -codec copy -bsf: h264_mp4toannexb 
-f h264  C:\Users\Administrator\Desktop\video.h264
ffmpeg入门教程之YUV编码成H264详解(翻译)_第8张图片
在这里插入图片描述

说明:

-i C:\Users\Administrator\Desktop\video.mp4 :是输入的MP4文件
-codec copy:从MP4封装中进行拷贝
-bsf: h264_mp4toannexb:从MP4拷贝到annexB封装
-f h264:采用h.264格式
C:\Users\Administrator\Desktop\video.h264:输出的文件名称

下面,我们来播放h264文件,可以用ffmpeg提供的命令行工具ffplay播放,也可以下载工具播放(这个贼爽)


ffmpeg入门教程之YUV编码成H264详解(翻译)_第9张图片
在这里插入图片描述

ffmpeg入门教程之YUV编码成H264详解(翻译)_第10张图片
在这里插入图片描述

可能mp4的时长和解封装出来的h264时长不一样,这是因为帧率不同导致的。不打紧,先不管。

h264--->yuv(ffmpeg命令行)

步骤如下:
1.打开CMD,进入shared/bin目录
2.输入如下代码:

ffmpeg -i C:\Users\Administrator\Desktop\video.h264 -s 1920x1080 -pix_fmt yuv420p 
C:\Users\Administrator\Desktop\video_1920x1080.yuv

说明:

-i C:\Users\Administrator\Desktop\video.h264:是输入的h264文件
-s 1920x1080:指定图片分辨率
-pix_fmt yuv420p :设置像素格式yuv420p
C:\Users\Administrator\Desktop\video_1920x1080.yuv:输出的yuv文件,这里写1920x1080,是因为我知道我用的视频文件分辨率就是1920x1080的。


ffmpeg入门教程之YUV编码成H264详解(翻译)_第11张图片
在这里插入图片描述

下面,我们来播放h264文件,可以用ffmpeg提供的命令行工具ffplay播放,也可以下载工具YUVPlayer播放(这个贼爽)

下载地址https://sourceforge.net/projects/raw-yuvplayer/

ffmpeg入门教程之YUV编码成H264详解(翻译)_第12张图片
在这里插入图片描述

这里显示有207张yuv图片,
注意:如果yuv文件名带像素意思,YUVPlayer能自动根据文件名设置播放的像素宽高,如video_1920x1080.yuv,或者bridge-close_qcif.yuv
如果文件名不带像素意思,那么YUVPlayer无法知道yuv文件的像素大小,会播放花屏。

ffmpeg入门教程之YUV编码成H264详解(翻译)_第13张图片
在这里插入图片描述

花屏


ffmpeg入门教程之YUV编码成H264详解(翻译)_第14张图片
在这里插入图片描述

已然迫不及待开始敲代码

整体流程图:


ffmpeg入门教程之YUV编码成H264详解(翻译)_第15张图片
在这里插入图片描述

编码流程图:


ffmpeg入门教程之YUV编码成H264详解(翻译)_第16张图片
在这里插入图片描述

首先创建一个头文件include/encode_video_yuv_h264.h,引入相关头文件
#include 
//为了能写C++,需要extern "C" ,C++还是比C得劲
extern "C" {
#include 
#include "libavformat/avformat.h"
#include 
using namespace std;
}

定义一个方法并且实现它

int main_encode_video_yuv_h264(char *fileInPath, const char *fileOutPath, 
AVPixelFormat pix_fmt,int width, int height, int fps)

参数分别是输入文件路径,输出文件路径,像素格式,视频宽度,视频高度,视频帧率。

在其中定义一些会使用到的变量

    AVFormatContext *avFormatContextOut;
    AVStream *avStream;
    AVCodecContext *avCodecContext;
    AVCodec *avCodec;
    AVPacket *avPacket;
    uint8_t *picture_buf;
    AVFrame *avFrame;
    int y_size;
    FILE *fileInput = fopen(fileInPath, "rb");   //Input raw YUV data
    FILE *fileOutput = fopen(fileOutPath, "wb");

fopen()

打开输入输出文件,我们需要调用fopen函数,
fopen函数调用的是common_fsopen函数,定义如下:

 Opens the file named by 'file_name' as a stdio stream.  The 'mode' determines
         the mode in which the file is opened and the 'share_flag' determines the
         sharing mode.  Supported modes are "r" (read), "w" (write), "a" (append),
         "r+" (read and write), "w+" (open empty for read and write), and "a+" (read
         and append).  A "t" or "b" may be appended to the mode string to request text
         or binary mode, respectively.

         Returns the FILE* for the newly opened stream on success; returns nullptr on
         failure.
         template 
        static FILE* __cdecl common_fsopen(
        Character const* const file_name,
        Character const* const mode,
        int              const share_flag
        ) throw()

打开名为“file_name”的文件作为stdio流。mode确定打开文件的模式,“share_flag”确定共享模式。
支持的模式是"r"(读)、"w"(写)、"a"(添加),"r+"(读和写)、"W+"(读和写空)和"a+"(读和添加)。
可以将""t""或"b"附加到模式字符串以请求文本、或二进制模式。
成功时返回新打开的流的文件*;失败是返回Nullptr。
rb 度二进制文件, wb写二进制文件

初始化AVFormatContext

将编码数据写入编码文件时,可以借助AVFormatContext,后面很多操作会用到AVFormatContext

    avformat_alloc_output_context2(&avFormatContextOut, NULL, NULL, fileOutPath);

该函数为输出格式初始化AVFormatContext指针。

创建AVStream

将编码数据写入编码文件时,需要经过AVStream,

    avStream = avformat_new_stream(avFormatContextOut, 0);

该函数创建一个用于输出的AVStream指针对象

获取AVCodec

将YUV像素数据编码成h264文件,必须使用编码器

avCodec = avcodec_find_encoder(avFormatContextOut->oformat->video_codec);
if (!avCodec) {
        printf("Can not find encoder! \n");
        goto end;
}

该函数由AVFormatContext的AVOutputFormat的AVCodecID查找已注册的编码器AVCodec

配置AVCodecContext

编码过程需要借助AVCodecContext

avCodecContext = avcodec_alloc_context3(avCodec);
avCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
avCodecContext->pix_fmt = pix_fmt;
avCodecContext->width = width;
avCodecContext->height = height;
avCodecContext->time_base.num = 1;
avCodecContext->time_base.den = fps;

avcodec_alloc_context3(),根据编码器AVCodec为AVCodecContext指针对象分配内存,并且将其属性初始化为默认值;
设置AVCodecContext的AVMediaType为视频;
设置AVCodecContext的像素格式;
设置AVCodecContext的宽高;

YUV420P

这里我们设置AVCodecContext的像素格式为YUV420P,
planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
YUV 4:2:0采样,每四个Y共用一组UV分量,一个YUV占8+2+2 = 12bits(12bpp:12bit per pixel) 1.5个字节

AVRational time_base


    * This is the fundamental unit of time (in seconds) in terms
    * of which frame timestamps are represented. For fixed-fps content,
    * timebase should be 1/framerate and timestamp increments should be
    * identically 1.
    * This often, but not always is the inverse of the frame rate or field rate
    * for video. 1/time_base is not the average frame rate if the frame rate is not
    * constant.
    *
    * Like containers, elementary streams also can store timestamps, 1/time_base
    * is the unit in which these timestamps are specified.
    * As example of such codec time base see ISO/IEC 14496-2:2001(E)
    * vop_time_increment_resolution and fixed_vop_rate
    * (fixed_vop_rate == 0 implies that it is different from the framerate)
    *
    * - encoding: MUST be set by user.
    * - decoding: the use of this field for decoding is deprecated.
    *             Use framerate instead.
    */

time_base是表示要被显示的帧的时间戳的基本时间单位(以秒为单位)。对于固定的fps(帧率,,一帧/S,frame per senconds)内容,时基应该是1/framerate,时间戳增量应该是相同的1。通常,但并不总是与视频的帧速率或场速率互为倒数(比如有些time_base={2,25})。

如果帧速率不是固定的,则1/time_base不是平均帧速率。和容器一样,基本流也可以存储时间戳,
1/time_base是指定这些时间戳的单位。
编码时:必须调用者设置。
解码时:解码器的time_base已经被废弃,用framerate帧率(一帧/S)代替
这里设置帧率为25即可(这里测试用的YUV视频序列是从MP4视频文件(帧率为30.15帧/秒)提取的6S视频,如果想编码出来的文件(比如h264)的视频时长和原来截取的MP4视频文件时长一样,那么设置帧率为30.15,当然,会被自动取整为30)

打开编码器

avcodec_open2()

 * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
 * function the context has to be allocated with avcodec_alloc_context3().
 * @warning This function is not thread safe!
   if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
        printf("Failed to open encoder! \n");
        goto end;
    }

初始化AVCodecContext以使用给定的avcodec。使用该函数前必须使用avcodec_alloc_context_3()为AVCodecContext指针分配内存。
@警告此函数非线程安全!

初始化AVFrame

YUV图片数据将会存储到AVFrame,再传递给AVPacket

 * This structure describes decoded (raw) audio or video data.
 *
 * AVFrame must be allocated using av_frame_alloc(). Note that this only
 * allocates the AVFrame itself, the buffers for the data must be managed
 * through other means (see below).
 * AVFrame must be freed with av_frame_free().
 *
 * AVFrame is typically allocated once and then reused multiple times to hold
 * different data (e.g. a single AVFrame to hold frames received from a
 * decoder). In such a case, av_frame_unref() will free any references held by
 * the frame and reset it to its original clean state before it
 * is reused again.
 */
typedef struct AVFrame

该结构体描述解码后的(原始)音频或视频数据(它data属性用于存储原始数据)。avFrame必须使用AV_frame_alloc()分配内存。
请注意,该函数仅分配AVFrame本身的内存,对其数据缓冲区进行管理必须通过其他方式(见下文)。
avFrame必须使用AV_frame_free()释放。
avFrame通常被分配一次,然后重复使用多次以持有不同的数据(例如,单个AVFrame,用于保存从解码器解码得到的帧)。在这种情况下,
av_frame_unref()将释放任何由帧占据的内存,并在其下次使用之前将其重置为其原始清空状态。

初始化AVFrame用到如下函数:

     * Allocate an AVFrame and set its fields to default values.  The resulting
     * struct must be freed using av_frame_free().
     * @note this only allocates the AVFrame itself, not the data buffers. Those
     * must be allocated through other means, e.g. with av_frame_get_buffer() or
     * manually.
    avFrame = av_frame_alloc();

为AVFrame指针分配内存并将其属性设置为默认值。返回的AVFrame结构体指针必须使用av_framework_free()释放内存。
@注意,这只分配AVFrame本身,而不是数据缓冲区。
数据缓冲区必须通过其他方式分配,例如使用av_framework_get_Buffer()或手动分配。

计算存储一张图片所需要的内存大小

定义 int picture_size,用来表示存储一张图片所需要的内存大小

av_image_get_buffer_size()

     * Return the size in bytes of the amount of data required to store an
     * image with the given parameters.
     *
     * @param pix_fmt  the pixel format of the image
     * @param width    the width of the image in pixels
     * @param height   the height of the image in pixels
     * @param align    the assumed linesize alignment
     * @return the buffer size in bytes, a negative error code in case of failure
    int picture_size = av_image_get_buffer_size(avCodecContext->pix_fmt, 
    avCodecContext->width,avCodecContext->height, 1);

返回能存储给定参数的图像所需数据量的大小(以字节为单位)。
@param pix_fmt图像的像素格式
@param width图像的宽度(像素)
@param height图像的高度(像素)
@param align 字节对齐跨度
数据类型自身的对齐值:为指定平台上基本类型的长度。对于char型数据,其自身对齐值为1,
对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
此处是存储像素数据,当然用char,字节对齐是1跨度
@返回缓冲区大小(以字节为单位),在失败时返回负数

分配存储图像数据的内存块

分配一块unsigned char 内存,用来存储图像数据,

typedef unsigned char      uint8_t;
     * Allocate a memory block with alignment suitable for all memory accesses
     * (including vectors if available on the CPU).
    picture_buf = (uint8_t *) av_malloc(picture_size);

使用适用于所有内存访问的对齐方式分配内存块(包括在CPU上可用的vector)。

为AVFrame指定data大小和linesize大小

为AVFrame指定data大小和linesize大小,后面会为data填充图像数据

 av_image_fill_arrays(avFrame->data, avFrame->linesize, picture_buf, avCodecContext->pix_fmt,
                         avCodecContext->width, avCodecContext->height, 1);

函数解析如下:

av_image_fill_arrays()

 * Setup the data pointers and linesizes based on the specified image
 * parameters and the provided array.
 *
 * The fields of the given image are filled in by using the src
 * address which points to the image data buffer. Depending on the
 * specified pixel format, one or multiple image data pointers and
 * line sizes will be set.  If a planar format is specified, several
 * pointers will be set pointing to the different picture planes and
 * the line sizes of the different planes will be stored in the
 * lines_sizes array. Call with src == NULL to get the required
 * size for the src buffer.
 *
 * To allocate the buffer and fill in the dst_data and dst_linesize in
 * one call, use av_image_alloc().
 *
 * @param dst_data      data pointers to be filled in
 * @param dst_linesize  linesizes for the image in dst_data to be filled in
 * @param src           buffer which will contain or contains the actual image data, can be NULL
 * @param pix_fmt       the pixel format of the image
 * @param width         the width of the image in pixels
 * @param height        the height of the image in pixels
 * @param align         the value used in src for linesize alignment
 * @return the size in bytes required for src, a negative error code
 * in case of failure
 */
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);

根据指定的图像参数和提供的数组设置数据指针和行大小。
通过指向图像数据缓冲区的src地址填充给定图像的属性。
根据指定的像素格式,设置一个或多个图像数据指针和行大小。
如果指定了平面格式,那么将设置指向不同图片平面的几个指针,
并且不同平面的行大小将存储在linesize数组中。
调用src==NULL,以获得src缓冲区所需的大小。
要在一个调用中分配缓冲区并填写dst_data和dst_linesize,请使用av_Image_alloc()。
@param 将要被填充的dst_data数据指针,
@param 将要被填充的dst_datast的行大小
@param src 将包含或包含实际图像数据的缓冲区,可以为NULL
@param 图像的像素格式
@param 图像的宽度,以像素为单位
@param 图像的高度,以像素为单位
@param src中用于行大小对齐的值
@返回 src所需的大小,在发生故障时返回负错误码,

AVFrame uint8_t *data[AV_NUM_DATA_POINTERS]

AVFrame中的属性uint8_t *data[AV_NUM_DATA_POINTERS],用来存储图片数据

     * pointer to the picture/channel planes.
    uint8_t *data[AV_NUM_DATA_POINTERS];

指向图片/频道平面的指针。AV_NUM_DATA_POINTERS是常量8,表示data数组元素个数为8

AVFrame int linesize[AV_NUM_DATA_POINTERS]

AVFrame中的属性int linesize[AV_NUM_DATA_POINTERS],定义如下

     * For video, size in bytes of each picture line.
     * For audio, size in bytes of each plane.
     *
     * For audio, only linesize[0] may be set. For planar audio, each channel
     * plane must be the same size.
     *
     * For video the linesizes should be multiples of the CPUs alignment
     * preference, this is 16 or 32 for modern desktop CPUs.
     * Some code requires such alignment other code can be slower without
     * correct alignment, for yet other it makes no difference.
     *
     * @note The linesize may be larger than the size of usable data -- there
     * may be extra padding present for performance reasons.
     */
    int linesize[AV_NUM_DATA_POINTERS];

对于视频,指的是每一行图片的大小(以字节为单位)。
对于音频,指的是每个平面的大小(以字节为单位)。
对于音频,只设置lisnesize[0]。对于平面音频,每个通道平面必须是相同的大小。
对于视频,行大小应该是CPU对齐跨度的倍数,对于目前CPU来说是16或32。
如果没有字节对齐,CPU访问主存储器(内存)的效率会比较低
@请注意,linesize可能大于可用数据的大小-由于性能原因,可能会有额外的填充。

将流头写入输出文件

    avformat_write_header(avFormatContextOut, NULL);

初始化流的私有数据并将流头写入输出媒体文件

初始化AVPacket

AVFrame的data存储的是输入的YUV文件的图像数据,编码的过程就是将AVFrame的data数据发送给AVCodecContext,AVPacket从AVCodecContext接收数据存储到data,然后将AVPacket的data数据写入输出文件,即大功告成。

 * This structure stores compressed data. It is typically exported by demuxers
 * and then passed as input to decoders, or received as output from encoders and
 * then passed to muxers.
 *
 * For video, it should typically contain one compressed frame. For audio it may
 * contain several compressed frames. Encoders are allowed to output empty
 * packets, with no compressed data, containing only side data
 * (e.g. to update some stream parameters at the end of encoding).
 *
 * AVPacket is one of the few structs in FFmpeg, whose size is a part of public
 * ABI. Thus it may be allocated on stack and no new fields can be added to it
 * without libavcodec and libavformat major bump.
 *
 * The semantics of data ownership depends on the buf field.
 * If it is set, the packet data is dynamically allocated and is
 * valid indefinitely until a call to av_packet_unref() reduces the
 * reference count to 0.
 *
 * If the buf field is not set av_packet_ref() would make a copy instead
 * of increasing the reference count.
 *
 * The side data is always allocated with av_malloc(), copied by
 * av_packet_ref() and freed by av_packet_unref().
 *
 * @see av_packet_ref
 * @see av_packet_unref
 */
typedef struct AVPacket

此结构体存储压缩数据(编码后的数据,编码的目的就是压缩数据,节省内存,提高存取效率和传输效率)。
它通常由demuxers(解封装器,解封装可以得到编码的数据)导出,然后作为输入传递给解码器,或者作为编码器的输出接收,
然后传递给muxers(封装器,封装可以将编码的数据封装为视频格式文件)。
对于视频,通常应该包含一个压缩帧。对于音频,它可以包含几个压缩帧。
编码器允许输出空数据包,没有压缩数据,只包含side数据(例如,在编码结束时更新一些流参数)。

如果设置了数据包,则数据包数据将被动态分配,并且内存一直占有,
直到对av_packet_unref()的调用将引用计数降至0。
如果未设置buf属性,那么av_packet_ref()将创建一个副本,而不是增加引用计数。
side数据总是使用av_malloc()分配,由av_packet_ref()复制,由av_packet_unref()释放。

可以通过如下代码初始化AVPacket:

    avPacket = av_packet_alloc();
    * Allocate an AVPacket and set its fields to default values.  The resulting
     * struct must be freed using av_packet_free().
     * @note this only allocates the AVPacket itself, not the data buffers. Those
     * must be allocated through other means such as av_new_packet.

为AVPacket指针分配内存并将其属性设置为默认值。AVPacket结构体指针内存必须调用av_packet_free()释放。
@注意,这只分配AVPacket*本身的内存,而不是数据缓冲区。数据缓冲区必须通过其他方式分配,例如av_new_packet。

然后通过如下代码为AVPacket分配内存容量:

av_new_packet(avPacket, picture_size);

传入上面计算得到的存储一张图片所需要的内存大小picture_size,因为后面需要将AVFrame存储的图像数据传递到AVPacket,所以AVPacket需要picture_size容量的内存。

函数解析如下:

     * Allocate the payload of a packet and initialize its fields with
     * default values.

分配数据包的内存容量并使用默认值初始化其属性。
注意:AVPacket必须先初始化才能调用该方法,因为该方法并未初始化AVPacket

YUV内存占用

上文说到YUV存储格式

如果视频帧的宽和高分别为w和h,那么一帧YUV420P像素数据一共占用wh3/2 Byte的数据。
其中前wh Byte存储Y,接着的wh1/4 Byte存储U,最后wh*1/4 Byte存储V

定义一个变量记录Y占用的内存大小,代码如下:

    y_size = avCodecContext->width * avCodecContext->height;

读取YUV图像数据

fread()

fread定义如下:

         Reads data from a stream into the result buffer.  The function reads elements
         of size 'element_size' until it has read 'element_count' elements, until the
         buffer is full, or until EOF is reached.

         Returns the number of "whole" elements that were read into the buffer.  This
         may be fewer than the requested number of elements if an error occurs or if
         EOF is encountered.  In this case, ferror() or feof() should be used to
         distinguish between the two conditions.

         If the result buffer becomes full before the requested number of elements are
         read, the buffer is zero-filled, zero is returned, and errno is set to ERANGE.
        extern "C" size_t __cdecl fread(
            void*  const buffer,
            size_t const element_size,
            size_t const element_count,
            FILE*  const stream
            )

从数据流中读取数据到结果缓冲区。函数读取element_size个字节,
直到读取element_count个元素(element_size*element_count个字节),
直到缓冲区已满,或直到达到EOF为止。
返回读入缓冲区的元素的数量element_count。
出错或读到文件末尾时返回的记录数小于 count,
在这种情况下,应使用FerroError()或FEOF()区分这两种情况。
如果结果缓冲区在所请求读取的元素数量读取完毕之前填满,读取,缓冲区为零填充,返回0,errno设置为erange。

上面求得一张图片Y占用内存大小为y_size=wh ,那么一张图片U占用内存大小为wh/2,V占用内存大小为w*h/2,一张图片总共占用内存大小为y_size * 3 / 2

利用fread函数读取YUV文件的图片数据到picture_buf,picture_buf 是unsigned char *,无符号字符数组,图片一张张的读,然后一张张地存储到AVFrame的data,即一帧数据,然后一帧帧地传递给编码器,一帧帧地编码压缩到AVPacket,再一个个AVPacket地写入h264文件。

这里读取 y_size * 3 / 2个元素,返回值必须== y_size * 3 / 2,才说明读取成功,否则失败

    while (fread(picture_buf, 1, y_size * 3 / 2, fileInput) == y_size * 3 / 2) {

上文说到YUV420P像素数据是连续分开存储的,那么代码应该如下:

  while (fread(picture_buf, 1, y_size * 3 / 2, fileInput) == y_size * 3 / 2) {
        avFrame->data[0] = picture_buf;              // Y
        avFrame->data[1] = picture_buf + y_size;      // U
        avFrame->data[2] = picture_buf + y_size + y_size / 4;  // V
  }

time_base转换

————————————————
版权声明
:以下摘抄为CSDN博主「bixinwei」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/bixinwei22/article/details/78770090

PTS:Presentation Time Stamp。PTS主要用于度量解码后的视频帧什么时候被显示出来
DTS:Decode Time Stamp。DTS主要是标识读入内存中的bit流在什么时候开始送入解码器中进行解码

也就是pts反映帧什么时候开始显示,dts反映数据流什么时候开始解码

怎么理解这里的“什么时候”呢?如果有某一帧,假设它是第10秒开始显示。那么它的pts是多少呢。是10?还是10s?还是两者都不是。

为了回答这个问题,先引入FFmpeg中时间基的概念,也就是time_base。它也是用来度量时间的。
如果把1秒分为25等份,你可以理解就是一把尺,那么每一格表示的就是1/25秒。此时的time_base={1,25}
如果你是把1秒分成90000份,每一个刻度就是1/90000秒,此时的time_base={1,90000}。
所谓时间基表示的就是每个刻度是多少秒
pts的值就是占多少个时间刻度(占多少个格子)。它的单位不是秒,而是时间刻度。只有pts加上time_base两者同时在一起,才能表达出时间是多少。
好比我只告诉你,某物体的长度占某一把尺上的20个刻度。但是我不告诉你,这把尺总共是多少厘米的,你就没办法计算每个刻度是多少厘米,你也就无法知道物体的长度。
pts=20个刻度
time_base={1,10} 每一个刻度是1/10厘米
所以物体的长度=ptstime_base=201/10 厘米

在ffmpeg中。av_q2d(time_base)=每个刻度是多少秒
此时你应该不难理解 pts*av_q2d(time_base)才是帧的显示时间戳。

下面理解时间基的转换,为什么要有时间基转换。
首先,不同的封装格式,timebase是不一样的。另外,整个转码过程,不同的数据状态对应的时间基也不一致。拿mpegts封装格式25fps来说(只说视频,音频大致一样,但也略有不同)。非压缩时候的数据(即YUV或者其它),在ffmpeg中对应的结构体为AVFrame,它的时间基为AVCodecContext 的time_base ,AVRational{1,25}。
压缩后的数据(对应的结构体为AVPacket)对应的时间基为AVStream的time_base,AVRational{1,90000}。
因为数据状态不同,时间基不一样,所以我们必须转换,在1/25时间刻度下占10格,在1/90000下是占多少格。这就是pts的转换。

根据pts来计算一桢在整个视频中的时间位置:
timestamp(秒) = pts * av_q2d(st->time_base)

duration和pts单位一样,duration表示当前帧的持续时间占多少格。或者理解是两帧的间隔时间是占多少格。一定要理解单位。
pts:格子数
av_q2d(st->time_base): 秒/格

计算视频长度:
time(秒) = st->duration * av_q2d(st->time_base)

ffmpeg内部的时间与标准的时间转换方法:
ffmpeg内部的时间戳 = AV_TIME_BASE * time(秒)
AV_TIME_BASE_Q=1/AV_TIME_BASE

av_rescale_q(int64_t a, AVRational bq, AVRational cq)函数
这个函数的作用是计算a*bq / cq来把时间戳从一个时间基调整到另外一个时间基。在进行时间基转换的时候,应该首先这个函数,因为它可以避免溢出的情况发生。
函数表示在bq下的占a个格子,在cq下是多少。

关于音频pts的计算:
音频sample_rate:samples per second,即采样率,表示每秒采集多少采样点。
比如44100HZ,就是一秒采集44100个sample.
即每个sample的时间是1/44100秒

一个音频帧的AVFrame有nb_samples个sample,所以一个AVFrame耗时是nb_samples(1/44100)秒
即标准时间下duration_s=nb_samples
(1/44100)秒,
转换成AVStream时间基下
duration=duration_s / av_q2d(st->time_base)
基于st->time_base的num值一般等于采样率,所以duration=nb_samples.
pts=nduration=nnb_samples

补充:
next_pts-current_pts=current_duration,根据数学等差公式an=a1+(n-1)d可得pts=nd

在某些场景下涉及到PTS的计算时,就涉及到两个Time的转换,以及到底取哪里的time_base进行转换:

场景1:编码器产生的数据AVPacket,写入编码文件(如h264)或者封装格式文件(mp4),需要借助AVStream完成写入操作,那么此时packet的pts要从AVCodecContext的pts转换成目标AVStream的pts

场景2:从一种封装格式demux(解封装)出来的源AVStream ,存入另一个封装格式中某个目的AVStream 。
此时的时间刻度pts应该从源AVStream 的time_base,转换成目的AVStream time_base下的时间刻度pts。

其实,问题的关键还是要理解,不同的场景下取到的数据帧的time_base是相对哪个时间体系的。
demux出来的帧的pts:是相对于源AVStream的pts
编码器出来的帧的pts:是相对于源AVCodecContext的pts
mux存入文件等容器的pts:是相对于目的AVStream的pts

将YUV编码数据AVPacket写入h264文件,可以借助AVFormatContext,AVFormatContext能帮我们创建AVStream,AVStream将AVPacket写入h264文件,那么我们必须将AVPacket的pts时间刻度转换成AVStream的pts时间刻度,换句话说就是:

编码器产生的帧,直接存入某个容器的AVStream中,那么此时packet的Time要从AVCodecContext的time转换成目标AVStream的time

那么代码将会如下所示:

  int i = 0;
  while (fread(picture_buf, 1, y_size * 3 / 2, fileInput) == y_size * 3 / 2) {
        avFrame->data[0] = picture_buf;              // Y
        avFrame->data[1] = picture_buf + y_size;      // U
        avFrame->data[2] = picture_buf + y_size + y_size / 4;  // V
        avFrame->pts = i++ * avCodecContext->time_base.num / avCodecContext->time_base.den /
                       (avStream->time_base.num / avStream->time_base.num);
 }
 cout << "frameCount=" << i << endl;

用i记录图片数量,frameCount输出是207,因为小编这里用的YUV文件正好是207张图片
举例来说:
FPS = 30,time_base = {1, 90000},则
duration = (1 / 30) / (1 / 90000) = 3000,
可以这么理解:
帧率是 30 HZ,每帧的持续时间是 1 / 30 秒,时间单位是 1 / 90000 秒, 那么,每帧的 duration 就是 3000 个时间单位。

那么第1张图片在i=0时,即pts =0刻度时显示,
第2张图片在i=1时,即

avFrame->pts = avCodecContext->time_base.num / avCodecContext->time_base.den /
                       (avStream->time_base.num / avStream->time_base.num)

时显示。

YUV编码成H264过程

定义一个编码函数如下:

int encode(AVCodecContext *avCodecContext, AVFrame *avFrame, AVPacket *avPacket, FILE *fileOut) {
    int ret;
    if (avFrame)
        printf("Send frame %3"
               PRId64"\n", avFrame->pts);
//    cout << "avcodec_send_frame++++++执行了208次(一共207帧图片,最后一次是flush)";
    ret = avcodec_send_frame(avCodecContext, avFrame);
    if (ret < 0) {
        fprintf(stderr, "Error sending a frame for encoding\n");
        return -1;
    }
    while (ret >= 0) {
    //        cout << "avcodec_receive_packet++++++执行了415次(最后一次是flush)";
        ret = avcodec_receive_packet(avCodecContext, avPacket);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return 0;
        else if (ret < 0) {
            fprintf(stderr, "Error during encoding\n");
            return -1;
        }
        printf("Write packet %3"
               PRId64" (size=%5d)\n", avPacket->pts, avPacket->size);
         //将AvPacket的data写入输出文件
        fwrite(avPacket->data, 1, avPacket->size, fileOut);
        //释放AvPacket*内存
        av_packet_unref(avPacket);
    }
    return 0;
}

返回-1,表示编码出错

过程如图所示:


ffmpeg入门教程之YUV编码成H264详解(翻译)_第17张图片
在这里插入图片描述

avcodec_send_frame()

 * Supply a raw video or audio frame to the encoder. Use avcodec_receive_packet()
 * to retrieve buffered output packets.
 *
 * @param avctx     codec context
 * @param[in] frame AVFrame containing the raw audio or video frame to be encoded.
 *                  Ownership of the frame remains with the caller, and the
 *                  encoder will not write to the frame. The encoder may create
 *                  a reference to the frame data (or copy it if the frame is
 *                  not reference-counted).
 *                  It can be NULL, in which case it is considered a flush
 *                  packet.  This signals the end of the stream. If the encoder
 *                  still has packets buffered, it will return them after this
 *                  call. Once flushing mode has been entered, additional flush
 *                  packets are ignored, and sending frames will return
 *                  AVERROR_EOF.
 *
 *                  For audio:
 *                  If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
 *                  can have any number of samples.
 *                  If it is not set, frame->nb_samples must be equal to
 *                  avctx->frame_size for all frames except the last.
 *                  The final frame may be smaller than avctx->frame_size.
 * @return 0 on success, otherwise negative error code:
 *      AVERROR(EAGAIN):   input is not accepted in the current state - user
 *                         must read output with avcodec_receive_packet() (once
 *                         all output is read, the packet should be resent, and
 *                         the call will not fail with EAGAIN).
 *      AVERROR_EOF:       the encoder has been flushed, and no new frames can
 *                         be sent to it
 *      AVERROR(EINVAL):   codec not opened, refcounted_frames not set, it is a
 *                         decoder, or requires flush
 *      AVERROR(ENOMEM):   failed to add packet to internal queue, or similar
 *      other errors: legitimate decoding errors
 */
int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame);

该函数向编码器提供原始视频或音频帧。使用avcodec_receive_packet()检索缓冲的输出数据包。
AVFrame包含要编码的原始音频或视频帧。帧的所有权由调用方保留,编码器不会写入该帧。
编码器可以创建对帧数据的引用(如果帧是未被引用计数的,则复制它)。

它可以是空的,在这种情况下,它被认为是一个刷新缓冲区的数据包。这意味着流的结束。如果编码器仍然有缓冲数据包,
它将在这个调用之后返回它们。一旦进入刷新模式,额外的刷新数据包将被忽略,发送帧将返回AVERROR_EOF。
对于音频:如果设置了AV_CODEC_CAP_VARIABLE_FRAME_SIZE,那么每个帧都可以有任意数量的采样。
(viriable_frame:可变帧率,就是帧率随时间变化而不固定)
如果没有设置,则frame->nb_samples 必须等于avctx->frame_size(最后一个除外)。
最终帧可能小于avctx->Frame_size。

在成功时返回0,否则是负数错误代码:
*AVERROR(EAGAIN):在当前状态下不接受输入-必须用avcodec_receive_packet()读取输出
* (一旦所有输出被读取,数据包应该被重新发送,并且调用不会在EAGAIN中失败)。
*AVERROR_EOF:编码器已被刷新,没有任何新帧可以发送给它
*AVERROR(EINVAL):编解码器未打开,refcounted_frames未设置,它是解码器,或要求刷新
*AVERROR(ENOMEM):未能向内部队列添加数据包,或类似的其他错误:合法解码错误

那么代码将会如下所示:

  while (fread(picture_buf, 1, y_size * 3 / 2, fileInput) == y_size * 3 / 2) {
        avFrame->data[0] = picture_buf;              // Y
        avFrame->data[1] = picture_buf + y_size;      // U
        avFrame->data[2] = picture_buf + y_size + y_size / 4;  // V
        avFrame->pts = i++ * avCodecContext->time_base.num / avCodecContext->time_base.den /
                       (avStream->time_base.num / avStream->time_base.num);
        //返回-1表示编码出错,应结束循环
        if (encode(avCodecContext, avFrame, avPacket, fileOutput)== -1)break;
    }
  encode(avCodecContext, NULL, avPacket, fileOutput);

上面在encode返回-1时,表示编码出错,结束YUV像素的读取
读取完毕之后再调用一次encode,目的是flush,刷新数据内存缓冲区(因为文件一般存储于海量存储器或者磁盘,主存储器(内存)用于程序的数据临时存放,如果文件读写过程中,CPU不停地访问磁盘,既效率低又磨损磁盘,而且容易阻塞,所以就有了内存缓冲区的概念,CPU访问内存的效率很高,所以将文件相关的数据临时存储到内存再一口气写入到磁盘,这样性能较高)

可以看到avcodec_send_frame()一共执行了208次(一共207帧图片,最后一次是flush)

avcodec_receive_packet()

定义如下:

 Read encoded data from the encoder.
 *
 * @param avctx codec context
 * @param avpkt This will be set to a reference-counted packet allocated by the
 *              encoder. Note that the function will always call
 *              av_frame_unref(frame) before doing anything else.
 * @return 0 on success, otherwise negative error code:
 *      AVERROR(EAGAIN):   output is not available in the current state - user
 *                         must try to send input
 *      AVERROR_EOF:       the encoder has been fully flushed, and there will be
 *                         no more output packets
 *      AVERROR(EINVAL):   codec not opened, or it is an encoder
 *      other errors: legitimate decoding errors
 */
int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt);

从编码器读取编码数据。
@param avctx编解码器上下文
@param avpkt请注意,该函数始终在执行任何其他操作之前会调用av_frame_unref()。
@return 0成功,否则负数错误代码:
AVERROR(EAGAIN):输出在当前状态中不可用-必须尝试发送输入
AVERROR_EOF:编码器已被完全flush,并且不再有输出数据包
AVERROR(EINVAL):编解码器未打开,或者是编码器
其他错误:合法解码错误

一次avcodec_send_frame要调用2次avcodec_receive_packet(avcodec_receive_packet返回==0说明还能继续接收数据),
否则会丢帧,如果是flush,当然只是一次

avcodec_receive_packet()执行了415次(最后一次是flush)

fwrite()

和上文中fread()意思类似

写文件尾

编码数据写入完毕后,添加文件尾,代码如下:

    av_write_trailer(avFormatContextOut);

end

写入文件完毕后,应该释放各种内存:

 end:
    //清理各种内存
    avformat_free_context(avFormatContextOut);
    avio_close(avFormatContextOut->pb);
    avcodec_close(avCodecContext);
    av_free(picture_buf);
    av_free(avFrame);
    fclose(fileInput);
    fclose(fileOutput);

完整代码如下

include/encode_video_yuv_h264.h:

//
// Created by Administrator on 2019/10/24 0024.
//

#ifndef FFMPEGDEMO_ENCODE_VIDEO_YUV_H264_H
#define FFMPEGDEMO_ENCODE_VIDEO_YUV_H264_H


#include 

extern "C" {
#include 
#include "libavformat/avformat.h"
#include 
using namespace std;
}
/**
 *
 * @param avCodecContext
 * @param avFrame
 * @param avPacket
 * @param fileOut
 * @return 返回-1表示编码出错
 */
int encode(AVCodecContext *avCodecContext, AVFrame *avFrame, AVPacket *avPacket, FILE *fileOut) {
    int ret;
    if (avFrame)
        printf("Send frame %3"
               PRId64"\n", avFrame->pts);
    /**
 * 向编码器提供原始视频或音频帧。使用avcodec_receive_packet()检索缓冲的输出数据包。
     AVFrame包含要编码的原始音频或视频帧。帧的所有权由调用方保留,编码器不会写入该帧。
     编码器可以创建对帧数据的引用(如果帧是未被引用计数的,则复制它)。
     它可以是空的,在这种情况下,它被认为是一个刷新缓冲区的数据包。这意味着流的结束。如果编码器仍然有缓冲数据包,
     它将在这个调用之后返回它们。一旦进入刷新模式,额外的刷新数据包将被忽略,发送帧将返回AVERROR_EOF。
     对于音频:如果设置了AV_CODEC_CAP_VARIABLE_FRAME_SIZE,那么每个帧都可以有任意数量的采样。
     (viriable_frame:可变帧率,就是帧率随时间变化而不固定)
     如果没有设置,则frame->nb_samples 必须等于avctx->frame_size(最后一个除外)。
     最终帧可能小于avctx->Frame_size。
    @在成功时返回0,否则是负数错误代码:
     *AVERROR(EAGAIN):在当前状态下不接受输入-必须用avcodec_receive_packet()读取输出
     * (一旦所有输出被读取,数据包应该被重新发送,并且调用不会在EAGAIN中失败)。
     *AVERROR_EOF:编码器已被刷新,没有任何新帧可以发送给它
     *AVERROR(EINVAL):编解码器未打开,refcounted_frames未设置,它是解码器,或要求刷新
     *AVERROR(ENOMEM):未能向内部队列添加数据包,或类似的其他错误:合法解码错误
 */
//    cout << "avcodec_send_frame++++++执行了208次(一共207帧图片,最后一次是flush)";
    ret = avcodec_send_frame(avCodecContext, avFrame);
    if (ret < 0) {
        fprintf(stderr, "Error sending a frame for encoding\n");
        return -1;
    }

    while (ret >= 0) {
//        cout << "avcodec_receive_packet++++++执行了415次(最后一次是flush)";
        /**
         *从编码器读取编码数据。
         * @param avctx编解码器上下文
         * @param avpkt请注意,该函数始终在执行任何其他操作之前会调用av_frame_unref()。
         * @return 0成功,否则负数错误代码:
         *AVERROR(EAGAIN):输出在当前状态中不可用-必须尝试发送输入
         *AVERROR_EOF:编码器已被完全flush,并且不再有输出数据包
         *AVERROR(EINVAL):编解码器未打开,或者是编码器
         *其他错误:合法解码错误
         * 一次avcodec_send_frame要调用2次avcodec_receive_packet(avcodec_receive_packet返回==0说明还能继续接收数据),
         * 否则会丢帧,如果是flush,当然只是一次
         */
        ret = avcodec_receive_packet(avCodecContext, avPacket);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return 0;
        else if (ret < 0) {
            fprintf(stderr, "Error during encoding\n");
            return -1;
        }
        printf("Write packet %3"
               PRId64" (size=%5d)\n", avPacket->pts, avPacket->size);
        //将AvPacket的data写入输出文件
        fwrite(avPacket->data, 1, avPacket->size, fileOut);
        //释放AvPacket*内存
        av_packet_unref(avPacket);
    }
    return 0;
}

int main_encode_video_yuv_h264(char *fileInPath, const char *fileOutPath, AVPixelFormat pix_fmt,int width, int height, int fps) {

    AVFormatContext *avFormatContextOut;
    AVStream *avStream;
    AVCodecContext *avCodecContext;
    AVCodec *avCodec;
    /*
     此结构体存储压缩数据(编码后的数据,编码的目的就是压缩数据,节省内存,提高存取效率和传输效率)。
     它通常由demuxers(解封装器,解封装可以得到编码的数据)导出,然后作为输入传递给解码器,或者作为编码器的输出接收,
     然后传递给muxers(封装器,封装可以将编码的数据封装为视频格式文件)。
     对于视频,通常应该包含一个压缩帧。对于音频,它可以包含几个压缩帧。
     编码器允许输出空数据包,没有压缩数据,只包含side数据(例如,在编码结束时更新一些流参数)。

     如果设置了数据包,则数据包数据将被动态分配,并且内存一直占有,
     直到对av_packet_unref()的调用将引用计数降至0。
     如果未设置buf属性,那么av_packet_ref()将创建一个副本,而不是增加引用计数。
     side数据总是使用av_malloc()分配,由av_packet_ref()复制,由av_packet_unref()释放。
   */
    AVPacket *avPacket;
    uint8_t *picture_buf;
    /**
     * 该结构描述解码后的(原始)音频或视频数据(它data属性用于存储原始数据)。avFrame必须使用AV_frame_alloc()分配内存。
     * 请注意,该函数仅分配AVFrame本身的内存,对其数据缓冲区进行管理必须通过其他方式(见下文)。
     * avFrame必须使用AV_frame_free()释放。
     * avFrame通常被分配一次,然后重复使用多次以持有不同的数据
     * (例如,单个AVFrame,用于保存从解码器解码得到的帧)。在这种情况下,
     * av_frame_unref()将释放任何由帧占据的内存,并在其下次使用之前将其重置为其原始清空状态。
     */
    AVFrame *avFrame;
    int y_size;
    /**
         打开名为“file_name”的文件作为stdio流。mode确定打开文件的模式,“share_flag”确定共享模式。
         支持的模式是"r"(读)、"w"(写)、"a"(添加),"r+"(读和写)、"W+"(读和写空)和"a+"(读和添加)。
         可以将""t""或"b"附加到模式字符串以请求文本、或二进制模式。
         成功时返回新打开的流的文件*;失败是返回Nullptr。
         rb 度二进制文件 wb写二进制文件
     */
    FILE *fileInput = fopen(fileInPath, "rb");   //Input raw YUV data
    FILE *fileOutput = fopen(fileOutPath, "wb");
    //为输出格式初始化AVFormatContext指针。
    avformat_alloc_output_context2(&avFormatContextOut, NULL, NULL, fileOutPath);
    //创建一个用于输出的AVStream指针对象
    avStream = avformat_new_stream(avFormatContextOut, 0);
    //由AVFormatContext的AVOutputFormat的AVCodecID查找已注册的编码器AVCodec
    avCodec = avcodec_find_encoder(avFormatContextOut->oformat->video_codec);
    if (!avCodec) {
        printf("Can not find encoder! \n");
        goto end;
    }
    //根据编码器AVCodec为AVCodecContext指针对象分配内存,并且将其属性初始化为默认值
    avCodecContext = avcodec_alloc_context3(avCodec);
    //设置AVCodecContext的AVMediaType为视频
    avCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
    //设置AVCodecContext的像素格式为YUV420P,
    // planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
    //YUV 4:2:0采样,每四个Y共用一组UV分量,一个YUV占8+2+2 = 12bits(12bpp:12bit per pixel) 1.5个字节
    avCodecContext->pix_fmt = pix_fmt;
    //设置AVCodecContext的宽高
    avCodecContext->width = width;
    avCodecContext->height = height;
    /**
     time_base是表示要被显示的帧的时间戳的基本时间单位(以秒为单位)。对于固定的fps(帧率,,一帧/S,frame per senconds)内容,
     时基应该是1/framerate,时间戳增量应该是相同的1。通常,但并不总是与视频的帧速率或场速率互为倒数。
     如果帧速率不是固定的,则1/time_base不是平均帧速率。和容器一样,基本流也可以存储时间戳,
     1/time_base是指定这些时间戳的单位。
     编码时:必须调用者设置。
     *-解码时:解码器的time_base已经被废弃,用framerate帧率(一帧/S)代替
     *这里设置帧率为25即可(这里测试用的YUV视频序列是从MP4视频文件(帧率为30.15帧/秒)提取的6S视频,
     * 如果想编码出来的文件(比如h264)的视频时长和原来截取的MP4视频文件时长一样,那么设置帧率为30.15,当然,会被自动取整为30)
    */
    avCodecContext->time_base.num = 1;
    avCodecContext->time_base.den = fps;
    cout << "avCodecContext->time_base.den:" << avCodecContext->time_base.den << endl;
    /**打印输入或输出格式的详细信息,
     is_output:0表示input,1表示output
   */
    av_dump_format(avFormatContextOut, 0, fileOutPath, 1);
    /**
     初始化AVCodecContext以使用给定的avcodec。使用该函数前必须使用avcodec_alloc_context_3()为AVCodecContext指针分配内存。
     @警告此函数非线程安全!
    */
    if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
        printf("Failed to open encoder! \n");
        goto end;
    }
    /*
     * 为AVFrame指针分配内存并将其属性设置为默认值。返回的AVFrame结构体指针必须使用av_framework_free()释放内存。
     * 。**@注意,这只分配AVFrame本身,而不是数据缓冲区。
     * 数据缓冲区必须通过其他方式分配,例如使用av_framework_get_Buffer()或手动分配。
     */
    avFrame = av_frame_alloc();
    /**
     * 返回能存储给定参数的图像所需数据量的大小(以字节为单位)。
     * @param pix_fmt图像的像素格式
     * @param width图像的宽度(像素)
     * @param height图像的高度(像素)
     * @param align 字节对齐跨度
     * 数据类型自身的对齐值:为指定平台上基本类型的长度。对于char型数据,其自身对齐值为1,
     * 对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
     * 此处是存储像素数据,当然用char,字节对齐是1跨度
     * @返回缓冲区大小(以字节为单位),在失败时返回负数
     */
    int picture_size = av_image_get_buffer_size(avCodecContext->pix_fmt, avCodecContext->width,
                                                avCodecContext->height, 1);
    /**
     * Allocate a memory block with alignment suitable for all memory accesses
     * (including vectors if available on the CPU).
     * 使用适用于所有内存访问的对齐方式分配内存块(包括在CPU上可用的vector)。
     */
    picture_buf = (uint8_t *) av_malloc(picture_size);
    /*
     *根据指定的图像参数和提供的数组设置数据指针和行大小。
     * 通过指向图像数据缓冲区的src地址填充给定图像的属性。
     * 根据指定的像素格式,设置一个或多个图像数据指针和行大小。
     * 如果指定了平面格式,那么将设置指向不同图片平面的几个指针,
     * 并且不同平面的行大小将存储在linesize数组中。
     * 调用src==NULL,以获得src缓冲区所需的大小。
     * 要在一个调用中分配缓冲区并填写dst_data和dst_linesize,请使用av_Image_alloc()。
     * @param 将要被填充的dst_data数据指针,
     * @param 将要被填充的dst_datast的行大小
     * @param src 将包含或包含实际图像数据的缓冲区,可以为NULL
     * @param 图像的像素格式
     * @param 图像的宽度,以像素为单位
     * @param 图像的高度,以像素为单位
     * @param src中用于行大小对齐的值
     * @返回 src所需的大小,在发生故障时返回负错误码,
     */
    av_image_fill_arrays(avFrame->data, avFrame->linesize, picture_buf, avCodecContext->pix_fmt,
                         avCodecContext->width, avCodecContext->height, 1);
    //初始化流的私有数据并将流头写入输出媒体文件
    avformat_write_header(avFormatContextOut, NULL);

    cout << "picture_size:" << picture_size << endl;
    /**
     * 为AVPacket指针分配内存并将其属性设置为默认值。AVPacket结构体指针内存必须调用av_packet_free()释放。
     * @注意,这只分配AVPacket*本身的内存,而不是数据缓冲区。数据缓冲区必须通过其他方式分配,例如av_new_packet。
     */
    avPacket = av_packet_alloc();

    /**
     * 分配数据包的内存容量并使用默认值初始化其属性。
     * 注意:AVPacket*必须先初始化才能调用该方法,因为该方法并未初始化AVPacket*
     */
    av_new_packet(avPacket, picture_size);
    //AV_PIX_FMT_YUV420P,   ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
    //YUV 4:2:0采样,每四个Y共用一组UV分量,一个YUV占8+2+2 = 12bits(12bpp:12bit per pixel) 1.5个字节
    /**
     * YUV格式有两大类:planar和packed。
       planar:YUV的存储中与RGB格式最大不同在于,RGB格式每个点的数据是连继保存在一起的。
       即R,G,B是前后不间隔的保存在2-4byte空间中。而YUV的数据中为了节约空间,U,V分量空间会减小。
       每一个点的Y分量独立保存,但连续几个点的U,V分量是保存在一起的.这几个点合起来称为macro-pixel,
       这种存储格式称为Packed(打包)格式。对于planar的YUV格式,先连续存储所有像素点的Y,
       紧接着存储所有像素点的U,随后是所有像素点的V。
       八个像素为:[Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3]
        [Y5 U5 V5] [Y6 U6 V6] [Y7U7 V7] [Y8 U8 V8]
        存放的码流为:Y0 U0 Y1 Y2 U2 Y3
        Y5 V5 Y6 Y7 V7 Y8
        映射出的像素点为:[Y0 U0 V5] [Y1 U0 V5] [Y2 U2 V7] [Y3 U2 V7]
        [Y5 U0 V5] [Y6 U0 V5] [Y7U2 V7] [Y8 U2 V7]
        内存则是:yyyyyyyyuuvv
        需要占用的内存:w * h * 3 / 2
        如果视频帧的宽和高分别为w和h,那么一帧YUV420P像素数据一共占用w*h*3/2 Byte的数据。
        其中前w*h Byte存储Y,接着的w*h*1/4 Byte存储U,最后w*h*1/4 Byte存储V
     */
    y_size = avCodecContext->width * avCodecContext->height;
    int i = 0;
    /**
         从数据流中读取数据到结果缓冲区。函数读取element_size个字节,
         直到读取element_count个元素(element_size*element_count个字节),
         直到缓冲区已满,或直到达到EOF为止。
         返回读入缓冲区的元素的数量element_count。
         出错或读到文件末尾时返回的记录数小于 count,
         在这种情况下,应使用FerroError()或FEOF()区分这两种情况。
         如果结果缓冲区在所请求读取的元素数量读取完毕之前填满,读取,缓冲区为零填充,返回0,errno设置为erange。
     */
    while (fread(picture_buf, 1, y_size * 3 / 2, fileInput) == y_size * 3 / 2) {
        avFrame->data[0] = picture_buf;              // Y
        avFrame->data[1] = picture_buf + y_size;      // U
        avFrame->data[2] = picture_buf + y_size + y_size / 4;  // V
        /**
         * 举例来说:
        FPS = 30,time_base = {1, 90000},则
        duration = (1 / 30) / (1 / 90000) = 3000,
        可以这么理解:
        帧率是 30 HZ,每帧的持续时间是 1 / 30 秒,时间单位是 1 / 90000 秒,  那么,每帧的 duration 就是 3000 个时间单位。
         编码器产生的帧,直接存入某个容器的AVStream中,那么此时packet的Time要从AVCodecContext的time转换成目标AVStream的time
         */
        avFrame->pts = i++ * avCodecContext->time_base.num / avCodecContext->time_base.den /
                       (avStream->time_base.num / avStream->time_base.num);
//        avFrame->pts = i++ * (avStream->time_base.den) / ((avStream->time_base.num) * avCodecContext->time_base.den);
        //返回-1表示编码出错,应结束循环
        if (encode(avCodecContext, avFrame, avPacket, fileOutput)== -1)break;
    }
    cout << "frameCount=" << i << endl;
    cout << "avStream->time_base.den=" << avStream->time_base.den << endl;
    cout << "avStream->time_base.num=" << avStream->time_base.num << endl;
    cout << "(avStream->time_base.den) / ((avStream->time_base.num) * avCodecContext->time_base.den)="
         << (avStream->time_base.den) / ((avStream->time_base.num) * avCodecContext->time_base.den) << endl;
    encode(avCodecContext, NULL, avPacket, fileOutput);
    av_write_trailer(avFormatContextOut);

    end:
    //清理各种内存
    avformat_free_context(avFormatContextOut);
    avio_close(avFormatContextOut->pb);
    avcodec_close(avCodecContext);
    av_free(picture_buf);
    av_free(avFrame);
    fclose(fileInput);
    fclose(fileOutput);
    return 0;
}

#endif //FFMPEGDEMO_ENCODE_VIDEO_YUV_H264_H

调用方式如下:
main.cpp:

#define  _CRT_SECURE_NO_WARNINGS

#include 
//#include 
//#include 
#include 

int main() {
//    main_remuxer("../resources/video.avi","../resources/video.mp4");
//    main_encode_video("../resources/video_1920x1080.yuv","../resources/video_1920x1080.h264");
    main_encode_video_yuv_h264("../resources/video_1920x1080.yuv","../resources/video_1920x1080.h264",AV_PIX_FMT_YUV420P,1920,1080,30);
    system("pause");
    return 0;
}

输入yuv文件,输出h264文件,如图:

ffmpeg入门教程之YUV编码成H264详解(翻译)_第18张图片
在这里插入图片描述

GitHub:https://github.com/AnJiaoDe/FFmpegDemo

欢迎分享、转载、联系、指正、批评、撕逼

Github:https://github.com/AnJiaoDe

:https://www.jianshu.com/u/b8159d455c69

CSDN:https://blog.csdn.net/confusing_awakening

ffmpeg入门教程:https://www.jianshu.com/p/042c7847bd8a

微信公众号


ffmpeg入门教程之YUV编码成H264详解(翻译)_第19张图片
这里写图片描述

QQ群

ffmpeg入门教程之YUV编码成H264详解(翻译)_第20张图片
这里写图片描述

你可能感兴趣的:(ffmpeg入门教程之YUV编码成H264详解(翻译))