ffmpeg 各库的介绍、解码流程、常用结构体

一、前言

在正式编写 FFmpeg 播放器前,我们需要先简单了解下所要用到的 FFmpeg 库、播放与解码流程、函数和相关结构体。

 

二、FFmpeg 库简介

介绍
avcodec 音视频编解码核心库
avformat 音视频容器格式的封装和解析
avutil 核心工具库
swscal 图像格式转换的模块
swresampel 音频重采样
avfilter 音视频滤镜库 如视频加水印、音频变声
avdevice 输入输出设备库,提供设备数据的输入与输出,和硬件设备交互

FFmpeg 就是依靠以上几个库,实现了强大的音视频编码、解码、编辑、转换、采集等能力。这里实现视频播放就除了 avfilter 库没用到。

 

三、FFmpeg播放流程

通常情况下,视频文件如 MP4,MKV、FLV 等都属于封装格式,就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。当我们播放一个媒体文件时,通常需要经过以下几个步骤:

可以看到这个视频播放器的实现需要涉及到以下内容:

  • 解封装(Demuxing):就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。
  • 软硬件解码(Decode):就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。通过解码,将压缩编码的视频数据 H.264,MPEG2 解码成为非压缩的颜色数据,例如 YUV 等等;将压缩编码的音频数据 AAC,MP3 解码成为非压缩的音频抽样数据,例如 PCM 数据。解码分为硬编码和软编码。
  • 像素格式转换:将 YUV 数据格式转换成 RGB 数据格式。
  • 重采样:对音频重新采样。
  • dts/pts:dts 是解码的时间戳,而 pts 是显示的时间戳。pts 用于获取当前播放进度。进度条移动需要用到av_seek_frame函数。
  • 音视频同步:就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的音频和视频数据,并将音视频频数据送至系统的显卡和声卡播放出来(Render)。

其中解码是最重要的,下面介绍一下解码的流程以及用到的 API 和结构体。


四、FFmpeg解码流程

 

五、使用到的FFmpeg API说明

5.1 av_register_all()

  • 注册 FFmpeg 的所有组件。
  • 在 4.0 版本以后已经被弃用,所以实际不加也可以正常编解码音视频。

 

5.2 avformat_alloc_context()

用于初始化 AVFormatContext 对象。其原型如下:

AVFormatContext *avformat_alloc_context(void)
  • 因为AVFormatContext 必须初始化为 NULL 或者用avformat_alloc_context()进行初始化。

 

5.3 avformat_open_input()

打开媒体文件,并获得解封装上下文。其原型如下:

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)
  • ps:AVFormatContext 双重指针,函数调用成功之后将解封装上下文赋值给 ps。
  • url:可以是 rtsp、http 网络流地址,或者本地视频文件路径。
  • fmt:指定输入音视频的封装格式,一般情况下可以设置为 nullptr,则会自动探索。
  • fmt:强制指定 AVFormatContext 的成员 AVInputFormat,即输入音视频的封装格式。一般情况下可以设置为 NULL,这样会自动探索 AVInputFormat。
  • options:附加的一些选项,一般情况下可以设置为 nullptr,但有时候播放 rtsp 时需要设置下。

 

5.4 avformat_find_stream_info()

探测获取流信息。其原型如下:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
  • 因为在一些格式当中没有头部信息,如 flv 格式,h264 格式,调用avformat_open_input()在打开文件之后就没有参数,也就无法获取到里面的信息。
  • 这个时候就可以调用此函数,因为它会试着去探测文件的格式,但是如果格式当中没有头部信息,那么它只能获取到编码、宽高这些信息,还是无法获得总时长。
  • 如果总时长无法获取到,则需要把整个文件读一遍,获取一下它的总帧数来计算。

 

5.5 avcodec_find_decoder()

查找解码器。函数的参数是所要用解码器的ID,成功返回查找到的解码器(没有找到就返回 NULL)。其原型如下:

AVCodec *avcodec_find_decoder(enum AVCodecID id);
  • id:查找到的解码器
  •  

5.6 avcodec_open2()

用于初始化一个音视频编解码器的 AVCodecContext,声明位于 libavcodec\utils.c。其原型如下:

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options)
  • avctx:需要初始化的 AVCodecContext。
  • codec:输入的AVCodec。
  • options:一些选项。例如使用 libx264 编码的时候,“preset”,“tune” 等都可以通过该参数设置。

 

5.7 av_read_frame()

读取码流中的音频若干帧或者视频一帧。例如,解码视频的时候,每解码一个视频帧,需要先调用av_read_frame()获得一帧视频的压缩数据,然后才能对该数据进行解码。其原型如下:

int av_read_frame(AVFormatContext *s, AVPacket *pkt)
  • s:解封装上下文。
  • pkt:存储一帧视频的压缩数据。

 

5.8 avcodec_decode_video2()

解码一帧视频数据。输入一个压缩编码的结构体 AVPacket,输出一个解码后的结构体 AVFrame。其原型如下:

