FFmpeg介绍
一、ffmpeg简述
FFmpeg
是一套可以用来记录
、转换数字音频
、视频
,并能将其转化为流
的开源计算机程序
。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec
,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。
框图如图所示:
二、编解码基础知识
(1)封装格式
所谓封装格式
是指音视频的组合格式
,例如最常见的封装格式有mp4
、mp3
、flv
等。简单来说,我们平时接触到的带有后缀
的音视频文件都是一种封装格式
。不同的封装格式遵循不同的协议标准
。有兴趣的可以自行扩展。
(2)编码格式
以mp4
为例,通常应该包含有视频和音频
。视频的编码格式为YUV420P
,音频的编码格式为PCM
。再以YUV420编码格式
为例。我们知道通常图像的显示为RGB
(红绿蓝三原色),在视频压缩
的时候会首先将代表每一帧
画面的RGB
压缩为YUV
,再按照关键帧
(I帧),过渡帧
(P帧或B帧)进行运算和编码。解码的过程正好相反,解码器会读到I帧,并根据I帧运算和解码P帧以及B帧。并最终根据视频文件预设的FPS还原
每一帧画面的RGB数据
。最后推送给显卡。所以通常我们说的编码过程就包括:画面采集
、转码
、编码
再封装
。
(3)视频解码和音频解码有什么区别
FPS
是图像领域中的定义,是指画面每秒传输帧数
,通俗来讲就是指动画或视频的画面数。FPS太低画面会感觉闪烁不够连贯,FPS越高需要显卡性能越好。一些高速摄像机的采集速度能够达到11000帧/秒,那么在播放这类影片的时候我们是否也需要以11000帧/秒播放呢?当然不是,通常我们会按照25帧/秒或者60帧/秒设定图像的FPS值。但是由于视频存在关键帧
和过渡帧
的区别,关键帧
保存了完整的画面
而过渡帧
只是保存了与前一帧画面的变化部分
,需要通过关键帧计算获得。因此我们需要对每一帧都进行解码
,即获取画面的YUV数据
。同时只对我们真正需要显示的画面进行转码,即将YUV数据转换成RGB数据,包括计算画面的宽高等。
但是音频
则不然,音频的播放
必须和采集
保持同步。提高或降低音频的播放速度都会让音质发生变化,这也是变声器的原理。因此在实际开发中为了保证播放的音视频同步,我们往往会按照音频的播放速度来控制视频的解码转码速度。
三、代码实现
(1)注册FFmpeg组件
//注册和初始化FFmpeg封装器和网络设备
av_register_all();
avformat_network_init();
avdevice_register_all();
(2)打开文件和创建输入设备
/*
AVFormatContext 表示一个封装器,
在读取多媒体文件的时候,它负责保存与封装和编解码有关的上下文信息。
avformat_open_input函数可以根据文件后缀名来创建封装器。
*/
AVFormatContext *pFormatCtx = NULL;
int errnum = avformat_open_input(&pFormatCtx, filename, NULL, NULL);
if (errnum < 0) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
(3)遍历流并初始化解码器
/*
封装器中保存了各种流媒体的通道,通常视频通道为0,音频通道为1。
除此以外可能还包含字幕流通道等。
第2步和第3步基本就是打开多媒体文件的主要步骤,
解码和转码的所有参数都可以在这里获取。
接下来我们就需要循环进行读取、解码、转码直到播放完成。
*/
for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
AVCodecContext *pCodecCtx = pFormatCtx->streams[i]->codec; // 解码器上下文
if (pCodecCtx->codec_type == AVMEDIA_TYPE_VIDEO) {
// 视频通道
int videoIndex = i;
// 视频的宽,高
int srcWidth = pCodecCtx->width;
int srcHeight = pCodecCtx->height;
// 创建视频解码器,打开解码器
AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
if (!codec) {
// 无法创建对应的解码器
errnum = avcodec_open2(pCodecCtx, codec, NULL);
if (errnum < 0) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
cout << "video decoder open success!" << endl;
}
}
if (pCodecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
// 音频通道
int audioIndex = i; // 创建音频解码器,打开解码器
AVCodec *codec = avcodec_find_decoder(pCodecCtx->codec_id);
if (!codec) {
// 无法创建对应的解码器
errnum = avcodec_open2(pCodecCtx, codec, NULL);
if (errnum < 0) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
int sampleRate = pCodecCtx->sample_rate; // 音频采样率
int channels = pCodecCtx->channels; // 声道数
AVSampleFormat fmt = pCodecCtx->sample_fmt; // 样本格式
cout << "audio decoder open success!" << endl;
}
}
}
(4)读取压缩数据
/*之所以称为压缩数据主要是为了区分AVPacket和AVFrame两个结构体。
AVPacket表示一幅经过了关键帧或过渡帧编码后的画面,
AVFrame表示一个AVPacket经过解码后的完整YUV画面*/
AVPacket *pkt = NULL;
pkt = av_packet_alloc(); // 初始化AVPacket// 读取一帧数据
errnum = av_read_frame(pFormatCtx, pkt);
if (errnum == AVERROR_EOF) { // 已经读取到文件尾
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
if (errnum < 0) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
(5)解码
errnum = avcodec_send_packet(pCodecCtx, pkt);
if (errnum < 0) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
AVFrame *yuv = av_frame_alloc();
AVFrame *pcm = av_frame_alloc();
if (pkt->stream_index == videoIndex) { // 判断当前解码帧为视频帧
errnum = avcodec_receive_frame(pCodecCtx, yuv); // 解码视频
if (errnum < 0) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
}
if (pkt->stream_index == audioIndex) { // 判断当前解码帧为音频帧
errnum = avcodec_receive_frame(pCodecCtx, pcm); // 解码音频
if (errnum < 0) {
av_strerror(errnum, errbuf, sizeof(errbuf));
cout << errbuf << endl;
}
}
(6)视频转码
// 720p输出标准
/*
这里需要解释一下outWidth * outHeight * 4计算理由:
720p标准的视频画面包含720 * 480个像素点,
每一个像素点包含了RGBA4类数据,每一类数据分别由1个byte即8个bit表示。
因此一幅完整画面所占的大小为outWidth * outHeight * 4。
*/
int outWidth = 720;
int outHeight = 480;
char *outData = new char[outWidth * outHeight * 4]
SwsContext *videoSwsCtx = NULL;
videoSwsCtx = sws_getCachedContext(videoSwsCtx, srcWidth, srcHeight, (AVPixelFormat)pixFmt, // 输入
outWidth, outHeight, AV_PIX_FMT_BGRA, // 输出
SWS_BICUBIC, // 算法
0, 0, 0);// 分配数据空间
uint8_t *dstData[AV_NUM_DATA_POINTERS] = { 0 };
dstData[0] = (uint8_t *)outData;
int dstStride[AV_NUM_DATA_POINTERS] = { 0 };
dstStride[0] = outWidth * 4;
int h = sws_scale(videoSwsCtx, yuv->data, yuv->linesize, 0, srcHeight, dstData, dstStride);
if (h != outHeight) { } // 转码失败
(7)音频转码
char *outData = new char[10000];//输出指针
AVCodecContext *pCodecCtx = pFormatCtx->streams[audioIndex]->codec; // 获取音频解码器上下文
SwrContext *audioSwrCtx = NULL;
audioSwrCtx = swr_alloc();
audioSwrCtx = swr_alloc_set_opts(audioSwrCtx,
AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100, // 输出参数:双通道立体声 CD音质
pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_rate, // 输入参数
0, 0);
swr_init(audioSwrCtx);
uint8_t *out[AV_NUM_DATA_POINTERS] = { 0 };
out[0] = (uint8_t *)outData;// 计算输出空间
int dst_nb_samples = av_rescale_rnd(pcm->nb_samples, pCodecCtx->sample_rate, pCodecCtx->sample_rate, AV_ROUND_UP);
int len = swr_convert(audioSwrCtx, out, dst_nb_samples,
(const uint8_t **)pcm->data, pcm->nb_samples);
int channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO); // AV_CH_LAYOUT_STEREO -> 2 根据声道类型得到声道数// 实际音频数据长度
int dst_bufsize = av_samples_get_buffer_size(NULL,
channels, // 通道数
pcm->nb_samples,// 1024
AV_SAMPLE_FMT_S16, 0);
if (dst_bufsize < 0) { }// 音频转码错误
四、代码地址
基于qt的FFmpeg客户端(Linux版本):
https://github.com/wengmq/ffmpeg_demo
参考来源:https://www.imooc.com/article/details/id/28012