音视频面经_音视频知识解析:第三章FFMPEG编写视频解码
本专栏会持续更新优质内容,敬请订阅,关注更新。
专栏地址:音视频面经_音视频知识解析
------------下面正文开始-------------
1 视频解码知识
播放一个视频文件的流程如下所示:
其中视频解码就是重要的一步。在解封装后,分离出的视频流通常是被压缩的。视频解码器负责将这些压缩的视频帧转换成未压缩的帧数据。这个过程涉及复杂算法,目的是尽可能准确地重建原始视频信号。
纯净的视频解码流程
▫ 压缩编码数据->像素数据。
▫ 例如解码H.264,就是“H.264码流->YUV”。
一般的视频解码流程
▫ 视频码流一般存储在一定的封装格式(例如MP4、AVI等)中。封装格式中通常还包含音频码流等内容。
▫ 对于封装格式中的视频,需要先从封装格式中提取中视频码流,然后再进行解码。
▫ 例如解码MKV格式的视频文件,就是“MKV->H.264码流->YUV”。
下面介绍的是一般的视频解码流程
2 VC下FFmpeg开发环境的搭建
2.1 环境准备
Visual Studio 2010
2.2 新建控制台工程
▫ 打开VC++
▫ 文件->新建->项目->Win32控制台应用程序
2.3 拷贝FFmpeg开发文件
▫ 头文件(*.h)拷贝至项目文件夹的include子文件夹下
▫ 导入库文件(*.lib)拷贝至项目文件夹的lib子文件夹下
▫ 动态库文件(*.dll)拷贝至项目文件夹下
PS:如果直接使用官网上下载的FFmpeg开发文件。则可能还需要将MinGW安装目录中的inttypes.h,stdint.h,_mingw.h三个文件拷贝至项目文件夹的include子文件夹下。
2.3 配置开发文件
▫ 打开属性面板
解决方案资源管理器->右键单击项目->属性
▫ 头文件配置
配置属性->C/C++->常规->附加包含目录,输入“include”(刚才拷贝头文件的目录)
▫ 导入库配置
配置属性->链接器->常规->附加库目录,输入“lib” (刚才拷贝库文件的目录)
配置属性->链接器->输入->附加依赖项,输入“avcodec.lib;avformat.lib; avutil.lib; avdevice.lib; avfilter.lib;postproc.lib; swresample.lib; swscale.lib”(导入库的文件名)
▫ 动态库不用配置
2.4 测试
▫ 创建源代码文件
在工程中创建一个包含main()函数的C/C++文件(如果已经有了可以跳过这一步)。
▫ 包含头文件
如果是C语言中使用FFmpeg,则直接使用下面代码
#include "libavcodec/avcodec.h"
如果是C++语言中使用FFmpeg,则使用下面代码
#define __STDC_CONSTANT_MACROS
extern "C"
{
#include "libavcodec/avcodec.h "
}
▫ main()中调用一个FFmpeg的接口函数
例如下面代码打印出了FFmpeg的配置信息
int main(int argc, char* argv[]){
printf("%s", avcodec_configuration());
return 0;
}
如果运行无误,则代表FFmpeg已经配置完成
源代码如下:
// ffmpegDecoder.cpp : 定义控制台应用程序的入口点。 // #define __STDC_CONSTANT_MACROS #include "stdafx.h" extern "C" { #include "libavcodec/avcodec.h " } int _tmain(int argc, _TCHAR* argv[]) { printf("%s\n", avcodec_configuration()); return 0; }
3 FFmpeg库简介
FFmpeg框架的基本组成包含AVFormat、AVCodec、AVFilter、AVDevice、AVUtil等模块库,结构如下图所示。
FFmpeg一共包含8个库:
▫ avcodec:编解码(最重要的库)。
▫ avformat:封装格式处理。
▫ avfilter:滤镜特效处理。
▫ avdevice:各种设备的输入输出。
▫ avutil:工具库(大部分库都需要这个库的支持)。
▫ postproc:后加工。
▫ swresample:音频采样数据格式转换。
▫ swscale:视频像素数据格式转换。
3.1 封装模块AVFormat
AVFormat中实现了目前多媒体领域中的绝大多数媒体封装格式,包括封装和解封装,如MP4、FLV、KV、TS等文件封装格式,RTMP、RTSP、MMS、HLS等网络协议封装格式。FFmpeg是否支持某种媒体封装格式,取决于编译时是否包含了该格式的封装库。根据实际需求,可进行媒体封装格式的扩展,增加自己定制的封装格式,即在AVFormat中增加自己的封装处理模块。
3.2 编解码模块AVCodec
AVCodec中实现了目前多媒体领域绝大多数常用的编解码格式,既支持编码,也支持解码。AVCodec除了支持MPEG4、AAC、MJPEG等自带的媒体编解码格式之外,还支持第三方的编解码器,如H.264(AVC)编码,需要使用x264编码器;H.265(HEVC)编码,需要使用x265编码器;MP3(mp3lame)编码,需要使用libmp3lame编码器。如果希望增加自己的编码格式,或者硬件编解码,则需要在AVCodec中增加相应的编解码模块,关于AVCode的更多相关信息以及使用信息将会在后面的章节中进行详细的介绍。
3.3 设备模块AVDevice
avdevice是FFmpeg的音视频设备库,它包含了音视频的各种输入输出设备库,其中输入设备指的是采集音视频信号的设备,输出设备指的是渲染音视频画面的设备。当然FFmpeg不会直接操作设备硬件,而是通过第三方的软件包去实现,比如采集媒体信号用到了Windows平台的VFW捕捉器(VFW全称Video for Windows),以及VFW的升级版DirectShow捕捉器;渲染媒体画面用到了Windows平台的GDI接收器(GDI全称Graphics Device Interface),以及跨平台的SDL2媒体开发库(SDL全称Simple DirectMedia Layer)。当然,FFmpeg也支持音效处理库OpenAL(全称Open Audio Library)和图形处理库OpenGL(全称Open Graphics Library)。
3.4 工具模块avutil
avutil是FFmpeg的音视频工具库,它包含了常见的通用工具和各类算法库,其中通用工具包括字典操作、日志记录、缓存交互、线程处理,以及加解密库aes、md5、sha、base64、等;各类算法包括排队算法fifo、排序算法qsort、哈希表hash、二叉树tree等等。除此以外,avutil也囊括了色彩空间、音频采样等方面的公共函数。
3.5 滤镜模块AVFilter
avfilter是FFmpeg的音视频滤镜库,它包含了加工编辑音频和视频的各种滤镜包,其中音频滤镜的源码文件名形如af_***.c,视频滤镜的源码文件名形如vf_***.c。音频滤镜多用于调整参数、混合音频等处理,视频滤镜多用于变换视频、特效画面、添加部件等处理。
3.6 视频图像转换计算模块swscale
swscale模块提供了高级别的图像转换API,例如它允许进行图像缩放和像素格式转换,常见于将图像从1080p转换成720p或者480p等的缩放,或者将图像数据从YUV420P转换成YUYV,或者YUV转RGB等图像格式转换。
3.7 后期效果处理模块postproc
postproc是FFmpeg的音视频后期效果处理库,它主要用于进行后期的效果处理,如果代码中使用了滤镜,编译时就要链接这个库,因为滤镜用到了postproc的一些基础函数。
3.8 音频转换计算模块swresample
swresample模块提供了高级别的音频重采样API。例如它允许操作音频采样、音频通道布局转换与布局调整。
4 FFmpeg解码的函数
FFmpeg解码的流程图如下所示
1 FFmpeg解码函数简介
▫ av_register_all():注册所有组件。
▫ avformat_open_input():打开输入视频文件。
▫ avformat_find_stream_info():获取视频文件信息。
▫ avcodec_find_decoder():查找解码器。
▫ avcodec_open2():打开解码器。
▫ av_read_frame():从输入文件读取一帧压缩数据。
▫ avcodec_decode_video2():解码一帧压缩数据。
▫ avcodec_close():关闭解码器。
▫ avformat_close_input():关闭输入视频文件。
2 FFmpeg解码的数据结构
FFmpeg解码的数据结构如下所示
▫ AVFormatContext
封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息。
▫ AVInputFormat
每种封装格式(例如FLV, MKV, MP4, AVI)对应一个该结构体。
▫ AVStream
视频文件中每个视频(音频)流对应一个该结构体。
▫ AVCodecContext
编码器上下文结构体,保存了视频(音频)编解码相关信息。
▫ AVCodec
每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。
▫ AVPacket
存储一帧压缩编码数据。
▫ AVFrame
存储一帧解码后像素(采样)数据。
3 FFmpeg数据结构分析
▫ AVFormatContext
iformat:输入视频的AVInputFormat
nb_streams :输入视频的AVStream 个数
streams :输入视频的AVStream []数组
duration :输入视频的时长(以微秒为单位)
bit_rate :输入视频的码率
▫ AVInputFormat
name:封装格式名称
long_name:封装格式的长名称
extensions:封装格式的扩展名
id:封装格式ID
一些封装格式处理的接口函数
▫ AVStream
id:序号
codec:该流对应的AVCodecContext
time_base:该流的时基
r_frame_rate:该流的帧率
▫ AVCodecContext
codec:编解码器的AVCodec
width, height:图像的宽高(只针对视频)
pix_fmt:像素格式(只针对视频)
sample_rate:采样率(只针对音频)
channels:声道数(只针对音频)
sample_fmt:采样格式(只针对音频)
▫ AVCodec
name:编解码器名称
long_name:编解码器长名称
type:编解码器类型
id:编解码器ID
一些编解码的接口函数
▫ AVPacket
pts:显示时间戳
dts :解码时间戳
data :压缩编码数据
size :压缩编码数据大小
stream_index :所属的AVStream
▫ AVFrame
data:解码后的图像像素数据(音频采样数据)。
linesize:对视频来说是图像中一行像素的大小;对音频来说是整个音
频帧的大小。
width, height:图像的宽高(只针对视频)。
key_frame:是否为关键帧(只针对视频) 。
pict_type:帧类型(只针对视频) 。例如I,P,B。
4 ffmpeg解码示例程序
#include <stdio.h> #define __STDC_CONSTANT_MACROS extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" }; int main(int argc, char* argv[]) { AVFormatContext *pFormatCtx; int i, videoindex; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVFrame *pFrame,*pFrameYUV; uint8_t *out_buffer; AVPacket *packet; int y_size; int ret, got_picture; struct SwsContext *img_convert_ctx; //输入文件路径 char filepath[]="Titanic.ts"; int frame_cnt; av_register_all(); avformat_network_init(); pFormatCtx = avformat_alloc_context(); if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){ printf("Couldn't open input stream.\n"); return -1; } if(avformat_find_stream_info(pFormatCtx,NULL)<0){ printf("Couldn't find stream information.\n"); return -1; } videoindex=-1; for(i=0; i<pFormatCtx->nb_streams; i++) if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){ videoindex=i; break; } if(videoindex==-1){ printf("Didn't find a video stream.\n"); return -1; } pCodecCtx=pFormatCtx->streams[videoindex]->codec; pCodec=avcodec_find_decoder(pCodecCtx->codec_id); if(pCodec==NULL){ printf("Codec not found.\n"); return -1; } if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){ printf("Could not open codec.\n"); return -1; } /* * 在此处添加输出视频信息的代码 * 取自于pFormatCtx,使用fprintf() */ pFrame=av_frame_alloc(); pFrameYUV=av_frame_alloc(); out_buffer=(uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height)); avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); packet=(AVPacket *)av_malloc(sizeof(AVPacket)); //Output Info----------------------------- printf("--------------- File Information ----------------\n"); av_dump_format(pFormatCtx,0,filepath,0); printf("-------------------------------------------------\n"); img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); frame_cnt=0; while(av_read_frame(pFormatCtx, packet)>=0){ if(packet->stream_index==videoindex){ /* * 在此处添加输出H264码流的代码 * 取自于packet,使用fwrite() */ ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); if(ret < 0){ printf("Decode Error.\n"); return -1; } if(got_picture){ sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); printf("Decoded frame index: %d\n",frame_cnt); /* * 在此处添加输出YUV的代码 * 取自于pFrameYUV,使用fwrite() */ frame_cnt++; } } av_free_packet(packet); } sws_freeContext(img_convert_ctx); av_frame_free(&pFrameYUV); av_frame_free(&pFrame); avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); return 0; }
5练习
• 给源代码中每个FFmpeg函数添加中文注释
• 修改源代码。对于测试文件,输出以下几种文件
▫“output.h264”文件
解码前的H.264码流数据(只对MPEG-TS,AVI格式作要求)
▫“output.yuv”文件
解码后的YUV420P像素数据
▫“output.txt”文件
封装格式参数:封装格式、比特率、时长
视频编码参数:编码方式、宽高
每一个解码前视频帧参数:帧大小
每一个解码后视频帧参数:帧类型
练习答案:
#include "StdAfx.h" #include <stdio.h> #include <fstream> #define __STDC_CONSTANT_MACROS extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" #include "libswscale/swscale.h" }; int main(int argc, char* argv[]) { AVFormatContext *pFormatCtx; int i, videoindex; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVFrame *pFrame,*pFrameYUV; uint8_t *out_buffer; AVPacket *packet; int y_size; int ret, got_picture; struct SwsContext *img_convert_ctx; //输入文件路径 char filepath[]="Titanic.ts"; int frame_cnt; av_register_all(); avformat_network_init(); pFormatCtx = avformat_alloc_context(); if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){ printf("Couldn't open input stream.\n"); return -1; } if(avformat_find_stream_info(pFormatCtx,NULL)<0){ printf("Couldn't find stream information.\n"); return -1; } videoindex=-1; for(i=0; i<pFormatCtx->nb_streams; i++) if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){ videoindex=i; break; } if(videoindex==-1){ printf("Didn't find a video stream.\n"); return -1; } pCodecCtx=pFormatCtx->streams[videoindex]->codec; pCodec=avcodec_find_decoder(pCodecCtx->codec_id); if(pCodec==NULL){ printf("Codec not found.\n"); return -1; } if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){ printf("Could not open codec.\n"); return -1; } /* * 在此处添加输出视频信息的代码 * 取自于pFormatCtx,使用fprintf() */ pFrame=av_frame_alloc(); pFrameYUV=av_frame_alloc(); out_buffer=(uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height)); avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); packet=(AVPacket *)av_malloc(sizeof(AVPacket)); //Output Info----------------------------- printf("--------------- File Information ----------------\n"); av_dump_format(pFormatCtx,0,filepath,0); printf("-------------------------------------------------\n"); img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); FILE *fp_264 = fopen("test264.h264", "wb+"); FILE *fp_yuv = fopen("testYUV.yuv", "wb+"); frame_cnt=0; while(av_read_frame(pFormatCtx, packet)>=0){ if(packet->stream_index==videoindex){ /* * 在此处添加输出H264码流的代码 * 取自于packet,使用fwrite() */ fwrite(packet->data, 1, packet->size, fp_264); ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); if(ret < 0){ printf("Decode Error.\n"); return -1; } if(got_picture){ sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); printf("Decoded frame index: %d\n",frame_cnt); /* * 在此处添加输出YUV的代码 * 取自于pFrameYUV,使用fwrite() */ fwrite(pFrameYUV->data[0], 1, pCodecCtx->width * pCodecCtx->height, fp_yuv);//写入Y分量数据到fp_yuv fwrite(pFrameYUV->data[1], 1, pCodecCtx->width * pCodecCtx->height / 4, fp_yuv);//写入U分量数据到fp_yuv, YUV420P的是四个Y共用一个UV fwrite(pFrameYUV->data[2], 1, pCodecCtx->width * pCodecCtx->height / 4, fp_yuv);//写入V分量数据到fp_yuv, YUV420P的是四个Y共用一个UV frame_cnt++; } } av_free_packet(packet); } fclose(fp_264); fclose(fp_yuv); sws_freeContext(img_convert_ctx); av_frame_free(&pFrameYUV); av_frame_free(&pFrame); avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); return 0; }
本人在CVTE从事音视频开发工作多年,推出该专栏的目的是帮助更多有意向从事音视频开发的同学了解音视频编解码等基本知识、熟悉FFmpeg、SDL、OpenGL等开源框架的使用和编程,并动手开发视频播放器。 这些都是目前大厂音视频开发工程师相关职位要求的必备技能。本专栏会持续更新优质内容,敬请订阅,关注更新。随着内容不断丰富,可能会做付费专栏。另外提供CVTE内推、职位信息、面试答疑