音视频学习一——FFmpeg封装
Posted 孟小胖_H
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了音视频学习一——FFmpeg封装相关的知识,希望对你有一定的参考价值。
系列文章目录
文章目录
前言
FFmpeg是本人初入音视频学习知道的第一个较重要的开源框架。本博客主要介绍FFmpeg接口用C++封装,网上有很多资源,此封装是按照自己想法做的,肯定不是很精简的很好的封装,但是应该是比较容易理解和容易实现的。
封装FFmpeg之前,先了解FFmpeg相关的结构体和基本函数。
FFmpeg结构体了解参考链接点击此处。
FFmpeg函数了解及源代码分析参考链接点击此处。
一、FFmpeg结构体和函数简述
看了前言里的两篇博客之后,会对FFmpeg的结构体和函数有深刻认识。
1.这里简单表明结构体的作用:
AVFormatContext //存储文件各种信息
AVPacket //存储读取视频文件的数据
AVCodecContext //解码器上下文
AVCodec //解码器
AVFrame //存放解码后的相关内容
//下面两个结构体,由于在视频播放器相关软件的实践中这两个结构体暂时还没有用到过,所以暂时先不管,在上面博客链接中可以查看。
AVStream* _avstream;
AVIOContext* _avioctx;
//AVFormatContext//存储文件各种信息
/*
很多博客中也都说,是直接贯穿整个FFmpeg和音视频文件的结构体。就相当于是文件在FFmpeg中展示的方式。一般一个FFmpeg封装里只使用一个。
*/
//AVPacket是存储音视频文件中的数据的,解码前的
/*
用来从音视频文件中读取数据并且保存在AVPacket中,一般一个FFmpeg封装只使用一个,只不过有时候读取的是音频数据,有时候读取的是视频数据,到时候判断一下读取的是音频还是视频就好,然后做音频或者视频处理。
*/
//AVCodecContext//解码器上下文
/*
音视频文件往往需要编解码操作,这个保存的是和解码器相关的信息。一般一个简单的FFmpeg封装中音频和视频各需要一个。
*/
//AVCodec//解码器
/*
对音视频解码所需要的解码器,一般一个简单的FFmpeg封装中音频和视频各需要一个。
*/
//AVFrame//存放解码后的相关内容
/*
用于音频或者视频解码后的数据存储。一般会用来存储视频解码后、视频转码后、音频解码后、音频转码后的数据。一般这些每一种使用情况都各需要一个
*/
2.FFmpeg函数简介
函数参数请仔细查看前言里的参考博客
下面中的转码部分较复杂,转码部分使用以及参数设置参考链接点击此处。以下函数都没有写形参,对着函数使用的时候再查找函数的使用以及参数的意思,因为参数比较多,小白使用的时候一定需要查找相关使用详解。
av_register_all();//ffmpeg各种初始化
/*一般使用FFmpeg前都会首先执行一下这个函数*/
avformat_open_input();
/*打开视频文件,参数需要AVFormatContext*/
avcodec_find_decoder();
/*查找解码器,参数需要AVCodecContex,返回是解码器AVCodex*/
avcodec_open2();
/*打开解码器,参数需要AVCodex*/
avcodec_send_packet();
/*读取视频数据并且解码,参数需要音频(视频)解码器和AVPacket
*这里具体解码器要看AVPacket数据是音频的还是视频的,要传入对应的解码器。
*/
avcodec_receive_frame();
/*和上面解码函数相对应,这里需要音频(视频)解码器和Frame,用于接收解码后的数据,而解码后的数据就会被存储在Frame中。
*/
//以上是从视频中读取数据,解码数据所使用的函数
//但是一般音视频播放还需要转码(渲染)
//旧版本中解码一个函数就可以了,后来被替换成了上面两个函数
//上面的函数是音频和视频通用的函数,而音频和视频转码(渲染)所需要的函数就不一样了。
//音视频解码很简单,但是渲染(转码)一开始真的蒙,主要是参数要搞清楚。
//视频转码(渲染):
av_image_get_buffer_size();
/*主要是求转码(渲染)后的数据大小,以便申请一个适当内存的buff存放转码(渲染)后的数据*/
//视频转码三剑客,具体怎么用看后面的代码,这个部分比较麻烦,特别是参数,视频部分Bug大部分时间都花费在这个上面:
sws_getContext();
sws_scale();
sws_freeContext();
*/
//音频转码(渲染)
av_samples_get_buffer_size();
/*计算将转码后的音频数据保存起来大概需要多少内存*/
//主要用来设置转码所需要的相关参数
swr_alloc();
//返回值是struct SwrContext*类型,返回值暂且称为swrctx,主要用于转码阶段
swr_alloc_set_opts();
/*
//设置swrctx的相关参数,用于后面初始化
参数1:重采样上下文
参数2:输出的layout, 如:5.1声道…
参数3:输出的样本格式。Float, S16, S24
参数4:输出的样本率。可以不变。
参数5:输入的layout。
参数6:输入的样本格式。
参数7:输入的样本率。
参数8,参数9,日志,不用管,可直接传0
*/
swr_init();
/*初始化swrctx*/
swr_convert();
/*转码的核心,进行转码*/
二、视视频解码转码流程
网上可以找到很多流程图,包括从什么格式转换到什么格式,下面是自己画的函数使用流程图,因为目的是为了封装。
三、FFmpeg封装(仅针对于普通FFmpeg简单的封装和实现)
1.FFmpeg类的封装
下面是实现音视频解码转码的简单的封装。如果要实现音视频播放器的话,下面的属性是不够的,后面肯定还是要添加,到后面的内容再慢慢展现。其实正常的话,解码转码函数可以写一个,在函数里面用if判断是音频还是视频数据,然后再执行if后的代码段。为了使得视频和音频明显区分开,这里就将他们尽量都区分开来了。
class MzFFmpeg
public:
//单例模式
static MzFFmpeg* Get_ffmpeg()
static MzFFmpeg mz_ffmpeg;
return &mz_ffmpeg;
~MzFFmpeg()
//将封装后的音视频解码转码的函数都放在run函数里面
void run();
AVFormatContext* Getavformatctx()
return _avformatctx;
private:
MzFFmpeg();
//获取错误码 并打印错误(可以输出到文件中,日志)
void PrintError();
//打开视频流文件 获取视频流文件信息
int open();
//关闭_avformatctx
void close();
private:
//下面这两个参数一般一个FFmpeg中只用使用一个
AVFormatContext* _avformatctx; //存储文件各种上下文
AVPacket* _avpacket; //存储读取视频文件的数据
/*******************视频需要的属性、方法********************/
private:
//查找并打开解码器
int FindAndOpenDecode();
//读取视频帧并解码
int ReadAndDecode();
//转码,转格式
void TransCode();
public:
std::queue<uchar*> _videobuff; //视频帧缓存
std::string _avfilepath; //音视频文件路径
private:
AVCodecContext* _avcodecctx; //视频解码器上下文
AVCodec* _avcodec; //视频解码器
AVFrame* _avframeyuv; //存放解码但是未转码的视频帧
AVFrame* _avframergb; //转码后的视频帧
/*
//如下两个未使用到
AVStream* _avstream;
AVIOContext* _avioctx;
*/
std::mutex _mtx;
ERROR_NUM _error; //获取错误信息
int _totaltime; //视频总时长
int _videoindex; //视频流下标
//视频流下标和音频流下标是分辨AVPacket里面是视频数据还是音频数据的关键
/**********************************************************/
/*******************音频需要的属性、方法********************/
signals:
void _signalaudio(char* buff, int len);
public:
//查找并打开音频解码器
int AudioFindAndOpenDecode();
//读取音频帧并解码
int AudioReadAndDecode();
//转码,转格式
void AudioTransCode();
public:
std::queue<AudioBuff*> _audiobuff;//音频缓存
private:
//处理音频需要的结构体
AVCodecContext* _audiocodecctx;//音频解码器上下文
AVCodec* _audiocodec; //音频解码器
AVFrame* _audioframeacc; //存放音频解码后的数据
//解码后的音频属性(音频原本的属性)
uint64_t _channellayout; //布局方式
int _nbsamples; //采样个数
int _samplerate; //采样率
int _channels; //通道数
enum AVSampleFormat _samplefmt;//样本格式
//转码后的音频属性
uint64_t _ochannellayout; //布局方式
int _onbsamples; //采样个数
int _osamplerate; //采样率
int _ochannels; //通道数
enum AVSampleFormat _osamplefmt;//样本格式
int _audioindex; //音频流下标
/**********************************************************/
;
#endif
2.FFmpeg封装后的成员函数的实现
注:如下并不是完整的实现代码,对于音视频播放器来说,下面的代码也不完善,要后面添加一些属性。下面只是FFmpeg简单的封装之后,对成员函数的实现。网上的代码很多,只不过每个人的都略有不同而已。仅供封装FFmpeg借鉴而已。实现音视频播放器的话还需要理解之后,看一些其他代码,慢慢实现。
//构造函数 初始化工作
MzFFmpeg::MzFFmpeg()
av_register_all(); //ffmpeg各种初始化
//avformat_network_init();//网络初始化 这里可有可无
_avformatctx = NULL;
_avpacket = NULL;
/**********************视频相关属性初始化*********************/
_avcodecctx = NULL;
_avcodec = NULL;
_avframergb = NULL; //解码未转码的视频帧
_avframeyuv = NULL; //转码后的视频帧
//_avstream = NULL;
//_avioctx = NULL;
_videoindex = -1;
/**************************************************************/
/**********************音频相关属性初始化********************/
_audiocodecctx = NULL;
_audiocodec = NULL;
_audioframeacc = NULL;
_audioindex = -1;
/**************************************************************/
//打开视频流文件 获取视频流文件信息
int MzFFmpeg::open()
_mtx.lock();
_avformatctx = avformat_alloc_context();//全程都要使用 析构函数里释放
int ret = avformat_open_input(&_avformatctx, _avfilepath.c_str(), NULL, NULL);//打开视频文件 获取文件信息 成功返回0
if (ret)
_mtx.unlock();
_error = AVFORMATOPENINPUT_ERROR;
PrintError();
return -1;
//_totaltime = _avformatctx->duration / (AV_TIME_BASE);//获取视频时长 总时长/时间单位
_mtx.unlock();
return 0;
//关闭视频流文件
void MzFFmpeg::close()
_mtx.lock();
if (_avformatctx != NULL)
avformat_close_input(&_avformatctx);
_avformatctx = NULL;
_mtx.unlock();
/***********************************************视频部分******************************************/
//查找并打开解码器
int MzFFmpeg::FindAndOpenDecode()
_mtx.lock();
//查找视频解码器
for (int i = 0; i < _avformatctx->nb_streams; i++)
if (_avformatctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
_videoindex = i;
break;
if (_videoindex == -1)
_mtx.unlock();
_error = FINDDECODE_ERROR;
PrintError();
return -1;
_avcodecctx = _avformatctx->streams[_videoindex]->codec;
_avcodec = avcodec_find_decoder(_avcodecctx->codec_id); //解码器确定
if (_avcodec == NULL)
_mtx.unlock();
_error = FINDDECODE_ERROR;
PrintError();
return -1;
//打开解码器
int ret = avcodec_open2(_avcodecctx, _avcodec, NULL);
if (ret < 0)
_mtx.unlock();
_error = AVCODECOPEN_ERROR;
PrintError();
return -1;
//视频帧率
//_fps = _avformatctx->streams[_videoindex]->avg_frame_rate.num / _avformatctx->streams[_videoindex]->avg_frame_rate.den;
//_sfps = _fps;
_mtx.unlock();
return 0;
//读取视频帧 并解码
int MzFFmpeg::ReadAndDecode()
_mtx.lock();
//读取视频帧并且解码
int ret = avcodec_send_packet(_avformatctx->streams[_videoindex]->codec, _avpacket);
if (ret < 0)
_mtx.unlock();
_error = AVCODECSENDPACKET_ERROR;
PrintError();
return -1;
ret = avcodec_receive_frame(_avformatctx->streams[_videoindex]->codec, _avframeyuv);
if(ret < 0)
_mtx.unlock();
return -1;
_mtx.unlock();
return 0;
//转码(RGB)
//参考博客https://blog.csdn.net/wzz953200463/article/details/115938597的设置
void MzFFmpeg::TransCode()
_mtx.lock();
AVCodecContext* avcodecctx = _avformatctx->streams[_videoindex]->codec;
int numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, avcodecctx->width, avcodecctx->height);
int nBGRFrameSize = av_image_get_buffer_size(AV_PIX_FMT_RGB32, _avframeyuv->width, _avframeyuv->height, 1);
uchar* rgbBuff = (uchar*)av_malloc(nBGRFrameSize);//slotdata槽函数中释放内存
_avframergb = av_frame_alloc(); //函数尾释放
av_image_fill_arrays(_avframergb->data, _avframergb->linesize, rgbBuff,
AV_PIX_FMT_RGB32, avcodecctx->width, avcodecctx->height, 1);
//改变像素格式
SwsContext *img_convert_ctx = sws_getContext(_avframeyuv->width, _avframeyuv->height,
AV_PIX_FMT_YUV420P, _avframeyuv->width, _avframeyuv->height, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);
//颜色空间转换 yuv420p --> rgb32
sws_scale(img_convert_ctx,(uint8_t const* const*)_avframeyuv->data,
_avframeyuv->linesize, 0, _avframeyuv->height, _avframergb->data, _avframergb->linesize);
sws_freeContext(img_convert_ctx);
_videobuff.push(rgbBuff);
av_frame_free(&_avframergb);
_mtx.unlock();
/*********************************************************************************************/
//音频相关的成员函数实现和视频基本一样,就参数不同而已,这里就呈现音频的转码,对于小白来说可以借鉴参考。但是参数要根据实际情况去修改。下面的注释也大部分都有。
void MzFFmpeg::AudioTransCode()
_mtx.lock();
struct SwrContext* swrctx = swr_alloc();
if (swrctx == NULL)
Sleep(1000);
/*
参数1:重采样上下文
参数2:输出的layout, 如:5.1声道…
参数3:输出的样本格式。Float, S16, S24
参数4:输出的样本率。可以不变。
参数5:输入的layout。
参数6:输入的样本格式。
参数7:输入的样本率。
参数8,参数9,日志,不用管,可直接传0
*/
swrctx = swr_alloc_set_opts(swrctx, _channellayout, _osamplefmt, _samplerate,
_channellayout, _samplefmt, _samplerate, 0, NULL);//分配并设定一些相应的参数
//计算将一系列样本保存起来大概需要多少内存 最后参数的1表明不对齐
//int outbuffsize = av_samples_get_buffer_size(NULL, _ochannels, _onbsamples, _osamplefmt, 1);
//通道数 采样个数 输出格式 对齐方式(1不对齐)
int outbuffsize = av_samples_get_buffer_size(NULL, _channels, _nbsamples, _osamplefmt, 1);
char* buff = (char*)av_malloc((MAX_AUDIO_FRAME_SIZE)*2);
if (buff == NULL)
Sleep(2000);
uchar* outbuff = (uchar*)buff;
swr_init(swrctx);//初始化
//swr_convert(swrctx, &outbuff, MAX_AUDIO_FRAME_SIZE, (const uint8_t**)_audioframeacc->data, _audioframeacc->nb_samples);
//第三个参数:每通道可用输出样本的数量 最后一个参数:每通道可用输入样本的数量
swr_convert(swrctx, &outbuff, MAX_AUDIO_FRAME_SIZE,
(const uint8_t**)_audioframeacc->data, _nbsamples);
AudioBuff* audiobuff = new AudioBuff;
audiobuff->buff = (char*)outbuff;
audiobuff->len = outbuffsize;
_audiobuff.push(audiobuff);
swr_free(&swrctx);
_mtx.unlock();
最后将FFmpeg对外的唯一接口run函数写一下:
/*************************************************************************************************/
void MzFFmpeg::run()
//查找并且打开解码器
FindAndOpenDecode(); //查找、打开视频解码器
AudioFindAndOpenDecode();//查找、打开音频解码器
//读取视频帧并且解码转码
while ((_avpacket = (AVPacket*)av_malloc(sizeof(AVPacket))) && av_read_frame(_avformatctx, _avpacket) >= 0)
_playmtx.lock();//用来暂停播放的锁
//是视频的数据
if ( _avpacket->stream_index == _videoindex)
_avframeyuv = av_frame_alloc();
int ret = ReadAndDecode();
if (ret == 0)
TransCode();
av_frame_free(&_avframeyuv);
//是音频的数据
else if(_avpacket->stream_index == _audioindex)
_audioframeacc = av_frame_alloc();
int ret = AudioReadAndDecode();
if (ret == 0)
AudioTransCode();
av_frame_free(&_audioframeacc);
av_freep(&_avpacket);
_playmtx.unlock();
/*************************************************************************************************/
总结
以上博客记录的是简单的对FFmpeg的封装,但是实现视频播放器的话,有些属性还是需要自己添加的。以上并不是完整的代码。对于FFmpeg的封装,网上代码很多,这里只是结合自己的理解将音频和视频的解码实现分离的更开而已,对于小白来说可能更容易借鉴学习。
FFMPEG+SDL实现视频播放器
一. 前言
基于学习ffmpeg和sdl,写一个视频播放器是个不错的练手项目。
视频播放器的原理很多人的博客都有讲过,这里出于自己总结的目的,还是会做一些概况。
二. 视频播放器基本原理
2.1 解封装
视频文件基本上都是将编码好的音频和视频数据封装在一起形成的,因此拿到视频文件的第一步就是先将它解封装,分为视频流和音频流压缩编码数据。常见的封装格式有MP4、MKV、FLV、AVI、RMVB、TS等。例如,FLV格式的文件经过解封装后,可能得到H.264编码的视频码流和AAC编码的音频码流。
在FFMPEG中,解封装的流程如下:
这一步最重要的是得到解封装器的上下文结构体"AVFormatContext *m_pFormatCtx", 以及接下来我们要解码的音视频流索引。
2.2 解码
原始数据基本上都是经过压缩编码后的数据,解码过程就是将H.264、AAC等压缩后的数据解码成非压缩的音频/视频原始数据,视频一般是YUV或者RGB数据,音频一般是PCM抽样数据。
解码过程可以总结如下:
2.3 SDL2播放视频数据
我们都知道视频其实都是由连续的一帧帧图像快速播放形成的动态效果,一般视频都设置成了25帧,即1s内播放25幅图片。
我们使用SDL2库来播放视频。这和我之前的SDL2学习(一): 显示一张图片中写到的SDL2显示一张图片就关联了起来,不过这里更加复杂点。
在视频解码完后,我们在avcodec_receive_frame
得到的AVFrame对象,就是视频的一帧数据。我们要做的是将这一帧的数据显示到SDL的Render中。总体流程如下:
首先我们需要对得到的AVFrame数据进行大小格式的变换,这里使用sws_scale函数实现,之后就是更新SDL中的Texture和Render了。下面是关键代码:
AVFrame *frame = m_videoFrameQueue.front();
m_videoFrameQueue.pop();
AVFrame *frameYUV = av_frame_alloc();
int ret = av_image_alloc(frameYUV->data, frameYUV->linesize, m_sdlRect.w, m_sdlRect.h, AV_PIX_FMT_YUV420P, 1);
//Convert image
if (m_imgConvertCtx)
{
sws_scale(m_imgConvertCtx, frame->data, frame->linesize, 0, m_videoCodecParams.height, frameYUV->data, frameYUV->linesize);
SDL_UpdateYUVTexture(m_sdlTexture, NULL, frameYUV->data[0], frameYUV->linesize[0], frameYUV->data[1], frameYUV->linesize[1], frameYUV->data[2], frameYUV->linesize[2]);
SDL_RenderClear(m_sdlRender);
SDL_RenderCopy(m_sdlRender, m_sdlTexture, NULL, &m_sdlRect);
// Present picture
SDL_RenderPresent(m_sdlRender);
}
2.4?SDL2播放音频数据
对于音频数据,avcodec_receive_frame
后得到的AVFrame是音频的pcm数据,但是它不向视频那样表示"一帧",它可能包含很多的sample,即多次的采样数据。
播放音频,同样需要对音频数据进行格式转换,以支持音频设备的播放。音频格式转换主要通过swr_convert函数完成。转换后的音频数据可以放到一个公共缓冲区中。
播放音频使用SDL_OpenAudio函数,它需要闯入一个SDL_AudioSpec结构体用于设置播放参数,其中需要设置一个callback用于音频设备取数据时执行,因此我们需要在这个回调里向音频设备"喂"数据:
SDL_AudioSpec m_sdlAudioSpec;
auto audioCtx = m_audioDecoder.GetCodecContext();
m_sdlAudioSpec.freq = audioCtx->sample_rate; //根据你录制的PCM采样率决定
m_sdlAudioSpec.format = AUDIO_S16SYS;
m_sdlAudioSpec.channels = audioCtx->channels;
m_sdlAudioSpec.silence = 0;
m_sdlAudioSpec.samples = SDL_AUDIO_BUFFER_SIZE;
m_sdlAudioSpec.callback = &SDLVideoPlayer::ReadAudioData;
m_sdlAudioSpec.userdata = NULL;
int re = SDL_OpenAudio(&m_sdlAudioSpec, NULL);
if (re < 0)
{
std::cout << "can't open audio: " << GetErrorInfo(re);
}
else
{
//Start play audio
SDL_PauseAudio(0);
}
void SDLVideoPlayer::ReadAudioData(void *udata, Uint8 *stream, int len) {
SDL_memset(stream, 0, len);
//需要向stream中填充len长度的音频数据
...
SDL_MixAudio(stream, m_audioPcmDataBuf, len, g_volum);
}
2.5 音视频同步的设计
用两个线程分别播放音频和视频,音频的话可以直接在所设置的回调中喂数据即可,而视频则需要我们自己来控制播放速度,这就涉及到两者播放速度的统一问题。
音视频同步的基本方式就是确定一个时钟作为主时钟,播放过程中,主时钟作为同步基准,不断判断当前流的播放时间和主时钟的差异,以调节自身的播放速度。按照主时钟的不同种类,可以分为:
- 音频同步到视频,视频时钟作为主时钟;
- 视频同步到音频,音频时钟作为主时钟;
- 音视频都同步到外部时钟。
由于音频播放时往往都是送很多数据到设备缓存中,而且音频播放效果对人的敏感度更高,因此以音频时钟为主是比较合理且简单的办法。具体实现就是:
- 在每次喂音频数据的时候,记录送入数据的起始pts时间戳,表示当前音频的播放进度;
- 每次刷新图片时,记录当前图片帧的pts时间戳;
- 在记录当前音频pts的同时,根据记录的图片pts,记录两者间的延时delay;
- 刷新图片时,根据delay值判断,当前视频如果比音频快,那么一次性调整视频等待时间为正常两帧间隔加音视频之间的延时,之后将delay置0;如果音频比视频快,那么直接丢弃当前的视频帧,直到和音频时间一致。
2.6 快进和快退
快进和快退,或者一些播放器直接拖动进度条,实现思路都是一样的,即使用av_seek_frame
实现:
av_seek_frame(m_pFormatCtx, -1, pts * AV_TIME_BASE, AVSEEK_FLAG_BACKWARD);
因此关键就是获取要跳转的时间戳,这个在做音视频同步处理后,这个时间戳就很容易拿到。
2.7 SDL事件处理
对于窗口大小更改、暂停、快进快退等,都是需要交互的,这个可以通过SDL的事件机制来实现。
监听事件:
SDL_Event event;
SDL_WaitEvent(&event);
if (event.type == SDL_WINDOWEVENT) {
...
}
...
除了预定义的事件,比如窗口事件、鼠标事件、按键事件等,你也可以自己触发或定义新的事件:
SDL_Event event;
event.type = SFM_REFRESH_PIC_EVENT;
SDL_PushEvent(&event);
我这里就是使用SDL事件来通知视频播放线程来进行下一帧的播放。
以上是关于音视频学习一——FFmpeg封装的主要内容,如果未能解决你的问题,请参考以下文章