前言
AVFormatContext 是一个贯穿始终的数据结构,很多函数都用到它作为参数,是输入输出相关信息的一个容器,本文讲解 AVFormatContext 的封装层,主要包括两大数据结构:AVInputFormat,AVOutputFormat。
一、封装格式简介
封装格式(container format)可以看作是编码流(音频流、视频流等)数据的一层外壳,将编码后的数据存储于此封装格式的文件之内。
封装又称容器,容器的称法更为形象,所谓容器,就是存放内容的器具,例如饮料是内容,那么装饮料的瓶子就是容器。
不同封装格式适用于不同的场合,支持的编码格式不一样,几个常用的封装格式如下:
1、FFmpeg 中的封装格式
FFmpeg 关于封装格式的处理涉及打开输入文件、打开输出文件、从输入文件读取编码帧、往输出文件写入编码帧这几个步骤,这些都不涉及编码解码层面。
在 FFmpeg 中,mux 指复用,是 multiplex 的缩写,表示将多路流(视频、音频、字幕等)混入一路输出中(普通文件、流等)。demux 指解复用,是 mux 的反操作,表示从一路输入中分离出多路流(视频、音频、字幕等)。
mux 处理的是输入格式,demux 处理的输出格式。输入/输出媒体格式涉及文件格式和封装格式两个概念【封装格式】 。
- 文件格式由文件扩展名标识【文件格式】,主要起提示作用,通过扩展名提示文件类型(或封装格式)信息。封装格式则是存储媒体内容的实际容器格式,不同的封装格式对应不同的文件扩展名,很多时候也用文件格式代指封装格式,例如常用 ts 格式(文件格式)代指 mpegts 格式(封装格式)。
- 例如,我们把 test.ts 改名为 test.mkv,mkv 扩展名提示了此文件封装格式为 Matroska,但文件内容并无任何变化,使用 ffprobe 工具仍能正确探测出封装格式为 mpegts。
2、查看 FFmpeg 支持的封装格式
使用 ffmpeg -formats 命令可以查看 FFmpeg 支持的封装格式。 FFmpeg 支持的封装非常多, 下面仅列出最常用的几种:
- h264/aac 裸流封装格式
- h264 裸流封装格式和 aac 裸流封装格式在后面的解复用和复用例程中会用到,这里先讨论一下。h264 本来是编码格式,当作封装格式时表示的是 H.264 裸流格式,所谓裸流就是不含封装信息的流,也就是没穿衣服的流。aac 等封装格式类似。
看一下 FFmpeg 工程源码中 h264 编码格式以及 h264 封装格式的定义:FFmpeg 工程包含 h264 解码器,而不包含 h264 编码器(一般使用第三方 libx264 编码器用作 h264 编码),所以只有解码器定义:
AVCodec ff_h264_decoder = { .name = "h264", .long_name = NULL_IF_CONFIG_SMALL("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10"), .type = AVMEDIA_TYPE_VIDEO, .id = AV_CODEC_ID_H264, ...... };
h264 封装格式定义如下:
AVOutputFormat ff_h264_muxer = { .name = "h264", .long_name = NULL_IF_CONFIG_SMALL("raw H.264 video"), .extensions = "h264,264", .audio_codec = AV_CODEC_ID_NONE, .video_codec = AV_CODEC_ID_H264, .write_header = force_one_stream, .write_packet = ff_raw_write_packet, .check_bitstream = h264_check_bitstream, .flags = AVFMT_NOTIMESTAMPS, }; AVOutputFormat ff_h264_muxer = { .name = "h264", .long_name = NULL_IF_CONFIG_SMALL("raw H.264 video"), .extensions = "h264,264", .audio_codec = AV_CODEC_ID_NONE, .video_codec = AV_CODEC_ID_H264, .write_header = force_one_stream, .write_packet = ff_raw_write_packet, .check_bitstream = h264_check_bitstream, .flags = AVFMT_NOTIMESTAMPS, };
二、API 介绍
FFmpeg 中将编码帧及未编码帧均称作 frame,本文为方便,将编码帧称作 packet,未编码帧称作 frame。
未压缩的,未编码的:原始图片(yuv,rgb)或声音(pcm 流)。
压缩的, 编码的:H264/265,aac/ac3/mp3
最主要的 API 有如下几个:
avformat_open_input()
:这个函数会打开输入媒体文件,读取文件头,将文件格式信息存储在第一个参数AVFormatContext
中。avformat_find_stream_info()
:这个函数会读取一段视频文件数据并尝试解码,将取到的流信息填入AVFormatContext.streams
中。AVFormatContext.streams
是一个指针数组,数组大小是AVFormatContext.nb_streams
。av_read_frame()
:本函数用于解复用过程。本函数将存储在输入文件中的数据分割为多个 packet, 每次调用将得到一个 packet。 packet 可能是视频帧、音频帧或其他数据,解码器只会解码视频帧或音频帧,非音视频数据并不会被扔掉、从而能向解码器提供尽可能多的信息。
- 对于视频来说,一个 packet 只包含一个视频帧;
- 对于音频来说,若是帧长固定的格式则一个 packet 可包含整数个音频帧,若是帧长可变的格式则一个 packet 只包含一个音频帧。
- 读取到的 packet 每次使用完之后应调用
av_packet_unref(AVPacket *pkt)
清空 packet。否则会造成内存泄露。
av_write_frame()
:本函数用于复用过程,将 packet 写入输出媒体。
- packet 交织是指:不同流的 packet 在输出媒体文件中应严格按照 packet 中 dts 递增的顺序交错存放。
- 本函数直接将 packet 写入复用器(muxer),不会缓存或记录任何 packet。本函数不负责不同流的 packet 交织问题。,由调用者负责。
- 如果调用者不愿处理 packet 交织问题,应调用
av_interleaved_write_frame()
替代本函数。
av_interleaved_write_frame()
:本函数用于复用过程, 将 packet 写入输出媒体。
- 本函数将按需在内部缓存 packet,从而确保输出媒体中不同流的 packet 能按照 dts 增长的顺序正确交织。
avio_open()
:创建并初始化一个AVIOContext
,用于访问输出媒体文件。avformat_write_header()
:向输出文件写入文件头信息。av_write_trailer()
:向输出文件写入文件尾信息。
三、 实战 1:解封装
1、原理讲解
本例子实现的是将音视频分离,例如将封装格式为 FLV、MKV、MP4、AVI 等封装格式的文件,将音频、视频读取出来并打印。实现的过程,可以大致用如下图表示:
2、示例源码 1
兼容旧版本使用遍历的方式查找给定媒体文件中音频流或视频流,未使用新版本的 FFmpeg 新增加的函数 av_find_best_stream()
#include <stdio.h> extern "C" { #include <libavformat/avformat.h> } /** * @brief 将一个AVRational类型的分数转换为double类型的浮点数 * @param r:r为一个AVRational类型的结构体变量,成员num表示分子,成员den表示分母,r的值即为(double)r.num / (double)r.den。用这种方法表示可以最大程度地避免精度的损失 * @return 如果变量r的分母den为0,则返回0(为了避免除数为0导致程序死掉);其余情况返回(double)r.num / (double)r.den */ static double r2d(AVRational r) { return r.den == 0 ? 0 : (double)r.num / (double)r.den; } int main() { //需要读取的本地媒体文件相对路径为 ./debug/test.mp4 const char *path = "./debug/test.mp4"; ///av_register_all(); //初始化所有组件,只有调用了该函数,才能使用复用器和编解码器。否则,调用函数avformat_open_input会失败,无法获取媒体文件的信息 avformat_network_init(); //打开网络流。这里如果只需要读取本地媒体文件,不需要用到网络功能,可以不用加上这一句 AVDictionary *opts = NULL; //AVFormatContext是描述一个媒体文件或媒体流的构成和基本信息的结构体 AVFormatContext *ic = NULL; //媒体打开函数,调用该函数可以获得路径为path的媒体文件的信息,并把这些信息保存到指针ic指向的空间中(调用该函数后会分配一个空间,让指针ic指向该空间) int re = avformat_open_input(&ic, path, NULL, &opts); if (re != 0) //如果打开媒体文件失败,打印失败原因。比如,如果上面没有调用函数av_register_all,则会打印“XXX failed!:Invaliddata found when processing input” { char buf[1024] = { 0 }; av_strerror(re, buf, sizeof(buf) - 1); printf("open %s failed!:%s", path, buf); } else //打开媒体文件成功 { printf("打开媒体文件 %s 成功!\n", path); //调用该函数可以进一步读取一部分视音频数据并且获得一些相关的信息。 //调用avformat_open_input之后,我们无法获取到正确和所有的媒体参数,所以还得要调用avformat_find_stream_info进一步的去获取。 avformat_find_stream_info(ic, NULL); //调用avformat_open_input读取到的媒体文件的路径/名字 printf("媒体文件名称:%s\n", ic->filename); //视音频流的个数,如果一个媒体文件既有音频,又有视频,则nb_streams的值为2。如果媒体文件只有音频,则值为1 printf("视音频流的个数:%d\n", ic->nb_streams); //媒体文件的平均码率,单位为bps printf("媒体文件的平均码率:%lldbps\n", ic->bit_rate); printf("duration:%d\n", ic->duration); int tns, thh, tmm, tss; tns = (ic->duration) / AV_TIME_BASE; thh = tns / 3600; tmm = (tns % 3600) / 60; tss = (tns % 60); printf("媒体文件总时长:%d时%d分%d秒\n", thh, tmm, tss); //通过上述运算,可以得到媒体文件的总时长 printf("\n"); //通过遍历的方式读取媒体文件视频和音频的信息, //新版本的FFmpeg新增加了函数av_find_best_stream,也可以取得同样的效果,但这里为了兼容旧版本还是用这种遍历的方式 for (int i = 0; i < ic->nb_streams; i++) { AVStream *as = ic->streams[i]; if (AVMEDIA_TYPE_AUDIO == as->codecpar->codec_type) //如果是音频流,则打印音频的信息 { printf("音频信息:\n"); printf("index:%d\n", as->index); //如果一个媒体文件既有音频,又有视频,则音频index的值一般为1。但该值不一定准确,所以还是得通过as->codecpar->codec_type判断是视频还是音频 printf("音频采样率:%dHz\n", as->codecpar->sample_rate); //音频编解码器的采样率,单位为Hz if (AV_SAMPLE_FMT_FLTP == as->codecpar->format) //音频采样格式 { printf("音频采样格式:AV_SAMPLE_FMT_FLTP\n"); } else if (AV_SAMPLE_FMT_S16P == as->codecpar->format) { printf("音频采样格式:AV_SAMPLE_FMT_S16P\n"); } printf("音频信道数目:%d\n", as->codecpar->channels); //音频信道数目 if (AV_CODEC_ID_AAC == as->codecpar->codec_id) //音频压缩编码格式 { printf("音频压缩编码格式:AAC\n"); } else if (AV_CODEC_ID_MP3 == as->codecpar->codec_id) { printf("音频压缩编码格式:MP3\n"); } int DurationAudio = (as->duration) * r2d(as->time_base); //音频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的 printf("音频总时长:%d时%d分%d秒\n", DurationAudio / 3600, (DurationAudio % 3600) / 60, (DurationAudio % 60)); //将音频总时长转换为时分秒的格式打印到控制台上 printf("\n"); } else if (AVMEDIA_TYPE_VIDEO == as->codecpar->codec_type) //如果是视频流,则打印视频的信息 { printf("视频信息:\n"); printf("index:%d\n", as->index); //如果一个媒体文件既有音频,又有视频,则视频index的值一般为0。但该值不一定准确,所以还是得通过as->codecpar->codec_type判断是视频还是音频 printf("视频帧率:%lffps\n", r2d(as->avg_frame_rate)); //视频帧率,单位为fps,表示每秒出现多少帧 if (AV_CODEC_ID_MPEG4 == as->codecpar->codec_id) //视频压缩编码格式 { printf("视频压缩编码格式:MPEG4\n"); } printf("帧宽度:%d 帧高度:%d\n", as->codecpar->width, as->codecpar->height); //视频帧宽度和帧高度 int DurationVideo = (as->duration) * r2d(as->time_base); //视频总时长,单位为秒。注意如果把单位放大为毫秒或者微妙,音频总时长跟视频总时长不一定相等的 printf("视频总时长:%d时%d分%d秒\n", DurationVideo / 3600, (DurationVideo % 3600) / 60, (DurationVideo % 60)); //将视频总时长转换为时分秒的格式打印到控制台上 printf("\n"); } } //av_dump_format(ic, 0, path, 0); } if (ic) { avformat_close_input(&ic); //关闭一个AVFormatContext,和函数avformat_open_input()成对使用 } avformat_network_deinit(); return 0; }
3、运行结果 1
打开媒体文件 ./debug/test.mp4 成功! 媒体文件名称:./debug/test.mp4 视音频流的个数:2 媒体文件的平均码率:1436830bps duration:117312000 媒体文件总时长:0时1分57秒 视频信息: index:0 视频帧率:25.000000fps 帧宽度:1280 帧高度:720 视频总时长:0时1分57秒 音频信息: index:1 音频采样率:48000Hz 音频采样格式:AV_SAMPLE_FMT_FLTP 音频信道数目:6 音频压缩编码格式:AAC 音频总时长:0时1分57秒
使用 MediaInfo 打开分析可以看到与上面的打印信息是对应上的
AVFormatContext封装层:理论与实战(二)https://developer.aliyun.com/article/1473935