int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
                         int *got_picture_ptr,
                         const AVPacket *avpkt);
  • avctx:需要初始化的 AVCodecContext。
  • codec:输入的AVCodec。
  • options:一些选项。例如使用libx264编码的时候,“preset”,“tune”等都可以通过该参数设置。

 

5.9 avformat_close_input()

关闭释放解封装上下文,并且设置为 0。其原型如下:

void avformat_close_input(AVFormatContext **s)
  • s:解封装上下文。

 

六、使用到的FFmpeg结构体说明

AVStream->codec->channel_layout:音频的通道格式类型(如单通道、双通道)

channel_layout_map[]
    { "mono",        1,  AV_CH_LAYOUT_MONO },
    { "stereo",      2,  AV_CH_LAYOUT_STEREO },
    { "2.1",         3,  AV_CH_LAYOUT_2POINT1 },
    { "3.0",         3,  AV_CH_LAYOUT_SURROUND },
    { "3.0(back)",   3,  AV_CH_LAYOUT_2_1 },
    { "4.0",         4,  AV_CH_LAYOUT_4POINT0 },
    { "quad",        4,  AV_CH_LAYOUT_QUAD },
    { "quad(side)",  4,  AV_CH_LAYOUT_2_2 },
    { "3.1",         4,  AV_CH_LAYOUT_3POINT1 },
    { "5.0",         5,  AV_CH_LAYOUT_5POINT0_BACK },
    { "5.0(side)",   5,  AV_CH_LAYOUT_5POINT0 },
    { "4.1",         5,  AV_CH_LAYOUT_4POINT1 },
    { "5.1",         6,  AV_CH_LAYOUT_5POINT1_BACK },
    { "5.1(side)",   6,  AV_CH_LAYOUT_5POINT1 },
    { "6.0",         6,  AV_CH_LAYOUT_6POINT0 },
    { "6.0(front)",  6,  AV_CH_LAYOUT_6POINT0_FRONT },
    { "hexagonal",   6,  AV_CH_LAYOUT_HEXAGONAL },
    { "6.1",         7,  AV_CH_LAYOUT_6POINT1 },
    { "6.1",         7,  AV_CH_LAYOUT_6POINT1_BACK },
    { "6.1(front)",  7,  AV_CH_LAYOUT_6POINT1_FRONT },
    { "7.0",         7,  AV_CH_LAYOUT_7POINT0 },
    { "7.0(front)",  7,  AV_CH_LAYOUT_7POINT0_FRONT },
    { "7.1",         8,  AV_CH_LAYOUT_7POINT1 },
    { "7.1(wide)",   8,  AV_CH_LAYOUT_7POINT1_WIDE },
    { "octagonal",   8,  AV_CH_LAYOUT_OCTAGONAL },
    { "downmix",     2,  AV_CH_LAYOUT_STEREO_DOWNMIX, },
channel_layout 对应关系

AVStream->codec->channels:音频的通道数

AVStream->codec->sample_fmt:音频的格式

带P和不带P,关系到了AVFrame中的data的数据排列,

不带P,则是LRLRLRLRLR排列,(每个LR为一个音频样本)

带P则是LLLLLRRRRR排列(平面格式),若是双通道则带P则意味着data[0]全是L,data[1]全是R(注意:该处是以点nb_samples采样点来交错,不是以字节交错,每个LLLLLLRRRRRR为一个音频帧),若您的通道数超过 8 个,则可以在 AVFrame 的extended_data属性中找到其余通道的数据。

PCM播放器播放的文件需要的是LRLRLRLR的。


 获取单个采样点的大小(如S16P):numBytes = av_get_bytes_per_sample(pAVCodecContext->sample_fmt);
 // 输出为2, S16P格式是16为即2字节
sample_fmt

AVStream->codec->sample_rate:音频的采样率

AVFrame->nb_samples:一帧音频数据中采样的数量(次数) // 如 aac 1024个采样次压缩一下为一帧,nb_samples就是1024

nb_samples表示一帧音频数据中采样的数量(次数),nb_samples与具体的码流类型和编码级别有关。nb_samples和AVCodecContext中的frame_size相同。

采样率44100Khz,采样格式s16,双声道时:

一秒44100个采样点,一个采样点2字节,总数据量为88200字节;一帧nb_samples次采样,数据量为nb_samples x 2 x 2;一秒有88200/(nb_sample x 4)帧音频。

编码类型    NB_SAMPLES
MP3                    1152
AAC LC            1024
AAC HE V2        2048        
nb_samples

 

 

6.1 AVFormatContext

解封装上下文,是存储音视频封装格式中包含信息的结构体。

char filename[1024] // 保存打开的文件名,一般用在 rtsp、rtmp 断开重连
unsigned int nb_streams // 音视频流的个数
AVStream **streams // 存储视频流、音频流、字幕流信息
int64_t duration // 媒体文件的总时长,单位是把 1 秒切成 AV_TIME_BASE(1000000)份,即单位。为 us,注意不一定每个视频都能获取到 duration
int64_t bit_rate // 比特率(单位bps,转换为kbps需要除以1000)

 

