释放双眼,带上耳机,听听看~!
简介
开发环境
FFmpeg sdk下载
项目配置
代码流程
开发环境
vs 2017
FFmpeg sdk下载
下载地址
这里下载3.3.3 — 32bit — share和Dev
8.png
Shared包含运行时的动态库在bin目录下
Dev包含开发是编译需要的头文件(include目录下)和库文件(lib目录下)
项目配置
先看下项目的目录结构
9.png
这里的bin、include、lib就是我们刚才在FFmpeg下载的相关文件。
src是我们的项目源码目录。
新建Win32控制台应用程序、选择位置、项目名称。注意:去掉“为结局方案创建目录”的勾选
10.png
然后选择空项目、去掉预编译头。完成项目的创建
11.png
项目属性配置
右击项目属性
【常规】=>【输出目录】 修改为....bin
【调试】=>【工作目录】修改为....bin
【C/C++】=>【常规】=>【附加包含目录】修改为....include
-【链接器】=>【常规】=>【附加库目录】修改为....lib
注意:这里所有的路径都是相对路径,相对于源码的路径
这里设置输出目录到bin。是因为win下运行时会默认在当前运行的目录下寻找dll文件。而我们的dll文件放在bin目录下。
开发流程
Flow Chart.png
流程详解
av_register_all()
该方法初始化所有的封装和解封装。在使用FFmpeg的时候首先要调用这个方法。
找到这个方法的源码libavformatallformats.c
void av_register_all(void)
{
static AVOnce control = AV_ONCE_INIT;
ff_thread_once(&control, register_all);
}
这里控制注册方法只被调用一次register_all是函数指针。看到实现部分。
static void register_all(void)
{
avcodec_register_all();
/* (de)muxers */
REGISTER_MUXER (A64, a64);
REGISTER_DEMUXER (AA, aa);
REGISTER_DEMUXER (AAC, aac);
//...
}
这里面就是进行各种注册,而REGISTER_MUXER 、REGISTER_DEMUXER 是前面定义的宏。我们看到是静态方法,说明该方法只能在所在的文件中使用,这也防止被注册多次。
#define REGISTER_MUXER(X, x)
{
extern AVOutputFormat ff_##x##_muxer;
if (CONFIG_##X##_MUXER)
av_register_output_format(&ff_##x##_muxer);
}
#define REGISTER_DEMUXER(X, x)
{
extern AVInputFormat ff_##x##_demuxer;
if (CONFIG_##X##_DEMUXER)
av_register_input_format(&ff_##x##_demuxer);
}
#define REGISTER_MUXDEMUX(X, x) REGISTER_MUXER(X, x); REGISTER_DEMUXER(X, x)
av_register_input_format和av_register_output_format我们也可以单独去初始化。这里内部细节就不做过多介绍。
avformat_network_init()
网络相关初始化。如果我们使用了网络拉流和推流等等,要先初始化。
avformat_open_input()
声明是
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
定义在libavformatutils.c中。主要功能
输入输出结构体AVIOContext的初始化;
输入数据的协议URLProtocol,通过函数指针的方式,与FFMPEG关联,剩下的就是调用该URLProtocol的函数进行open,read等操作了
avformat_find_stream_info
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
可以读取视音频数据并且获得一些相关的信息。定义在libavformatutils.c下
avformat_alloc_output_context2
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat,
const char *format_name, const char *filename);
定义在libavformatmux.c中
ctx:函数调用成功之后创建的AVFormatContext结构体。
oformat:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。
PS:使用该参数需要自己手动获取AVOutputFormat,相对于使用后两个参数来说要麻烦一些。
format_name:指定输出格式的名称。根据格式名称,FFmpeg会推测输出格式。输出格式可以是“flv”,“mkv”等等。
filename:指定输出文件的名称。根据文件名称,FFmpeg会推测输出格式。文件名称可以是“xx.flv”,“yy.mkv”等等。
函数执行成功的话,其返回值大于等于0。
内部流程
调用avformat_alloc_context()初始化一个默认的AVFormatContext。
如果指定了输入的AVOutputFormat,则直接将输入的AVOutputFormat赋值给AVOutputFormat的oformat。如果没有指定输入的AVOutputFormat,就需要根据文件格式名称或者文件名推测输出的AVOutputFormat。无论是通过文件格式名称还是文件名推测输出格式,都会调用一个函数av_guess_format()。
avio_open
打开FFmpeg的输入输出文件
int avio_open2(AVIOContext **s, const char *url, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options);
s:函数调用成功之后创建的AVIOContext结构体。
url:输入输出协议的地址(文件也是一种“广义”的协议,对于文件来说就是文件的路径)。
flags:打开地址的方式。可以选择只读,只写,或者读写。取值如下。
AVIO_FLAG_READ:只读。
AVIO_FLAG_WRITE:只写。
AVIO_FLAG_READ_WRITE:读写。
int_cb:不太清楚
options:不太清楚
avformat_write_header
写视频文件头,av_write_trailer()用于写视频文件尾
av_read_frame
定义在libavformatutils.c中
读取码流中的音频若干帧或者视频一帧。解码视频的时候,每解码一个视频帧,需要先调用 av_read_frame()获得一帧视频的压缩数据,然后才能对该数据进行解码(例如H.264中一帧压缩数据通常对应一个NAL)。
这里我贴上官方的注释,很详细:
/**
* Return the next frame of a stream.
* This function returns what is stored in the file, and does not validate
* that what is there are valid frames for the decoder. It will split what is
* stored in the file into frames and return one for each call. It will not
* omit invalid data between valid frames so as to give the decoder the maximum
* information possible for decoding.
*
* If pkt->buf is NULL, then the packet is valid until the next
* av_read_frame() or until avformat_close_input(). Otherwise the packet
* is valid indefinitely. In both cases the packet must be freed with
* av_packet_unref when it is no longer needed. For video, the packet contains
* exactly one frame. For audio, it contains an integer number of frames if each
* frame has a known fixed size (e.g. PCM or ADPCM data). If the audio frames
* have a variable size (e.g. MPEG audio), then it contains one frame.
*
* pkt->pts, pkt->dts and pkt->duration are always set to correct
* values in AVStream.time_base units (and guessed if the format cannot
* provide them). pkt->pts can be AV_NOPTS_VALUE if the video format
* has B-frames, so it is better to rely on pkt->dts if you do not
* decompress the payload.
*
* &#64;return 0 if OK, <0 on error or end of file
*/
总结起来每段的核心意思
读取码流中的音频若干帧或者视频一帧
如果pkt->buf是空&#xff0c;那么就要等待下一次av_read_frame调用。否则无法确定是否有效
pts dts duration通常被设置为正确的值。但如果视频帧包括Bzh帧&#xff0c;那么pts可以是AV_NOPTS_VALUE。所以最好依赖dts。
av_interleaved_write_frame
输出一帧视音频数据
核心类
AVFormatContext
AVFormatContext是一个贯穿始终的数据结构&#xff0c;很多函数都要用到它作为参数。它是FFMPEG解封装(flv&#xff0c;mp4&#xff0c;rmvb&#xff0c;avi)功能的结构体。
内部的成员变量&#xff0c;大家可以查看头文件。这里我们列举下一些常用重要的成员变量&#xff1a;
struct AVInputFormat *iformat&#xff1a;输入数据的封装格式
AVIOContext *pb&#xff1a;输入数据的缓存
unsigned int nb_streams&#xff1a;视音频流的个数
AVStream **streams&#xff1a;视音频流
char filename[1024]&#xff1a;文件名
int64_t duration&#xff1a;时长(单位&#xff1a;微秒us&#xff0c;转换为秒需要除以1000000)
int bit_rate&#xff1a;比特率(单位bps&#xff0c;转换为kbps需要除以1000)
AVDictionary *metadata&#xff1a;元数据
视频的原数据(metadata)信息可以通过AVDictionary获取。元数据存储在AVDictionaryEntry结构体中
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
每一条元数据分为key和value两个属性。
在ffmpeg中通过av_dict_get()函数获得视频的原数据。
cout < string meta, key, value; AVDictionaryEntry *m &#61; NULL; while (m &#61; av_dict_get(ictx->metadata, "", m, AV_DICT_IGNORE_SUFFIX)) { key&#61;m->key; value&#61;m->value; meta.append(key).append("t:").append(value).append("rn"); } cout < AVStream AVStream是存储每一个视频/音频流信息的结构体。 int index&#xff1a;标识该视频/音频流 AVCodecContext *codec&#xff1a;指向该视频/音频流的AVCodecContext(它们是一一对应的关系) AVRational time_base&#xff1a;时基。通过该值可以把PTS&#xff0c;DTS转化为真正的时间。- FFMPEG其他结构体中也有这个字段&#xff0c;但是根据我的经验&#xff0c;只有AVStream中的time_base是可用的。PTS*time_base&#61;真正的时间 int64_t duration&#xff1a;该视频/音频流长度 AVDictionary *metadata&#xff1a;元数据信息 AVRational avg_frame_rate&#xff1a;帧率(注&#xff1a;对视频来说&#xff0c;这个挺重要的) AVPacket attached_pic&#xff1a;附带的图片。比如说一些MP3&#xff0c;AAC音频文件附带的专辑封面。 AVPacket AVPacket是存储压缩编码数据相关信息的结构体。 uint8_t *data&#xff1a;压缩编码的数据。 例如对于H.264来说。1个AVPacket的data通常对应一个NAL。 注意&#xff1a;在这里只是对应&#xff0c;而不是一模一样。他们之间有微小的差别&#xff1a;使用FFMPEG类库分离出多媒体文件中的H.264码流 因此在使用FFMPEG进行视音频处理的时候&#xff0c;常常可以将得到的AVPacket的data数据直接写成文件&#xff0c;从而得到视音频的码流文件。 int size&#xff1a;data的大小 int64_t pts&#xff1a;显示时间戳 int64_t dts&#xff1a;解码时间戳 int stream_index&#xff1a;标识该AVPacket所属的视频/音频流。 源码 #include using namespace std; //引入头文件 extern "C" { #include "libavformat/avformat.h" //引入时间 #include "libavutil/time.h" } //引入库 #pragma comment(lib,"avformat.lib") //工具库&#xff0c;包括获取错误信息等 #pragma comment(lib,"avutil.lib") //编解码的库 #pragma comment(lib,"avcodec.lib") int avError(int errNum); static double r2d(AVRational r) { return r.num &#61;&#61; 0 || r.den &#61;&#61; 0 ? 0. : (double)r.num / (double)r.den; } int main() { //所有代码执行之前要调用av_register_all和avformat_network_init //初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码 av_register_all(); //初始化网络库 avformat_network_init(); //使用的相对路径&#xff0c;执行文件在bin目录下。test.mp4放到bin目录下即可 const char *inUrl &#61; "test.flv"; //输出的地址 const char *outUrl &#61; "rtmp://192.168.136.131/live/test"; // // 输入流处理部分 / //打开文件&#xff0c;解封装 avformat_open_input //AVFormatContext **ps 输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO&#xff0c;网络就对应网络IO //const char *url 路径 //AVInputFormt * fmt 封装器 //AVDictionary ** options 参数设置 AVFormatContext *ictx &#61; NULL; //打开文件&#xff0c;解封文件头 int ret &#61; avformat_open_input(&ictx, inUrl, 0, NULL); if (ret <0) { return avError(ret); } cout <<"avformat_open_input success!" < //获取音频视频的信息 .h264 flv 没有头信息 ret &#61; avformat_find_stream_info(ictx, 0); if (ret !&#61; 0) { return avError(ret); } //打印视频视频信息 //0打印所有 inUrl 打印时候显示&#xff0c; av_dump_format(ictx, 0, inUrl, 0); // // 输出流处理部分 / AVFormatContext * octx &#61; NULL; //如果是输入文件 flv可以不传&#xff0c;可以从文件中判断。如果是流则必须传 //创建输出上下文 ret &#61; avformat_alloc_output_context2(&octx, NULL, "flv", outUrl); if (ret <0) { return avError(ret); } cout <<"avformat_alloc_output_context2 success!" < //配置输出流 //AVIOcontext *pb //IO上下文 //AVStream **streams 指针数组&#xff0c;存放多个输出流 视频音频字幕流 //int nb_streams; //duration ,bit_rate //AVStream //AVRational time_base //AVCodecParameters *codecpar 音视频参数 //AVCodecContext *codec //遍历输入的AVStream for (int i &#61; 0; i //创建一个新的流到octx中 AVStream *out &#61; avformat_new_stream(octx, ictx->streams[i]->codec->codec); if (!out) { return avError(0); } //复制配置信息 用于mp4 过时的方法 //ret&#61;avcodec_copy_context(out->codec, ictx->streams[i]->codec); ret &#61; avcodec_parameters_copy(out->codecpar, ictx->streams[i]->codecpar); if (ret <0) { return avError(ret); } out->codec->codec_tag &#61; 0; } av_dump_format(octx, 0, outUrl, 1); // // 准备推流 / //打开IO ret &#61; avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE); if (ret <0) { avError(ret); } //写入头部信息 ret &#61; avformat_write_header(octx, 0); if (ret <0) { avError(ret); } cout <<"avformat_write_header Success!" < //推流每一帧数据 //int64_t pts [ pts*(num/den) 第几秒显示] //int64_t dts 解码时间 [P帧(相对于上一帧的变化) I帧(关键帧&#xff0c;完整的数据) B帧(上一帧和下一帧的变化)] 有了B帧压缩率更高。 //uint8_t *data //int size //int stream_index //int flag AVPacket avPacket; //获取当前的时间戳 微妙 long long startTime &#61; av_gettime(); while (true) { ret &#61; av_read_frame(ictx, &avPacket); if (ret <0) { break; } cout < //计算转换时间戳 pts dts //获取时间基数 AVRational itime &#61; ictx->streams[avPacket.stream_index]->time_base; AVRational otime &#61; octx->streams[avPacket.stream_index]->time_base; avPacket.pts &#61; av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF)); avPacket.dts &#61; av_rescale_q_rnd(avPacket.pts, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF)); //到这一帧时候经历了多长时间 avPacket.duration &#61; av_rescale_q_rnd(avPacket.duration, itime, otime, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_NEAR_INF)); avPacket.pos &#61; -1; //视频帧推送速度 if (ictx->streams[avPacket.stream_index]->codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) { AVRational tb &#61; ictx->streams[avPacket.stream_index]->time_base; //已经过去的时间 long long now &#61; av_gettime() - startTime; long long dts &#61; 0; dts &#61; avPacket.dts * (1000 * 1000 * r2d(tb)); if (dts > now) av_usleep(dts - now); else { cout <<"sss"; } } //推送 会自动释放空间 不需要调用av_packet_unref ret &#61; av_interleaved_write_frame(octx, &avPacket); if (ret <0) { break; } //视频帧推送速度 //if (avPacket.stream_index &#61;&#61; 0) // av_usleep(30 * 1000); //释放空间。内部指向的视频空间和音频空间 //av_packet_unref(&avPacket); } return 0; } int avError(int errNum) { char buf[1024]; //获取错误信息 av_strerror(errNum, buf, sizeof(buf)); cout <<" failed! " < return -1; } 彩蛋 上面的代码在推送flv格式文件时候可能没问题&#xff0c;当换成mp4或者rmvb时候可能出现各种问题。如果你是在无法解开这个问题&#xff0c;请看下节基于FFmpeg进行RTMP推流(二)