系列文章目录
文章目录
前言
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的封装,网上代码很多,这里只是结合自己的理解将音频和视频的解码实现分离的更开而已,对于小白来说可能更容易借鉴学习。