FFmpeg/SDL相关

FFMpeg 实现视频编码、解码、封装、解封装、转码、缩放

FFmpeg音视频解码过程

  1. 初始化网络 avformat_network_init
  2. 打开文件avformat_open_input
  3. 从文件中提取流信息avformat_find_stream_info
  4. 在多个数据流中找到视频流(类型为MEDIA_TYPE_VIDEO)和音频流(AVMEDIA_TYPE_AUDIO
  5. 查找相对应的解码器avcodec_find_decoder
  6. 打开解码器avcodec_open2
  7. 从数据流中读取数据到Packet中av_read_frame
  8. 将数据送入到解码器内部avcodec_send_packet
  9. 为解码帧分配内存av_frame_alloc
  10. 接收解码帧avcodec_receive_frame
  11. 解码完成后需要关闭avformat_open_input打开的输入流,avcodec_open2打开的CODEC
    image.png

    注:注册所支持的文件格式和遍解码器CODECav_register_all接口已经遗弃,不再需要调用这个接口注册了。参考:ffmpeg4.2.2 av_register_all()的分析

打开一个视频解码器示例

// 获取视频流
videoStream = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
// 获取codec
AVCodec *vcodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);
// 构造AVCodecContext ,并将vcodec填入AVCodecContext中
AVCodecContext *vc = avcodec_alloc_context3(vcodec); 
// 初始化AVCodecContext
avcodec_parameters_to_context(vc, ic->streams[videoStream]->codecpar); 
// 打开解码器,由于之前调用avcodec_alloc_context3(vcodec)初始化了vc,
// 那么codec(第2个参数)可以填NULL
int ret = avcodec_open2(vc, NULL,NULL); 

FFmpeg重要结构体

参考:FFMPEG中最关键的结构体之间的关系

AVCodecContext

音视频编解码器上下文。作为AVStream的一个字段存在。avcodec_open2avcodec_send_packetavcodec_receive_frame打开编解码器和解码时依赖。在使用avcodec_open2打开后,需要使用avcodec_close将其关闭。

AVCodecContext* avcodecCtx = avformatCtx->streams[streamId]->codec;

AVCodec

AVCodec是存储编解码器信息的结构体,表示音视频编解码器,着重于功能函数。
参考: AVCodecContext和AVCodec

AVFormatContext

在FFmpeg中有很重要的作用,描述一个多媒体文件的构成及其基本信息,存放了视频编解码过程中的大部分信息。通常该结构体由avformat_open_input分配存储空间,在最后调用avformat_input_close关闭。

AVStream

AVStream表示AVFormatContext中一条具体的流结构,比如:音频流、视频流和字幕流等。在解码的过程中,作为AVFormatContext的一个字段存在,不需要单独的处理。

AVStream* avstream = avformatCtx->streams[streamId];

AVpacket

用来存放解码之前的数据,它只是一个容器,其data成员指向实际的数据缓冲区,在解码的过程中可由av_read_frame创建和填充AVPacket中的数据缓冲区,当数据缓冲区不再使用的时候可以调用av_free_apcket释放这块缓冲区,最新接口改为av_packet_unref释放。
对于视频(Video)来说,AVPacket通常包含一个压缩的Frame,而音频(Audio)则有可能包含多个压缩的Frame。并且,一个Packet有可能是空的,不包含任何压缩数据,只含有side data(side data,容器提供的关于Packet的一些附加信息。例如,在编码结束的时候更新一些流的参数)。
AVPacket结构定义如下:

typedef struct AVPacket {
    // 若为空,则表示AVPacket未使用引用计数管理负载数据,否则指向存储负载数据的引用计数AVBufferRef -> AVBuffer
    AVBufferRef *buf;
    
    // 基于AVStream->time_base的pts,若文件中没有,则为AV_NOPTS_VALUE。
    // pts必须大于等于dts,因为显示不能早于解码
    int64_t pts;
    
    // 基于AVStream->time_base的dts,若文件中没有,则为AV_NOPTS_VALUE。
    int64_t dts;
    // 负载数据
    uint8_t *data;
    // 负载数据的长度
    int   size;
    // 属于AVFormatContext中的哪个AVStream
    int   stream_index;

    // AV_PKT_FLAG_KEY表示关键帧
    int   flags;

    // 携带的不同类型的side data
    AVPacketSideData *side_data;
    int side_data_elems;

    // 当前帧的持续时间,基于AVStream->time_base
    int64_t duration;
    // byte position in stream, -1 if unknown
    int64_t pos;                            
} AVPacket;

