简介
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。
FFmpeg基本概念
1 、容器
容器就是一种文件格式,视频文件本身是一个容器(container),里面包括了视频和音频,也可能有字幕等其他内容。常见的容器格式有以下几种:
MP4、MKV、WebM、AVI
下面的命令查看 FFmpeg 支持的容器。
ffmpeg -formats
2、 编码格式
视频和音频都需要经过编码才能保存成文件,因而就有了不同的编码格式(CODEC),对应着不同的压缩率
常用的视频编码格式:
H.264、H.265
常用的音频编码格式:
MP3、AAC
下面的命令可以查看 FFmpeg 支持的编码格式
ffmpeg -codecs
3 、流
流(Stream)是一种视频数据信息的传输方式,有5种流:音频,视频,字幕,附件,数据
4 、帧
帧(Frame)代表一幅静止的图像,分为I帧,P帧,B帧
5 、帧率
帧率也叫帧频率,帧率是视频文件中每一秒的帧数,肉眼想看到连续移动图像至少需要15帧
6 、码率
比特率,也叫码率、数据率,是一个确定整体视频/音频质量的参数,秒为单位处理的字节数,码率和视频质量成正比,在视频文件中中比特率用bps来表达设置帧率
FFmpeg组件
FFmpeg的组件包括libavcodec、libavutil、libavformat、libavfilter、libavdevice、libswscale和libswresample(这些都是可以应用与应用程序)
- libavutil是一个包含简化编程功能的库,包括随机数生成器、数学例程、核心多媒体使用程序等
- libavcodec是一个包含解码和编码器的音视频编解码器的库 libavformat是一个包含用于多媒体容器格式的demuxers和muxers的库
- libavdevice是一个包含输入和输出设备的库,用于抓取和呈现许多常见的多媒体输入/输出软件框架,包括Video4Linux、Video4Linux2、VFW和ALSA
- libavfilter是一个包含媒体过滤器的库
- libswscale是一个执行高度优化的音频重采样、rematrixing个实例格式转换操作的库
- libpostproc是一个用于后期效果处理的库
FFmpeg命令
三条最主要的命令:
- ffmpeg:由命令行组成,用于音视频转码
- ffplay:基于ffmpeg开源代码库libraries做的多媒体播放器
- ffprobe:基于ffmpeg做的多媒体流分析器,可查看多媒体文件的信息
FFmpeg语法
ffmpeg 的命令行参数非常多,输入 ffmpeg -h 查看支持的参数,具体可以分成五个部分
ffmpeg {1} {2} -i {3} {4} {5}
具体解释如下:
- 全局参数
- 输入文件参数
- 输入文件
- 输出文件参数
- 输出文件
为了便于查看,ffmpeg 命令可以写成多行
ffmpeg \
[全局参数] \
[输入文件参数] \
-i [输入文件] \
[输出文件参数] \
[输出文件]
FFmpeg解码流程
解码流程总览
解码流程分解
第一步:注册
使用FFmpeg对应的库,都需要进行注册,注册了这个才能正常使用编码器和解码器;
///第一步
av_register_all();
第二步:打开文件
打开文件,根据文件名信息获取对应的FFmpeg全局上下文
///第二步
AVFormatContext *pFormatCtx; //文件上下文,描述了一个媒体文件或媒体流的构成和基本信息
pFormatCtx = avformat_alloc_context(); //分配指针
if (avformat_open_input(&pFormatCtx, file_path, NULL, NULL) != 0) { //打开文件,信息存储到文件上下文中,后续对针对文件上下文即可
printf("无法打开文件");
return -1;
}
第三步:探测流信息
一定要探测流信息,拿到流编码的编码格式,不探测流信息则器流编码器拿到的编码类型可能为空,后续进行数据转换的时候就无法知晓原始格式,导致错误;
///第三步
//探寻文件中是否存在信息流
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
printf("文件中没有发现信息流");
return -1;
}
//探寻文件中是否存储视频流
int videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
}
}
//如果videoStream为-1 说明没有找到视频流
if (videoStream == -1) {
printf("文件中未发现视频流");
return -1;
}
//探寻文件中是否存在音频流
int audioStream = -1
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
audioStream = i;
}
}
//如果audioStream 为-1 说明没有找到音频流
if (audioStream == -1) {
printf("文件中未发现音频流");
return -1;
}
第四步:查找对应的解码器
依据流的格式查找解码器,软解码还是硬解码是在此处决定的,但是特别注意是否支持硬件,需要自己查找本地的硬件解码器对应的标识,并查询其是否支持。普遍操作是,枚举支持文件后缀解码的所有解码器进行查找,查找到了就是可以硬解了;
注意:解码时查找解码器,编码时查找编码器,两者函数不同,不要弄错了,否则后续能打开但是数据是错的;
///第四步
AVCodecContext *pCodecCtx; //描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息
AVCodec *pCodec; //存储编解码器信息的结构体
//查找解码器
pCodecCtx = pFormatCtx->streams[videoStream]->codec; //获取视频流中编码器上下文
pCodec = avcodec_find_decoder(pCodecCtx->codec_id); //获取视频流的编码器信息
if (pCodec == NULL) {
printf("未发现编码器");
return -1;
}
第五步:打开解码器
打开获取到的解码器
///第五步
//打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
printf("无法打开编码器");
return -1;
}
第六步:申请缩放数据格式转换结构体
基本上解码的数据都是yuv系列格式,但是我们显示的数据是rgb等相关颜色空间的数据,所以此处转换结构体就是进行转换前导转换后的描述,给后续转换函数提供转码依据,是很关键并且非常常用的结构体;
///第六步
static struct SwsContext *img_convert_ctx; //用于视频图像的转换
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
第七步:计算缩放颜色空间转换后缓存大小
///第七步
int numBytes; //字节数
numBytes = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width,pCodecCtx->height);
第八步:申请缓存区,将AVFrama的data映射到单独的outBuffer上
申请一个缓存区outBuffer,fill到我们目标帧数据的data上,比如rgb数据,QAVFrame的data上存的是有指定格式的数据且存储有规则,而fill到outBuffer(自己申请的目标格式一帧缓存区),则是我们需要的数据格式存储顺序;
例如:解码转换后的数据为rgb888,实际直接使用data数据是错误的,但是用outBuffer就是对的,所以此处应该是FFmpeg的fill函数做了一些转换;
///第七步
AVFrame *pFrame, *pFrameRGB; //存储音视频原始数据(即未被编码的数据)的结构体
pFrame = av_frame_alloc();
pFrameRGB = av_frame_alloc();
uint8_t *out_buffer; //缓存
out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
avpicture_fill((AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_BGR24,
pCodecCtx->width, pCodecCtx->height);
第九步:循环解码
1、获取一帧packet
int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据
int ret, got_picture;
while(1) {
if (av_read_frame(pFormatCtx, packet) < 0) { //读取一帧packet数据包
break; //这里认为视频读取完了
}
......
}
2、解码获取原始数据
int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据
int ret, got_picture;
while(1) {
if (av_read_frame(pFormatCtx, packet) < 0) { //读取一帧packet数据包
break; //这里认为视频读取完了
}
if (packet->stream_index == videoStream) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); //解码packet包,原始数据存入pFrame中
if (ret < 0) {
printf("decode error.");
return -1;
}
......
}
}
3、数据转换
int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据
int ret, got_picture;
while(1) {
if (av_read_frame(pFormatCtx, packet) < 0) { //读取一帧packet数据包
break; //这里认为视频读取完了
}
if (packet->stream_index == videoStream) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); //解码packet包,原始数据存入pFrame中
if (ret < 0) { //是否解析成功?
printf("decode error.");
return -1;
}
if (got_picture) { //是否get一帧?
//数据转换
sws_scale(img_convert_ctx,
(uint8_t const * const *) pFrame->data,
pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
pFrameRGB->linesize);
......
}
......
}
}
4、自由操作
int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据
int ret, got_picture;
while(1) {
if (av_read_frame(pFormatCtx, packet) < 0) { //读取一帧packet数据包
break; //这里认为视频读取完了
}
if (packet->stream_index == videoStream) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet); //解码packet包,原始数据存入pFrame中
if (ret < 0) { //是否解析成功?
printf("decode error.");
return -1;
}
if (got_picture) { //是否get一帧?
//数据转换
sws_scale(img_convert_ctx,
(uint8_t const * const *) pFrame->data,
pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
pFrameRGB->linesize);
//自由操作,SaveFrame是自定义函数
SaveFrame(pFrameRGB, pCodecCtx->width,pCodecCtx->height,index++); //保存图片
if (index > 50) return 0; //这里我们就保存50张图片
}
//释放QAVPacket
av_free_packet(packet);
}
}
5、释放QAVPacket
在进入循环解码前进行了av_new_packet,循环中未av_free_packet,造成内存溢出; 在进入循环解码前进行了av_new_packet,循环中进行av_free_pakcet,那么一次new对应无数次free,在编码器上是不符合前后一一对应规范的。 查看源代码,其实可以发现av_read_frame时,自动进行了av_new_packet(),那么其实对于packet,只需要进行一次av_packet_alloc()即可,解码完后av_free_packet。
//释放QAVPacket
av_free_packet(packet);
第十步:释放a资源
全部解码完成后,按照申请顺序,进行对应资源的释放。
av_free(out_buffer);
av_free(pFrameRGB);
sws_freeContext(img_convert_ctx);
avcodec_close(pCodecCtx); //关闭编码/解码器
avformat_close_input(&pFormatCtx); //关闭文件全局上下文
小结,这以上就是有关音视频开发的FFmpeg的基础入门学习,主要介绍基本知识、组件、命令、语法以及简单的解码流程分析。对于FFmpeg的学习还有很多,大家可以参考《Android音视频开发入门精通版》这个由【网易音视频开发大佬整理】出的PDF文档,我看了里面内容很详细,100w字数以上+图文解析。所以这里推荐给各位想学习音视频的程序员。
音视频学习之路,是需要技术知识慢慢积累的。虽然技术需要很广很深,一步也不能吃成胖子,需要长时间学习加消化;冰冻三尺非一日之寒,加油鸭!
网友评论