6.2 AVStream

AVStream 是存储每一个音频/视频流信息的结构体。其重要的变量如下所示:

int index // 标识该视频/音频流
AVCodecContext *codec // 解码器,4.0 版本后已弃用
AVRational time_base // 时基。通过该值可以把PTS,DTS转化为实际的时间(单位为秒s)
int64_t duration // 该视频/音频流时长,单位为 ms
AVRational avg_frame_rate // 帧率(注:对视频来说,这个挺重要的)
AVPacket attached_pic // 附带的图片。比如说一些 MP3,AAC 音频文件附带的专辑封面
AVCodecParameters *codecpar // 音视频参数,新增用来替换AVCodecContext *codec

 

6.3 AVCodecContext

AVCodecContext 是一个描述编解码器上下文的结构体,包含了众多编解码器需要的参数信息。下面挑一些关键的变量来看看(这里只考虑解码)。

enum AVMediaType codec_type // 编解码器的类型(视频,音频...)
struct AVCodec  *codec // 采用的解码器AVCodec(H.264,MPEG2...)    
enum AVCodecID codec_id // 标示特定的编解码器(H.264,MPEG2...)
int format // 视频像素格式/音频采样数据格式
int width, height // 表示视频的宽和高
int bit_rate // 平均比特率    
int channels // 声道数(音频)
uint64_t channel_layout // 声道格式
int sample_rate // 采样率(音频)
AVRational time_base; // 时基。通过该值可以把PTS,DTS转化为实际的时间(单位为秒s)
uint8_t *extradata; int extradata_size; // 针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)

 

6.4 AVCodec

AVCodec 是存储编码器信息的结构体。其重要的变量如下所示:

const char *name; // 编解码器的名字的简称
const char *long_name; // 编解码器名字的全称
enum AVMediaType type; // 指明了类型,是视频,音频,还是字幕
enum AVCodecID id; // ID,不重复
const AVRational *supported_framerates; // 支持的帧率(仅视频)
const enum AVPixelFormat *pix_fmts; // 支持的像素格式(仅视频),如RGB24、YUV420P等。
const int *supported_samplerates; // 支持的采样率(仅音频)
const enum AVSampleFormat *sample_fmts; // 支持的采样格式(仅音频)
const uint64_t *channel_layouts; // 支持的声道数(仅音频)
int priv_data_size; // 私有数据的大小

 

6.5 AVCodecParameters

新增用来替换AVCodecContext *codec。因为 AVCodecContext 结构体包含的参数太多,AVCodecParameters 将编码器的参数从 AVCodecContext 分离出来,AVCodecParameters 结构体中部分重要的参数如下:

enum AVMediaType codec_type // 编解码器的类型(视频,音频...)   
enum AVCodecID codec_id // 标示特定的编解码器(H.264,MPEG2...)
int format // 视频像素格式/音频采样数据格式
int width, height // 表示视频的宽和高
int bit_rate // 平均比特率    
int channels // 声道数(音频)
uint64_t channel_layout // 声道格式
int sample_rate // 采样率(音频)
AVRational time_base; // 时基。通过该值可以把PTS,DTS转化为实际的时间(单位为秒s)
uint8_t *extradata; int extradata_size; // 针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)

可以看到两者的成员基本一致。

avcodec_decode_video2():解码一帧视频数据
sws_scale():转换视频数据格式    
av_frame_free():释放xx上下文申请的内存
avcodec_close():关闭解码器

 

6.6 AVPacket

AVPacket 是存储压缩编码数据相关信息的结构体。其重要的变量如下所示:

uint8_t *data; // 压缩编码的数据。
/* 例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流。因此在使用FFMPEG进行音视频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到音视频的码流文件。*/
int size; // data的大小
int64_t pts; // 显示时间戳
int64_t dts; // 解码时间戳
int stream_index; // 标识该AVPacket所属的视频/音频流。

 

6.7 AVFrame

AVFrame 结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是 YUV,RGB,对音频来说是 PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP 表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用 FFmpeg 进行码流分析的时候,AVFrame 是一个很重要的结构体。

下面看几个主要变量的作用(在这里考虑解码的情况):

uint8_t *data[AV_NUM_DATA_POINTERS]; // 解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
int linesize[AV_NUM_DATA_POINTERS]; // data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。
int width, height; // 视频帧宽和高(1920x1080,1280x720...)
int nb_samples; // 音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个
int format; // 解码后原始数据类型(YUV420,YUV422,RGB24...)
int key_frame; // 是否是关键帧
enum AVPictureType pict_type; // 帧类型(I,B,P...)
AVRational sample_aspect_ratio; // 宽高比(16:9,4:3...)
int64_t pts; // 显示时间戳
int coded_picture_number; // 编码帧序号
int display_picture_number; // 显示帧序
posted @ 2023-03-18 13:13  封兴旺  阅读(897)  评论(0编辑  收藏  举报

联系方式: 18274305123(微信同号)