参考:
FFmpeg数据结构:AVPacket解析
FFmpeg之AVPacket

AVFrame

typedef struct AVFrame {

     //指向图片/频道平面的指针
    uint8_t *data[AV_NUM_DATA_POINTERS];

    //对于视频,每行图片的大小以字节为单位。
    //对于音频,每个平面的大小(以字节为单位)。
    int linesize[AV_NUM_DATA_POINTERS];
    .....

    //此帧描述的音频采样数(每个通道)
    int nb_samples;

    // 音频数据的采样率
    int sample_rate;
    .....
} AVFrame;

存放从AVPacket中解码出来的原始数据,其必须通过av_frame_alloc来创建,通过av_frame_free来释放。

AVFrame *rgbFrame = av_frame_alloc();

AVPacket类似,AVFrame中也有一块数据缓存空间data,在调用av_frame_alloc的时候并不会为这块缓存区域分配空间,需要使用av_image_alloc来为其申请空间。

av_image_alloc(rgbFrame->data, rgbFrame->linesize, videoWidth, videoHeight, AV_PIX_FMT_RGB32, 1);

在解码的过程使用了两个AVFrame,这两个AVFrame分配缓存空间的方法也不相同:
一个AVFrame用来存放从AVPacket中解码出来的原始数据,这个AVFrame的数据缓存空间通过调avcodec_decode_video分配和填充。
另一个AVFrame用来存放将解码出来的原始数据变换为需要的数据格式(例如RGB,RGBA)的数据,这个AVFrame需要手动的分配数据缓存空间。

FFmpeg重要概念

PTS和DTS

PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
音频的PTS和DTS是一致的,而含有B帧的视频会存在DTS和PTS不一致的帧,我们这里主要通过PTS来控制播放时间。对解码后的AVFrame使用av_frame_get_best_effort_timestamp可以获取PTS,再乘以time_base即可得到实际时间。只有AVStream中获取的time_base才是对的,其他地方获取的可能会有问题。
avformatCtx->streams[streamId]->time_base * pts

FFmpeg相关API解析

解码相关

av_find_best_stream

获取音视频对应的stream_index。
av_find_best_stream(fmt, type, -1, -1, NULL, 0);
fmtavformat_open_input获取的AVFormatContext实例。

图像内存相关

av_image_alloc

此函数的功能是按照指定的宽、高、像素格式来申请图像内存。

int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
                   int w, int h, enum AVPixelFormat pix_fmt, int align);

参数说明如下:

pointers[4]:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址,第四个指针保留不用。
linesizes[4]:保存图像每个通道的内存对齐的步长,即一行的对齐内存的宽度,此值大小等于图像宽度。 
w:                 要申请内存的图像宽度。 
h:                  要申请内存的图像高度。 
pix_fmt:        要申请内存的图像的像素格式。 
align:            用于内存对齐的值。 
返回值:所申请的内存空间的总大小。如果是负值,表示申请失败。

av_image_get_buffer_size

针对给图像申请内存,有另外一种方式,即使用av_image_get_buffer_sizeav_malloc来申请图像所需的内存。

int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align);

align:此参数是设定内存对齐的对齐数,也就是按多大的字节进行内存对齐。比如设置为1,表示按1字节对齐,那么得到的结果就是与实际的内存大小一样。再比如设置为4,表示按4字节对齐。也就是内存的起始地址必须是4的整倍数。
av_image_get_buffer_size用来返回指定格式、宽高及对齐下的图像所需空间的大小。使用样例如下:

