前言
书接上回,我们介绍了ffmpeg的一些基础知识,使用方法,接下来介绍如何使用ffmpeg进行开发,所谓使用ffmpeg进行开发,就是依赖它的基础库,调用它的API来实现我们的功能。
当然要看懂文章需要一些C++的基础知识,能看懂基本语法,了解指针(一级指针/二级指针)的基本知识。
image.png我们在上篇文章中也介绍了ffmpeg对于音视频操作的主要流程:
image.png其实差不多每个阶段都通过对应的关键函数以及关键结构体来对应,因此,接下来先重点介绍一下这些重要的结构体,以及它的用法。
核心结构体
AVFormatContext
媒体信息存储结构,同时管理了IO音视频流对文件进行读写,相当于保存了音视频信息的上下文。
它在ffmpeg中的作用是非常重要的,在封装/解封装/编解码过程中都需要用到它。在程序中关于某个音视频的所有信息归根结底都来自于AVFormatContext。
结构体信息
typedef struct AVFormatContext {
// 针对输入逻辑的结构体
const struct AVInputFormat *iformat;
// 针对输出逻辑的结构体
const struct AVOutputFormat *oformat;
//字节流IO操作 结构体
AVIOContext *pb;
...
...
unsigned int nb_streams; // 视音频流的个数
AVStream **streams; // 视音频流
char *url; //输入或输出地址 替换原有的filename
int64_t duration; // 时长,微秒(1s/1000_000)
int64_t bit_rate; // 比特率(单位bps,转换为kbps需要除以1000)
...
...
} AVFormatContext;
以上只是一个简略的结构体成员信息展示,但是已经能体现它管理IO,数据流,保存媒体信息的的功能了,对于初学者而言,只需要关注nb_streams和streams这两个成员,表示流的数量以及流数组。后面我们需要通过流来获取对应的信息。
使用的基本方法
输入过程
- 申请内存创建结构体
// 可以,但一般没必要
AVFormatContext *formatContext = avformat_alloc_context();
但是一般在开发中并不需要开发者手动创建结构体,而是在读取文件的接口中通过库自动创建即可
AVFormatContext *formatContext = NULL;
// 注意 即使传入的formatContext为NULL,avformat_open_input内部也会为formatContext申请内存空间的
if (avformat_open_input(&formatContext, "xxx.mp4", NULL, NULL) != 0) {
fprintf(stderr, "Failed to open input file\n");
return 1;
}
注意我们需要在avformat_open_input函数中传入正确的媒体文件路径,这样才能正确读取到文件头的信息。
- 为防止获取不到文件头信息,可以尝试进一步获取音视频流的信息
if (avformat_find_stream_info(formatContext, NULL) < 0) {
fprintf(stderr, "Failed to find stream information\n");
return 1;
}
avformat_open_input即使成功调用,也不一定能获取到文件头信息,因为可能有的媒体格式没有文件头?哈哈,所以一般继续调用avformat_find_stream_info可以获取正确的信息
- 读取数据包:确认有音视频数据之后,可以通过av_read_frame把数据流读取到AVPacket中
// 此处主要涉及解码过程,可以略过,知道解码过程也需要传递formatContext信息即可
AVPacket packet;
while (av_read_frame(formatContext, &packet) == 0) {
// 处理 packet 中的数据
// 在使用完 packet 后释放引用
av_packet_unref(&packet);
}
- 关闭输入
avformat_close_input(&formatContext);
avformat_close_input会关闭输入流,同时释放AVFormatContext结构体
输出过程
上面介绍的主要输入过程中使用AVFormatContext的基本方式,那么输出过程是否一致呢?函数调用上略有区别。
- 创建输出音视频格式的AVFormatContext
// 通常在你需要进行音视频编码并生成一个新的音视频文件时使用
AVFormatContext *output_format_context = NULL;
//
avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
- 写入数据
//先写入头文件
ret = avformat_write_header(output_format_context, &opts);
//再写入帧数据
ret = av_interleaved_write_frame(output_format_context, &packet);
// 写入收尾(同时刷新缓冲区)
av_write_trailer(output_format_context);
- 释放avformatcontext结构体
avformat_free_context(output_format_context);
AVStream
AVStream是AVFormatContext结构体中的一个成员(数组结构),它表示媒体文件中某一种数据的流以及对应的媒体信息,比如该流表示视频流,则同时也会含有视频相关的宽高,帧率等信息,以及time_base等基础信息。
typedef struct AVStream {
int index; /**< stream index in AVFormatContext */
// stream ID
int id;
// 与流关联的编解码器的参数结构
AVCodecParameters *codecpar;
//time_base AVRational结构体有两个成员,组成一个分数(有理数)
AVRational time_base;
...
...
int64_t duration;
int64_t nb_frames; ///< number of frames in this stream if known or 0
...
...
/**
* sample aspect ratio (0 if unknown)
* - encoding: Set by user.
* - decoding: Set by libavformat.
*/
AVRational sample_aspect_ratio;
...
...
} AVStream;
对于初学者而言,可以先重点关注time_base和codecpar这两个成员,time_base不用讲,是ffmpeg中的时间基本单位,codecpar则表示了当前流的解码信息。
我们可以看一下AVCodecParameters这个结构体的成员情况
typedef struct AVCodecParameters {
/**
* General type of the encoded data.
*/
enum AVMediaType codec_type;
/**
* Specific type of the encoded data (the codec used).
*/
enum AVCodecID codec_id;
...
/**
* - video: the pixel format, the value corresponds to enum AVPixelFormat.
* - audio: the sample format, the value corresponds to enum AVSampleFormat.
*/
int format;
...
...
/**
* 视频帧相关的一些参数
* Video only. The dimensions of the video frame in pixels.
*/
int width;
int height;
AVRational sample_aspect_ratio;
enum AVColorRange color_range;
enum AVColorPrimaries color_primaries;
enum AVColorTransferCharacteristic color_trc;
enum AVColorSpace color_space;
enum AVChromaLocation chroma_location;
/**
* Audio only. The number of audio samples per second.
*/
int sample_rate;
// Audio only. Audio frame size
int frame_size;
// 声道配置情况(音频)
AVChannelLayout ch_layout;
AVRational framerate;
} AVCodecParameters;
可以看到AVCodecParameters是把音频和视频这两种信息混合在一起了,在流属于不同类型时使用不同的字段,或者同一个字段表达不同的含义。比如format,如果是视频,则表示AVPixelFormat枚举类型,如果是音频,则表示AVSampleFormat枚举类型。
- 如何获取AVStream
// formatContext->nb_streams表示流的个数
int stream_size = formatContext->nb_streams;
for(int i=0;i<stream_size;i++){
//获取一个AVStream
AVStream *in_stream = formatContext->streams[i];
// 从AVStream中获取AVCodecParameters
AVCodecParameters *av_in_codec_param = in_stream->codecpar;
...
}
AVCodec
//编解码器
typedef struct AVCodec {
// 编解码器的名称
const char *name;
const char *long_name;
enum AVMediaType type; // 媒体类型(视频,音频,字幕等)
enum AVCodecID id; // 编解码器的ID
// 编解码器所支持的一些参数
const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
/**
* Array of supported channel layouts, terminated with a zeroed layout.
*/
const AVChannelLayout *ch_layouts;
} AVCodec;
AVCodec可以表示一个编解码器,里面包含了编解码的一些基本信息。
一般而言,媒体文件中的音频流,视频流中都保存有解码器ID等信息,通过这个ID可以获取对应AVCodec,从而获取该解码器的比较全面的信息。
- 获取AVCodec
// formatContext即 AVFormatContext的结构体对象,此时应该已经创建并读取了信息
// codecpar是AVStream中的结构体成员,表示该流数据对应的解码器信息
// 从流中找到对应编解码器信息和id
enum AVCodecID id = formatContext->streams[videoStreamIndex]->codecpar->codec_id
// 通过ID找到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);
获取到AVCodec之后,需要通过它构建一个可用编解码器上下文(提供编解码过程中待解码数据的背景和配置)
AVCodecContext
typedef struct AVCodecContext {
enum AVMediaType codec_type;// 数据类型(音频、视频、字幕、等)
const struct AVCodec *codec; // 对应的编解码器
enum AVCodecID codec_id; //编解码器ID
// time_base,编码时必须设置
AVRational time_base;
/*视频使用*/
int width, height;
// 像素格式,告诉解码器你想要把数据解码成哪个像素格式,不设置的话ffmpeg会有默认值
enum AVPixelFormat pix_fmt;
/* audio only */
int sample_rate; ///< samples per second
...
...
enum AVSampleFormat sample_fmt; ///< 采样格式
// AVFrame中每个声道的采样数,音频时使用
int frame_size;
//也是time_base,解码时设置
AVRational pkt_timebase;
}
AVCodecContext就是我们前面说的编解码上下文,主要包含待解码数据的一些特性,便于在解码过程中解码器正确解析数据。比如等待解码的是视频数据,那么解码器需要知道time_base(关于time_base的概念不懂可以看前一篇文章)统一时间单位;每帧图片的宽高;视频帧的像素格式(关于像素格式见(移动开发中关于视频的一些基本概念),了解像素排列方式....
有了以上信息,解码器才可以正确的对数据进行解码。
AVCodecContext中音频的的frame_size,在解码器中可能不存在,因此在解码过程避免使用这个字段,可以找decoded_frame中的nb_samples来替代
- 创建(解码为例)
// id 是从前文中通过AVStream中获取的
// 获取到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);
// 创建AVCodecContext的结构,此时还没有对应的参数(都是默认参数)
AVCodecContext *pCodecCtx = avcodec_alloc_context3(av_codec);
// 从数据流AVStream中得到的AVCodecParameter中的相关信息复制到AVCodecContext
// 此时AVCodecContext就有了正确的信息了
if(avcodec_parameters_to_context(pCodecCtx,av_codec_parameters) < 0) {
fprintf(stderr, "Couldn't copy codec context");
return -1; // Error copying codec context
}
//初始化并启动解码器
if(avcodec_open2(pCodecCtx, pCodec, NULL)<0){
return -1; // Could not open codec
}
利用AVCodec构建AVCodecContext,然后把AVStream中已知的一些信息复制到AVCodecContext中,接着初始化并开启编解码器。
- 销毁
avcodec_free_context(&pCodecCtx)
AVCodecContext在编解码过程中都会被用到。
AVPacket
读取文件获取AVFormatContext结构体,并且获取了解码器上下文
AVPacket是存储压缩编码数据相关信息的结构体
typedef struct AVPacket {
int64_t pts; // 显示时间戳
int64_t dts; // 解码时间戳
uint8_t *data; // 压缩编码的数据
int size; // data的大小
int stream_index; // 当前packet所属的流(视频流或者音频流等)
...
...
...
AVRational time_base;
}
AVPacket的成员主要包括time_base,pts,dts等一些在解码时可能被用到的参数以及编码数据data。
创建过程
- 在正常的编解码过程中,AVPacket手动申请内存,则需要手动释放内存,如果自动申请内存则不需要。
- 从编解码器中接收数据放到packet中,使用完之后,需要释放引用av_packet_unref 即可
AVPacket pkt; // 自动申请出内存
// 此时只是申请了AVPacket结构体的内存空间,其所指向的数据内存区域还没有创建
AVPacket *pkt2 = av_packet_alloc(); // 如果手动申请内存,泽需要和后续av_packet_free释放
...
// do something with avpacket
...
// 在使用完 pkt 后释放内存
av_packet_free(&pkt2);
使用方式
// 从AVFormatContext中读取数据到avpacket中
// 创建avpacket指向的数据内存区域的函数是av_new_packet
if (av_read_frame(formatContext, &pkt) == 0) {
...
//解码器解码处理 pkt中的数据
...
//在使用完 pkt 后释放引用(引用数到0),从而释放其指向的数据内存区域
av_packet_unref(&pkt);
}
AVFrame
AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),除此之外就是数据对应的一些属性:时长,格式等
视频和音频共用一个结构体,因此有的属性是双方公用,有的可能主要用于一方
typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
// *data[]是一个成员为指针的数组
// 原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
uint8_t *data[AV_NUM_DATA_POINTERS];
// data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽
int linesize[AV_NUM_DATA_POINTERS];
uint8_t **extended_data;
int width, height;
/**
* number of audio samples (per channel) described by this frame
*/
// 音频类型中,AVFrame包含的多少个采样
int nb_samples;
// 音视频的格式,
int format;
//帧类型,I帧,P帧,B帧等
enum AVPictureType pict_type;
...
/**
* Presentation timestamp in time_base units (time when frame should be shown to user).
*/
int64_t pts;
/**
* DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used)
* This is also the Presentation time of this AVFrame calculated from
* only AVPacket.dts values without pts values.
*/
int64_t pkt_dts;
AVRational time_base;
int sample_rate;
AVChannelLayout ch_layout;
int64_t duration;
} AVFrame;
data与linesize
关于data和linesize这两个字段,分别表示原始数据存储数组和每一行的大小。但是数据是如何排列的我们并不清楚。
之前讲视频的基础知识时,我们讲到YUV的数据排列有多种方式,因此想要知道data中的YUV数据排列,我们还需要知道AVFrame的format,这个format来自于AVCodecContext->pix_fmt,这个解码器的参数设置成什么,最终解码出来的杨素格式就是什么。假如不指定的话,默认会解码为YUV420p。
我们假设视频数据解码出来的AVFrame,format是YUV420P,那么data和linesize的数据在ffmpeg中的内存示意图可能是这样的:
image.png类似的音频数据解码出来的AVFrame,format是AV_SAMPLE_FMT_FLTP,双声道,那么对应的数据在ffmpeg中的内存示意图可能是这样的:
image.png以上都是planar的存储模式,如果是packed(关于planar/packed的解释见文章)存储模式呢?
YUV422 packed存储格式的视频,ffmpeg中的内存示意图大概是这样的:
image.png当然,其实对于初学者而言,一般不需要直接操作data和linesize,但是能够把ffmpeg中的数据结构和所学的音视频知识做一个对应理解会更深刻。
对于linsize,音频类型。一般只有linesize[0]会被设置;视频则需要看存储方式的不同,常用的planar模式下,linsize数组一般会用到前三个。
对于data指针数组而言,音频数据占用数组几个的指针要看声道数和存储格式(palnar/packed);视频则只看存储格式,planar一般占3个,packed占
使用方式
- 创建,申请内存空间
// 申请内存空间
AvFrame *pFrame = av_frame_alloc();
- 解码
解码就是AVPakcet=>AVFrame的过程。
/********一次循环************/
// 从输入文件的流中读取数据到packet中
av_read_frame(pFormatCtx, &packet)
// 把AVPacket中的数据发送到解码器
avcodec_send_packet(pCodecCtx,&packet);
// 从解码器中读取数据到AVFrame中
avcodec_receive_frame(pCodecCtx,pFrame);
- 编码
编码则是AVFrame=>AVPacket的过程(解码的逆过程)。
// av_encode_ctx 编码器的上下文
// pFrame 已获得的原始数据帧
int ret = avcodec_send_frame(av_encode_ctx,pFrame); // 把原始数据发送到编码器
// 从编码器中读取编码后的数据到av_out_packet
ret = avcodec_receive_packet(av_encode_ctx,av_out_packet);
- 销毁
// 使用完之后
av_frame_free(&pFrame);
手动填充AVFrame->data
ffmpeg中,我们是通过av_frame_alloc函数来获得AVFrame,但是这个函数只是开辟了AVFrame结构体空间,而avframe->data是一个成员为指针的数组,这些成员指针和它们指向的内存空间并未被开辟出来。我们从源码实现中也能看到:
AVFrame *av_frame_alloc(void)
{
// 为AVFrame的结构体开辟空间
AVFrame *frame = av_malloc(sizeof(*frame));
if (!frame)
return NULL;
// 未某些成员赋默认值值(不包括data)
get_frame_defaults(frame);
return frame;
}
是因为在编解码过程中编解码器会帮助我们开辟这块空间,所以我们不必管。
但是假如我们在编解码之外使用AVFrame,比如把YUV类型的AVFrame转换为RGB类型的AVFrame,那么AVFrame->data的空间就需要我们自己开辟了,也需要我们进行释放。
- 填充AVFrame->data
以视频帧为例
// Allocate an AVFrame structure
pFrameRGB=av_frame_alloc();
// 通过宽高以及像素格式来计算获得新的帧所需要的缓冲区大小
numBytes= av_image_get_buffer_size(AV_PIX_FMT_RGB24, width,height,1);
// 假设 buffer = 1024byte 表示buffer是指向一个1024个uint_8数据的内存区域的指针
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
// 让pFrameRGB->data数组中几个指针分别指向buffer这块空间(不同位置),
// 然后可以向这块空间填充数据
av_image_fill_arrays(pFrameRGB->data,pFrameRGB->linesize, buffer,AV_PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height,1);
总结
本文主要详细介绍了ffmpeg中比较重要的几个结构体,他们都伴随着音视频处理的某个阶段而存在的,因此了解他们有助于我们理解音视频的处理流程。
image.png我们把前面的音视频解码播放流程图添加关键API和搭配关键结构体就会发现ffmpeg的处理流程还是比较简洁的。接下来我们尝试用一个较完整的demo来熟悉ffmpeg的使用方式。
网友评论