//计算指定像素格式、图像宽、高所需要的对齐的内存大小
int image_buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);   
//分配指定大小的内存空间
out_buffer = (unsigned char *)av_malloc(image_buf_size);   

av_image_fill_arrays

/**
* Setup the data pointers and linesizes based on the specified image
* parameters and the provided array.
**/
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的数据填充到dst_datadst_linesize里。参数具体说明如下:

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内存时指定的对齐字节数。

SWS图像转换相关

重点参考:雷霄骅的最简单的基于FFmpeg的libswscale的示例,其中有对图像拉伸的算法SWS_BICUBICSWS_BILINEARSWS_POINT做了详细的解释,并对像素格式AV_PIX_FMT_有所描述。

sws_getContext

struct SwsContext *sws_getContext(
            int srcW, /* 输入图像的宽度 */
            int srcH, /* 输入图像的宽度 */
            enum AVPixelFormat srcFormat, /* 输入图像的像素格式 */
            int dstW, /* 输出图像的宽度 */
            int dstH, /* 输出图像的高度 */
            enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
            int flags,/* 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR */
            SwsFilter *srcFilter, /* 输入图像的滤波器信息, 若不需要传NULL */
            SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
            const double *param /* 特定缩放算法需要的参数(?),默认为NULL */
            );

初始化SwsContext的函数之一,成功返回SwsContext,否则返回NULL。参数说明:
其中flags,可以设置为SWS_BICUBIC性能比较好,SWS_FAST_BILINEAR在性能和速度之间有一个比好好的平衡,而SWS_POINT的效果比较差。

sws_getCachedContext

struct SwsContext *sws_getCachedContext(struct SwsContext *context,
                                        int srcW, int srcH, enum AVPixelFormat srcFormat,
                                        int dstW, int dstH, enum AVPixelFormat dstFormat,
                                        int flags, SwsFilter *srcFilter,
                                        SwsFilter *dstFilter, const double *param);

初始化SwsContext的函数之一,成功返回SwsContext,否则返回NULL。参数说明:

int srcW, /* 输入图像的宽度 */
int srcH, /* 输入图像的宽度 */
enum AVPixelFormat srcFormat, /* 输入图像的像素格式 */
int dstW, /* 输出图像的宽度 */
int dstH, /* 输出图像的高度 */
enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
int flags,/* 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR */
SwsFilter *srcFilter, /* 输入图像的滤波器信息, 若不需要传NULL */
SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
const double *param /* 特定缩放算法需要的参数(?),默认为NULL */

sws_scale

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
              const int srcStride[], int srcSliceY, int srcSliceH,
              uint8\_t *const dst[], const int dstStride[]);

该函数实现了三个功能:1.图像色彩空间转换;2.分辨率缩放;3.前后图像滤波处理。
参数说明如下:

  1. 参数 SwsContext *c, 转换格式的上下文。也就是 sws_getContext 函数返回的结果。
  2. 参数 const uint8_t *const srcSlice[], 输入图像的每个颜色通道的数据指针。其实就是解码后的AVFrame中的data[]数组。因为不同像素的存储格式不同,所以srcSlice[]维数也有可能不同。
    以YUV420P为例,它是planar格式,它的内存中的排布如下:
    YYYYYYYY UUUU VVVV
    使用FFmpeg解码后存储在AVFramedata[]数组中时:
    data[0]——-Y分量, Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8……
    data[1]——-U分量, U1, U2, U3, U4……
    data[2]——-V分量, V1, V2, V3, V4……
    像素格式名称后面有“P”的,代表是planar格式,否则就是packed格式。Planar格式不同的分量分别存储在不同的数组中,Packed格式的数据都存储在同一个数组中,例如AV_PIX_FMT_RGB24存储方式如下:
    data[0]: R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4……
    AVFrame中的linesize[]数组中保存的是对应通道的数据宽度 ,
    linesize[0]——-Y分量的宽度
    linesize[1]——-U分量的宽度
    linesize[2]——-V分量的宽度
  3. 参数const int srcStride[],输入图像的每个颜色通道的跨度。也就是每个通道的行字节数,对应的是解码后的AVFrame中的linesize[]数组。根据它可以确立下一行的起始位置,不过stride和width不一定相同,这是因为:
    a. 由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样stride = width + N
    b. packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。
  4. 参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。
    如果srcSliceY=0; srcSliceH=height,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。并行处理加快速度。
  5. 参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个颜色通道数据指针,每个颜色通道行字节数)

参考:FFmepg中的sws_scale() 函数分析

swr_convert

int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, const uint8_t **in , int in_count);

该函数主要是实现音频文件的编码转换。

sws_setColorspaceDetails

初始化颜色空间

int sws_setColorspaceDetails(struct SwsContext *c, const int inv_table[4],
                             int srcRange, const int table[4], int dstRange,
                             int brightness, int contrast, int saturation);

c:需要设定的SwsContext。
inv_table:描述输出YUV颜色空间的参数表。
srcRange:输入图像的取值范围(“1”代表JPEG标准,取值范围是0-255;“0”代表MPEG标准,取值范围是16-235)。
table:描述输入YUV颜色空间的参数表。
dstRange:输出图像的取值范围。

时间戳相关

AVRational

FFMPEG的很多结构中有AVRational time_base这样的一个成员,它是AVRational结构的

typedef struct AVRational {
    int num; ///< numerator 分数
    int den; ///< denominator 分母
} AVRational;

AVRational这个结构标识一个分数,num为分数,den为分母。
实际上time_base的意思就是时间的刻度:
如(1,25),那么时间刻度就是1/25
(1,9000),那么时间刻度就是1/90000
那么,在刻度为1/25的体系下的time=5,转换成在刻度为1/90000体系下的时间time为(51/25)/(1/90000) = 36005=18000,即下面说到的函数av_rescale_q
参考:FFMPEG之TimeBase成员理解

av_rescale_q

// The operation is mathematically equivalent to `a * bq / cq`.
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;

该函数的作用是在不同的时间基之间进行转换。a表示要原始值,bq表示原来的时间基准;cq表示要转换到的时间基准。

  • 内部时间基准:AV_TIME_BASE
    AVFormatContext中的参数多是基于AV_TIME_BASE的。
  • 流时间基准:AVStream->time_base
    AVStreamAVPacketAVFrame中的时间戳多是基于对应的流时间基准。

参考:FFmpeg时间戳

操作相关

av_seek_frame

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);

参数说明:

s              // 描述了一个媒体文件或媒体流的构成和基本信息
stream_index   //表示当前的seek是针对哪个基本流,比如视频或者音频等等
timestamp      //将要定位的时间戳,时间戳会从以AV_TIME_BASE为单位向具体流的时间基自动转换。
flags          //
              AVSEEK_FLAG_BACKWARD  是seek到请求的timestamp之前最近的关键帧
              AVSEEK_FLAG_BYTE      是基于字节位置的查找 
              AVSEEK_FLAG_ANY       是可以seek到任意帧,注意不一定是关键帧,因此使用时可能会导致花屏    
              AVSEEK_FLAG_FRAME     是基于帧数量快进

在进行seek操作时,时间戳必须基于流时间基准(AVStream->time_base),比如:seek到第5秒,需做如下的操作:

// 首先计算出基于视频流时间基准的时间戳
int64_t timestamp = av_rescale_q(5 * AV_TIME_BASE, AV_TIME_BASE_Q, video_stream_->time_base);
// 然后seek
av_seek_frame(av_format_context, video_stream_index, timestamp, AVSEEK_FLAG_BACKWARD);

avcodec_flush_buffers

void avcodec_flush_buffers(AVCodecContext *avctx);

说明:回放过程中跳转播放,或者切换到一个不同的流,调用此函数,清空内部缓存的帧数据。
实际应用场景:在网络环境中,传输数据用UDP经常有丢包,而丢包很容易造成FFmpeg解码器缓冲的帧数增加,可以在一段时间内清空解码器缓存
切记:应该在接收到I帧的时候,清空缓存,否则出现画面不连续或者马赛克的情况

你可能感兴趣的:(FFmpeg/SDL相关)