C++ FFMPEG 项目 这个项目主要是为了学习FFMPEG
完整的主要功能 是 实现对一个封装格式的视频(比如MP4)进行解码,随后获取音视频采样的数据,通过实现音视频同步之后再调用音频驱动与视频取驱动进行播放
这里做了一些简化操作
完成的功能 是 解码之后再进行编码最后再将之组合一下 形成 一个新的 封装好的mp4文件
整体的框架是这样的:
FFMPEG介绍 对于FFMPEG这个强大的的跨平台多媒体框架就不过多介绍了。咱们平时再用的时候主要是用他的命令行来进行操作。这个项目主要是使用C++结合FFMPEG对音视频进行处理。所以这里主要介绍一下C++ FFMPEG的一些API
我们先来介绍一下一般FFMPEG的一些整体的流程,如下图所示:
基本流程介绍 我们一般拿到一个音视频文件,首先需要对其进行拆分,得到音频和视频对应的包。
这里要注意,我们平时所接触到的视频都是以某一种封装格式封装压缩好的一些比如(视频H264,音频AAC等),所以我们得到的音频和视频对应的包都是编码之后的。
因此后面我们需要对其进行解码,然后得到相应的结果解码之后的视频帧(YUV数据)与音频帧(PCM数据)。
在这里我们就可以对原始的视频数据和音频数据进行处理了,比如加一些滤镜等操作。
后面我们将处理之后的原始数据再重新编码,在得到编码之后的视频包和音频包
最后我们可以把音视频包组合在一起,输出为最后的音视频文件。
所以使用C++调用FFMPEG API的时候一般都会有一些基本流程的
基本API介绍 1.打开文件 涉及到的结构: AVFormatContext
这个是管理媒体文件格式(容器格式)的核心结构体
下面是精简版本:
typedef struct AVFormatContext { const AVClass *av_class; AVInputFormat *iformat; AVOutputFormat *oformat; unsigned int nb_streams; AVStream **streams; int64_t duration; int64_t bit_rate; AVDictionary *metadata; } AVFormatContext;
这个里面存储的数据信息有很多,上面只是列出一些基本的信息
有两个比较重要的值是:unsigned int nb_streams; 与 AVStream **streams;
unsigned int nb_streams 存储的是流的数量
AVStream **streams用来存储流的数组,里面有很多流。比如视频流[AVStream1],音频流[AVStream2],字幕流[AVStream3]
我们来介绍一下 AVStream 流
流里面存储的一些视频与音频等的元数据,比如编码器参数AVCodecParameters等
我们来看一下 AVStream 一些基本结构:
typedef struct AVStream { int index; AVCodecParameters *codecpar; AVRational time_base; AVRational avg_frame_rate; AVDictionary *metadata; };
其他的就不多介绍了,主要是index和codecpar比较重要
index是流的索引,也就是AVStream **streams 数组对应的下标
AVCodecParameters 是编码器参数,主要标注一些视频或者音频等的信息,下面来看一下基本结构
typedef struct AVCodecParameters { enum AVMediaType codec_type; enum AVCodecID codec_id; int width, height; int sample_rate; int channels; uint64_t channel_layout; };
那么问题来了,流里面存储的是元数据或者是信息数据,那么视频包音频包存在那里,怎么区分这是视频包还是音频包?
首先视频包和音频包肯定不是存在流里面的,这样的话流就太大了。视频包和音频包还是存储在文件里面,只不过包里面有一个 stream_index 用来指示对应的那一个流(流里面的index),这样的话就可以区分视频包和音频包或者其他包了。(AVPacket咱们后面介绍)
这样抛析来讲是不是就理解AVFormatContext了
涉及到的API avformat_open_input
原型:
int avformat_open_input (AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options) ;
这个就是用来打开文件的,具体怎么做的就不多介绍了(我没看源码),咱们先知道其作用就好
一般在使用的时候都会把后面两个参数设成NULL除非有特殊的需求
比如在这个项目里面:
int openResult = avformat_open_input (&formatCtx,filePath,nullptr ,nullptr );if (openResult!=0 ) { cout<<"不可以打开视频文件" <<filePath<<endl; return -1 ; }
2.获取流信息 涉及到的API avformat_find_stream_info
这个函数主要是为了确定文件的流信息的完整性
在调用 avformat_open_input 后,文件的流信息可能不完整,此时调用avformat_find_stream_info 可以获取更完整的流信息
int avformat_find_stream_info (AVFormatContext *ic, AVDictionary **options) ;
说一下咱们一般会怎么用:
在打开文件之后,调用这个确保流信息完整,然后在遍历整个 AVStream 数组得到视频流和音频流的index
(这里就单处理音视频哈,其他的字幕流啥的就不处理了)
通过上面对流的介绍,那么就很容易写代码了,咱们来看一下
int vsIndex,asIndex;vsIndex = asIndex = -1 ; int findStreaResuly = avformat_find_stream_info (formatCtx,nullptr );if (findStreaResuly<0 ) { cout<<"无法找到流信息" <<endl; return -2 ; } for (int i=0 ;i<formatCtx->nb_streams;i++) { if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { vsIndex = i; break ; } } if (vsIndex==-1 ) { cout<<"无法找到视频流" <<endl; return -3 ; } for (int i=0 ;i<formatCtx->nb_streams;i++) { if (formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { asIndex = i; break ; } } if (asIndex==-1 ) { cout<<"无法找到音频流" <<endl; return -4 ; }
3.拆分视频包与音频包 涉及到的结构 AVPacket
用于存储压缩后的媒体数据(如 H.264 视频帧、AAC 音频帧等)的结构体
这个是GPT生成的,不一定对,还是要看源码的哈。这里咱们主要讲主要的东西
typedef struct AVPacket { uint8_t *data; int size; int stream_index; int64_t pts; int64_t dts; int duration; int64_t pos; int flags; #define AV_PKT_FLAG_KEY 0x0001 #define AV_PKT_FLAG_CORRUPT 0x0002 uint8_t *side_data; int side_data_elems; AVPacketSideData *side_data; int side_data_elems; void *opaque; void (*destruct)(struct AVPacket *); void *priv; AVRational time_base; } AVPacket;
主要的东西
缓冲区数据, 这个理解一下,就是包里面的数据
uint8_t *data;
int size; // 数据大小(字节数)
流索引 int stream_index;
这个就是咱们之前说的,指向对应类型的包的索引,这个很重要,可以区分这个包属于什么流(音频流视频流)
涉及到的API av_packet_alloc
一般咱们获取包都是使用从音视频文件之中解析下来的,但是解析下来的时候咱们总得有个空的来接收把。
因此就需要申请一个空的AVPacket,这个函数就是用来干这件事的
AVPacket *av_packet_alloc (void ) ;
av_read_frame
这个函数是用来读取包的,这个我觉得有点变态了,读包的函数名字是读帧,当时我还搞混了。
int av_read_frame (AVFormatContext *s, AVPacket *pkt) ;
av_packet_ref
复制包的函数,很好理解
int av_packet_ref (AVPacket *dst, const AVPacket *src) ;
av_packet_unref
注意哈,咱们调用av_read_frame之后获取一个包,做了处理之后,要清空一下包里面的东西,以便于下一次再存储新的包,这个函数就是这么用的,就是清空包
void av_packet_unref (AVPacket *pkt) ;
av_packet_free
同理,最后咱们用完了这个临时的包,后面不需要再用了,就得释放掉咱们申请的空间,于是就有了av_packet_free
void av_packet_free (AVPacket **pkt) ;
那么到这里咱们来看一下拆分整个包的过程(这个项目是使用一个线程来进行操作的,所以会涉及到一些线程的操作,大家看一看,能理解就好)
AVPacket* packet = av_packet_alloc (); while (av_read_frame (formatCtx, packet) >= 0 ) { if (packet->stream_index == vsIndex) { lock_guard<mutex> lock (videoMtx) ; AVPacket* videoPkt = av_packet_alloc (); av_packet_ref (videoPkt, packet); videoPacketQueue.push (videoPkt); videoCV.notify_one (); } else if (packet->stream_index == asIndex) { lock_guard<mutex> lock (audioMtx); AVPacket* audioPkt = av_packet_alloc (); av_packet_ref (audioPkt, packet); audioPacketQueue.push (audioPkt); audioCV.notify_one (); } av_packet_unref (packet); } av_packet_free (&packet); av_packet_free (&packet); doneVideoReading = true ; videoCV.notify_all (); doneAudioReading = true ; audioCV.notify_all ();
这里留个小问题哈,为啥要复制一下包av_packet_ref
4.解码包 这里要注意哈,咱们得到的包是编码之后的结果,到这里才是解码的过程
这里以视频为例哈,音频是同样的操作
涉及到的结构 AVCodecParameters
既然要解码,咱们就得获取一下编解码器 的参数,获取参数之后才能做解码操作
这个咱们前面在介绍流的时候已经做了介绍,现在再拿下来说一下
typedef struct AVCodecParameters { enum AVMediaType codec_type; enum AVCodecID codec_id; int width, height; int sample_rate; int channels; uint64_t channel_layout; };
这里要特别注意哈,这里是一个结构体,里面存储的是编解码器 的参数,不是所谓的编解码器
注意这里比较重要的是 enum AVCodecID codec_id; // 编解码器 ID
它是编解码器 对应的id,我们点进去可以看到,这是一个枚举类型的数据,里面有好多比那解码器的类型
enum AVCodecID { AV_CODEC_ID_NONE, AV_CODEC_ID_MPEG1VIDEO, AV_CODEC_ID_MPEG2VIDEO, AV_CODEC_ID_H261, AV_CODEC_ID_H263, AV_CODEC_ID_FIRST_AUDIO = 0x10000 , AV_CODEC_ID_PCM_S16LE = 0x10000 , AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_U16LE, }
AVCodec
这个才是相应的编解码器 ,为什么我老是加粗这个编解码器 ,因为AVCodec这个结构既可以表示编码器又可以表示解码器!!!这里一定要注意,非常非常容易搞混乱
typedef struct AVCodec { const char *name; const char *long_name; enum AVMediaType type; enum AVCodecID id; const AVRational *supported_framerates; const enum AVPixelFormat *pix_fmts; const int *supported_samplerates; const enum AVSampleFormat *sample_fmts; const uint64_t *channel_layouts; int priv_data_size; } AVCodec;
上面是一些常规的内容,区分编码器与解码器主要靠着下面两个:
int (*encode2)(struct AVCodecContext *avctx, struct AVPacket *avpkt, const struct AVFrame *frame, int *got_packet_ptr); int (*decode)(struct AVCodecContext *avctx, void *outdata, int *got_frame_ptr, struct AVPacket *avpkt);
if (codec->encode2 != NULL ) { } if (codec->decode != NULL ) { }
注意哈,这里只是简单的区分,其实在用的时候总得初始化吧,初始化的时候会针对编码器和解码器会调用不同的函数来做
比如:
AVCodec *decoder = avcodec_find_decoder (AV_CODEC_ID_H264); AVCodec *encoder = avcodec_find_encoder (AV_CODEC_ID_H264);
AVCodecContext
AVCodec是编解码器 那么这个AVCodecContext就是*编解码器的上下文
这是编解码器 的上下文,保存了编解码过程中的所有状态信息:
typedef struct AVCodecContext { enum AVMediaType codec_type; const struct AVCodec *codec; enum AVCodecID codec_id; int bit_rate; int width, height; enum AVPixelFormat pix_fmt; int sample_rate; int channels; enum AVSampleFormat sample_fmt; uint64_t channel_layout; } AVCodecContext;
注意区分一下AVCodec与AVCodecContext哈,咱们来对比一下
你看上面的AVCodec的结构,一般里面是不是有很多的指针也就是数组的类型,这里表示这个编解码器 锁支持的,举个简单的例子,比如对于与编码格式 支持 [A,B,C,D…G]编码格式
VCodecContext里面一般都是非常具体的值,比如还用那个例子,编码格式所对应的状态是A 具体的A
因此,咱们在后面进行编解码操作的时候使用的都是VCodecContext
AVFrame
咱们已经介绍了AVPacket,也就是咱们上一步所获取的压缩状态的数据包,那么AVFrame就是解码之后的
typedef struct AVFrame { uint8_t *data[AV_NUM_DATA_POINTERS]; int linesize[AV_NUM_DATA_POINTERS]; int width, height; int nb_samples; int format; int64_t pts; int64_t duration; AVBufferRef *buf[AV_NUM_DATA_POINTERS]; AVBufferRef *hw_frames_ctx; AVDictionary *metadata; } AVFrame;
这里咱们就先知道AVFrame就是解码之后的数据就好了,里面涉及到一些平面存储的概念这个感兴趣的可以取搜索一下学习学习
linesize[0 ] = 1280 ; linesize[1 ] = 640 ; linesize[2 ] = 640 ;
涉及到的API 首先要获取编解码器 的参数吧(这里不涉及API哈,别激动)就是从咱们刚得到的流中拿哈
这里以视频为例哈
videoCodecParams = formatCtx->streams[vsIndex]->codecpar; if (!videoCodecParams) { cout<<"无法获取解码器参数-视频" <<endl; return -5 ; }
emmm就这么简单,哈哈哈哈
avcodec_find_decoder
那么获取之后就要创建一个编解码器 了,也就是一个 AVCodec 这里主要是解码,所以用decoder
AVCodec *avcodec_find_decoder (enum AVCodecID id) ;
avcodec_find_decoder 只是创建的一种哈,还有很多其他的函数比如
AVCodec *avcodec_find_decoder_by_name (const char *name) ;
那么这里咱们再和之前获取的videoCodecParams结合再一起,那就是
AVCodec* videoCodec = avcodec_find_decoder (videoCodecParams->codec_id); if (!videoCodec) { cout<<"无法识别解码器-视频" <<endl; return -7 ; }
记得回想一下videoCodecParams->codec_id是什么,上面介绍了
avcodec_alloc_context3
那么创建完了之后就需要 创建AVCodecContext 保存咱们上面所创建的解码器 (这里不是编解码器了哈,上面调用的是avcodec_find_decoder,创建的是解码器)上下文了
AVCodecContext *avcodec_alloc_context3 (const AVCodec *codec) ;
AVCodecContext* videoCodecCtx = avcodec_alloc_context3 (videoCodec);
avcodec_parameters_to_context
注意哈,上面创建只是创建一个空壳子,意味着videoCodecCtx可以保存videoCodec的状态,这里就需要给他赋值了,那么想到状态是不是就想到了之前提到的AVCodecParameters
没错,看看下面的函数原型
int avcodec_parameters_to_context (AVCodecContext *codec_ctx, const AVCodecParameters *codec_par) ;
所以咱们就这么使用:
int P2CResultVi = avcodec_parameters_to_context (videoCodecCtx, videoCodecParams);if (P2CResultVi<0 ) { cout<<"无法将解码器参数复制到解码器上下文之中-视频" <<endl; return -11 ; }
avcodec_open2
OK,到这里就把所有的东西都初始化好了,最后一步就是打开解码器
int avcodec_open2 (AVCodecContext *avctx, AVCodec *codec, AVDictionary **options) ;
int openAvcodecResultVi = avcodec_open2 (videoCodecCtx, videoCodec, nullptr );if (openAvcodecResultVi<0 ) { cout<<"无法打开解码器-视频" <<endl; return -13 ; }
av_frame_alloc
仿照之前的av_packet_alloc,这里就不过多介绍了
AVFrame *av_frame_alloc (void ) ;
同样的还有 av_frame_unref 与 av_frame_free
void av_frame_unref (AVFrame *frame) ;
void av_frame_free (AVFrame **frame) ;
OK,下面是两个非常关键的函数
avcodec_send_packet
第一个是 把文件packet放进解码器里面解码,然后解码
int avcodec_send_packet (AVCodecContext *avctx, const AVPacket *avpkt) ;
avcodec_send_packet (videoCodecCtx, packet)
avcodec_receive_frame
解码之后,咱们得拿一下所得到的解码帧
int avcodec_receive_frame (AVCodecContext *avctx, AVFrame *frame) ;
avcodec_receive_frame (videoCodecCtx, frame)
这里就要注意理解一下哈,avcodec_send_packet解码之后会放入一个缓冲区里面,avcodec_receive_frame会来取
avcodec_receive_frame的返回值里面会有一个AVERROR(EAGAIN):解码器需要更多输入数据才能产生输出,需要先调用 avcodec_send_packet。这里做一下简单的介绍
关键帧(I帧):通常一个AVPacket对应一个AVFrame。
B/P帧:可能需要参考前后帧,解码器可能缓存多个AVPacket后才输出AVFrame。
分包处理:某些编码格式(如H.264)可能将一个帧拆分到多个AVPacket中。
一般的使用的格式:
while (condition){ avcodec_send_packet (codec_ctx, packet); while (avcodec_receive_frame (codec_ctx, frame) == 0 ) { } }
这里再说一下为什么会有第二个循环,其实上面的已经可以解释了,这里再说一下音频
音频解码的典型场景,音频帧打包:一个AVPacket可能包含多个音频帧(如AAC每个包含1024帧)。解码时需循环调用avcodec_receive_frame()直到返回AVERROR(EAGAIN),表示需要新输入。
到这里解码就结束了,那么来看一下这个项目里面完整的处理流程(初始化就不说了,这里还是涉及到一些线程安全的操作哈,记得理解一下)
void processVideoPackets () { AVFrame* frame = av_frame_alloc (); AVPacket* packet = av_packet_alloc (); while (!doneVideoReading || !videoPacketQueue.empty ()) { std::unique_lock<std::mutex> lock (videoMtx) ; videoCV.wait (lock, [] { return !videoPacketQueue.empty () || doneVideoReading; }); if (!videoPacketQueue.empty ()) { *packet = *videoPacketQueue.front (); videoPacketQueue.pop (); lock.unlock (); if (avcodec_send_packet (videoCodecCtx, packet) == 0 ) { while (avcodec_receive_frame (videoCodecCtx, frame) == 0 ){ AVFrame* frameCopy = av_frame_alloc (); if (av_frame_ref (frameCopy, frame) != 0 ) { av_frame_free (&frameCopy); cout<<"视频 复制失败" <<endl; continue ; } unique_lock<mutex> frameLock (videoFrameMtx) ; videoFrameCV.wait (frameLock, [&] { return !videoFrameBuffer.full (); }); videoFrameBuffer.push (frameCopy); videoFrameCV.notify_all (); } } av_frame_unref (frame); av_packet_unref (packet); } } doneVideoFrameReading = true ; videoFrameCV.notify_all (); av_frame_free (&frame); av_packet_free (&packet); cout<<"--线程解析视频包完成!--" <<endl; }
5.处理解码之后的数据 这里的操作有很多,主要是为解码之后的数据添加滤镜,比如旋转,倍速,变声等操作都是在这里完成的
你看上面的代码我是不是就只是单纯的拿到解码之后的数据,然后放在一个环形缓冲区里面,其实这里放在环形缓冲区之前还可以做我上面所说的加滤镜的操作,然后再放进去。
这里太多了,就不一一介绍了,感兴趣的可以搜一下,学习一下,很有意思的。
6.再编码回去 哈哈哈哈,好不容易做了解码这里咋又编码回去了
我们看一下前面的 FFMPEG介绍 里面的那个图,我们可以看到,再解码之后我们再加上一些同步的机制就可以分别调用视频和音频的驱动进行播放了
这里之前做过安卓的 native C++的项目 是这样处理的
但是我们这里主要是工作是 解码之后 做出处理(比如添加滤镜等功能) 然后再合成最后输出的mp4
所以我们还得再编码回去
这里编码就会简单了
因为编码也是用的 AVCodec 这个东西咱们之前已经介绍了,这里就不多说了
这里主要说一下编码器的初始化的操作,这里使用不同的方式进行初始化
之前咱们做解码的时候直接使用的是AVCodecParameters里面参数(解码器这样处理是必须的,毕竟不使用原始音视频文件之中的参数进行初始化的话就无法解码了)
咱们再编码的时候就可以自己定制编码器的参数了(这里需要有一些专业的视频编码的一些知识,我也没有这个知识,我写的时候是GPT生成了,哈哈哈哈)
这边要一定要搞懂 AVCodec 与 AVCodecContext的关系哈,一定要清楚,不然就会糊里糊涂的,用的时候根本搞不明白
涉及到的结构 AVCodec
看前面的介绍
AVCodecContext
看前面的介绍
涉及到的API avcodec_find_encoder_by_name
这里咱们前面其实也介绍了 avcodec_find_decoder _by_name
这个和那个是一样的,只不过这里是 avcodec_find_encoder _by_name
const AVCodec *avcodec_find_encoder_by_name (const char *name) ;
可以通过这个来获取咱们指定的编码器格式
const AVCodec* videoEncoder = avcodec_find_encoder_by_name ("mpeg4" );
avcodec_alloc_context3
后面就是创建编器的上下文状态(AVCodecContext)了
这个函数和之前的一样,就不介绍了哈
AVCodecContext* videoEncoderCtx = avcodec_alloc_context3 (videoEncoder);
这里要注意了哈,之前 咱们是用的 avcodec_parameters_to_context 给 AVCodecContext赋值的
这里可以回头到前面看看,是咋处理的
在这里咱们需要手动赋值
videoEncoderCtx->width = 1024 ; videoEncoderCtx->height = 436 ; videoEncoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; videoEncoderCtx->time_base = {1 , int (25 * playSpeed)}; videoEncoderCtx->bit_rate = 2500000 ; videoEncoderCtx->gop_size = 12 ; videoEncoderCtx->max_b_frames = 10 ; videoEncoderCtx->level = 5 ;
说实话有些参说我也搞不动,我这个是GPT生成的
大概意思就是把那些重要的参数给赋值了,后面就直接用就好了
avcodec_open2
后面就是打开编码器了,这个前面已经说过了,就不再赘述了。
avcodec_send_frame
咱们之前已经说了解码的时候的 avcodec_send_packet 函数
packet是封住之后的包,那么咱们现在需要对原始数据frame做处理,自然就是 avcodec_send_frame
这个用法和avcodec_send_packet是一样的
int avcodec_send_frame (AVCodecContext *avctx, const AVFrame *frame) ;
avcodec_receive_packet
同理哈,就和前面的一样, avcodec_send_packet 与 avcodec_receive_frame 配套出现
这里就是 avcodec_send_frame 与 avcodec_receive_packet 配套出现
int avcodec_receive_packet (AVCodecContext *avctx, AVPacket *avpkt) ;
这里做一下说明哈,这里面的一些处理的操作基本上都是配套出现的,就比如咱们上面的所说的
avcodec_send_packet 与 avcodec_receive_frame
avcodec_send_frame 与 avcodec_receive_packet
还有其他的操作,比如说 滤镜 也是先把帧send进入,然后再receive一下
再比如音频的重定向 也是这样的。
那么咱们再来看一下视频部分的整体操作(同样的哈,我这里面有一些线程的操作,清稍微忽略一下哈)
static int64_t frame_pts = 0 ;while (!doneVideoFrameReading||!videoFrameBuffer.empty ()){ unique_lock<mutex> frameLock (videoFrameMtx) ; videoFrameCV.wait (frameLock, [&] {return !videoFrameBuffer.empty () || doneVideoFrameReading;}); if (!videoFrameBuffer.empty ()) { AVFrame* frame = videoFrameBuffer.front (); videoFrameBuffer.pop (); frame->pts = frame_pts++; frameLock.unlock (); videoFrameCV.notify_all (); videoEncoderCtxMtx.lock (); if (avcodec_send_frame (videoEncoderCtx,frame)<0 ){ av_frame_free (&frame); cout<<"视频,发送帧到编码器失败!" <<endl; continue ; } videoEncoderCtxMtx.unlock (); av_frame_free (&frame); AVPacket* encodedPacket = av_packet_alloc (); videoEncoderCtxMtx.lock (); int receivePacketResult = avcodec_receive_packet (videoEncoderCtx, encodedPacket); videoEncoderCtxMtx.unlock (); while (receivePacketResult == 0 ) { AVPacket* pkt = av_packet_clone (encodedPacket); unique_lock<mutex> encLock (videoFrameEncoderMtx) ; videoFrameEncoderQueue.push (pkt); encLock.unlock (); videoFrameEncoderCV.notify_all (); av_packet_unref (encodedPacket); videoEncoderCtxMtx.lock (); receivePacketResult = avcodec_receive_packet (videoEncoderCtx, encodedPacket); videoEncoderCtxMtx.unlock (); } av_packet_free (&encodedPacket); } }
有一个点忘记说了,这里的frame_pts很重要,这是时间戳,一般会有编码时间戳和显示时间戳,这里要设置好,看这一行!frame->pts = frame_pts++; //!!!很关键的pts设置
还有一点,音频在编码的时候如果像咱们这样 自己设置了编码器的参数,就需要有一个重定向的过程。
这个重定向的过程就不详细讲了。感兴趣的可以取搜一下
7.最后就是封装在一起了 这个过程就很简单了,理解了之前打开的例子,这个就好理解了
唯一一点需要注意的就是音视频的同步的问题
涉及到的结构 没有新的结构,上面都介绍过了。
涉及到的API avformat_alloc_output_context2
就是打开一个输出的AVFormatContext
int avformat_alloc_output_context2 (AVFormatContext **ctx, const AVOutputFormat *oformat, const char *format_name, const char *filename) ;
没啥复杂的用法
AVFormatContext* outFormatCtx = nullptr ; avformat_alloc_output_context2 (&outFormatCtx, nullptr , nullptr , outFilePath);if (!outFormatCtx) { cout << "无法创建输出上下文" << endl; return ; }
是不是很简单
后面就需要创建流了,咱们前面也说了,AVFormatContext有很多的流信息,现在咱们有个空的AVFormatContext就需要先把流信息填进取。
咱们这里只需要视频流和音频流就可以了
avformat_new_stream
AVStream *avformat_new_stream (AVFormatContext *s, const AVCodec *c) ;
相当于new一个
基本用法也很简单
AVStream* videoStream = avformat_new_stream (outFormatCtx, nullptr );
avcodec_parameters_from_context
注意哈,咱们用avformat_new_stream创建了一个流,并且放进了outFormatCtx里面,但是这个流是啥流还不知道哇,因此咱们需要给他的codecpar赋值(这里想一下前面是如何判断是啥流的的哈)
就是把咱们的编码器的参数给放进去哈
AVStream *avformat_new_stream (AVFormatContext *s, const AVCodec *c) ;
基本用法:
avcodec_parameters_from_context (videoStream->codecpar, videoEncoderCtx);
那么咱们看一下完整的
AVStream* videoStream = avformat_new_stream (outFormatCtx, nullptr ); avcodec_parameters_from_context (videoStream->codecpar, videoEncoderCtx);videoStream->time_base = videoEncoderCtx->time_base; AVStream* audioStream = avformat_new_stream (outFormatCtx, nullptr ); avcodec_parameters_from_context (audioStream->codecpar, audioEncoderCtx);audioStream->time_base = audioEncoderCtx->time_base;
avio_open
然后就需要打开文件了
if (avio_open (&outFormatCtx->pb, outFilePath, AVIO_FLAG_WRITE) < 0 ) { cout<<"无法打开输出文件" <<endl; return ; }
avformat_write_header
这些都是一些固定的操作了
int avformat_write_header (AVFormatContext *s, AVDictionary **options) ;
if (avformat_write_header (outFormatCtx, nullptr ) < 0 ) { cout << "无法写入文件头" << endl; return ; }
av_packet_rescale_ts
咱们拿到一个包之后还不能直接写,得先调用av_packet_rescale_ts转化一下时间基,就是把原来的时间其转化到对应流的时间基下,这样才能保证正常的播放那个
void av_packet_rescale_ts (AVPacket *pkt, AVRational tb_src, AVRational tb_dst) ;
咱们来看一下整体的流程
*videoPacket = *videoFrameEncoderQueue.front (); videoFrameEncoderQueue.pop (); av_packet_rescale_ts (videoPacket, videoEncoderCtx->time_base, videoStream->time_base);videoPacket->stream_index = videoStream->index; packetToWrite = videoPacket; streamIndex = videoStream->index;
其他的都不用多看哈,这里就两行比较重要,后面的都是逻辑处理的
1.转化时间基 av_packet_rescale_ts(videoPacket, videoEncoderCtx->time_base, videoStream->time_base);
2.设置videoPacket的stream_index,这个非常重要,要不然不知道他是啥流
av_interleaved_write_frame
这里就是写的了
int av_interleaved_write_frame (AVFormatContext *s, AVPacket *pkt) ;
注意哈,这里是交叉写入。这里可以搜一下为啥要交叉写入。
同步的基本操作 后面就没啥了,但是这边还有一个比较重要的就是音频和视频的同步
如果不进行同步的话,就会发现 视频和音频各播各的
同步的思路:
获得到的视频和音频的 packet 先把两个包转化到同一个时间基下
比较一下那个时间基小,先把小的给写进取
(你品,你细品)
整体的代码(涉及的到线程的操作):
void videoOutMP4File (const char * outFilePath) { videoEncoderCtxMtx.lock (); audioEncoderCtxMtx.lock (); AVFormatContext* outFormatCtx = nullptr ; avformat_alloc_output_context2 (&outFormatCtx, nullptr , nullptr , outFilePath); if (!outFormatCtx) { audioEncoderCtxMtx.unlock (); videoEncoderCtxMtx.unlock (); cout << "无法创建输出上下文" << endl; return ; } AVStream* videoStream = avformat_new_stream (outFormatCtx, nullptr ); avcodec_parameters_from_context (videoStream->codecpar, videoEncoderCtx); videoStream->time_base = videoEncoderCtx->time_base; AVStream* audioStream = avformat_new_stream (outFormatCtx, nullptr ); avcodec_parameters_from_context (audioStream->codecpar, audioEncoderCtx); audioStream->time_base = audioEncoderCtx->time_base; if (avio_open (&outFormatCtx->pb, outFilePath, AVIO_FLAG_WRITE) < 0 ) { cout<<"无法打开输出文件" <<endl; audioEncoderCtxMtx.unlock (); videoEncoderCtxMtx.unlock (); avformat_free_context (outFormatCtx); return ; } if (avformat_write_header (outFormatCtx, nullptr ) < 0 ) { cout << "无法写入文件头" << endl; audioEncoderCtxMtx.unlock (); videoEncoderCtxMtx.unlock (); avio_closep (&outFormatCtx->pb); avformat_free_context (outFormatCtx); return ; } audioEncoderCtxMtx.unlock (); videoEncoderCtxMtx.unlock (); AVPacket* videoPacket = av_packet_alloc (); AVPacket* audioPacket = av_packet_alloc (); bool videoDone = videoFrameEncoderQueue.empty () && doneVideoFrameEncoderReading; bool audioDone = audioFrameEncoderQueue.empty () && doneAudioFrameEncoderReading; while (!videoDone||!audioDone) { AVPacket* packetToWrite = nullptr ; int streamIndex = -1 ; { unique_lock<mutex> videoQueue (videoFrameEncoderMtx) ; unique_lock<mutex> audioQueue (audioFrameEncoderMtx) ; videoFrameEncoderCV.wait (videoQueue,[]{return !videoFrameEncoderQueue.empty ()||doneVideoFrameEncoderReading; }); audioFrameEncoderCV.wait (audioQueue,[]{return !audioFrameEncoderQueue.empty ()||doneAudioFrameEncoderReading; }); int64_t video_pts = (!videoFrameEncoderQueue.empty ()) ? av_rescale_q (videoFrameEncoderQueue.front ()->pts,videoEncoderCtx->time_base,videoStream->time_base):INT64_MAX; int64_t audio_pts = (!audioFrameEncoderQueue.empty ()) ? av_rescale_q (audioFrameEncoderQueue.front ()->pts,audioEncoderCtx->time_base,audioStream->time_base):INT64_MAX; if (!videoFrameEncoderQueue.empty ()&& (audioFrameEncoderQueue.empty ()|| av_compare_ts (video_pts,videoStream->time_base,audio_pts,audioStream->time_base)<=0 )) { *videoPacket = *videoFrameEncoderQueue.front (); videoFrameEncoderQueue.pop (); av_packet_rescale_ts (videoPacket, videoEncoderCtx->time_base, videoStream->time_base); videoPacket->stream_index = videoStream->index; packetToWrite = videoPacket; streamIndex = videoStream->index; } else if (!audioFrameEncoderQueue.empty ()) { *audioPacket = *audioFrameEncoderQueue.front (); audioFrameEncoderQueue.pop (); av_packet_rescale_ts (audioPacket, audioEncoderCtx->time_base, audioStream->time_base); audioPacket->stream_index = audioStream->index; packetToWrite = audioPacket; streamIndex = audioStream->index; } videoDone = videoFrameEncoderQueue.empty () && doneVideoFrameEncoderReading; audioDone = audioFrameEncoderQueue.empty () && doneAudioFrameEncoderReading; audioQueue.unlock (); videoQueue.unlock (); } if (packetToWrite) { if (av_interleaved_write_frame (outFormatCtx, packetToWrite) < 0 ) { cout<<"写入包失败: " <<streamIndex<<endl; } av_packet_unref (packetToWrite); } } videoEncoderCtxMtx.lock (); avcodec_send_frame (videoEncoderCtx, nullptr ); videoEncoderCtxMtx.unlock (); audioEncoderCtxMtx.lock (); avcodec_send_frame (audioEncoderCtx, nullptr ); audioEncoderCtxMtx.unlock (); av_packet_free (&videoPacket); av_packet_free (&audioPacket); av_write_trailer (outFormatCtx); avio_closep (&outFormatCtx->pb); avformat_free_context (outFormatCtx); cout<<"--线程写入MP4完成!--" <<endl; }
项目介绍 说实话有些不想写了,前面已经把所有的基本的流程给介绍过了,这里最多补充一下细节
运行起来 地址:https://github.com/zqzhang2023/zzqStudy/tree/main/project/1_ffmpeg
环境:
linux ubuntu 20.04
C++ 11
ffmpeg(这个有没有都可以)
cmake
进入根目录文件夹
然后
cd build
cmake ..
make
./transcode
(要处理的是视频文件在resources里面 1.mp4 输出为 2.mp4)
文件说明 transcode.cpp 为入口程序
lib/include/manageAll.h 为管理全局变量和函数以及参数的头文件
src/initGlobalVariable.cpp 为全局变量以及参数的初始化和释放
src/productionPackage.cpp 实现了拆分音视频为音频包和视频包的线程
src/videoAnalysis.cpp 实现了对视频包进行解码成视频帧的线程
src/videoFrameEncoding.cpp 实现了对视频帧重新编码成为视频包的线程
src/audioAnalysis.cpp 实现了对音频包进行解码成音频帧的线程
src/audioFrameEncoding.cpp 实现了对音频帧重新编码成为音频包的线程
src/videoOutMP4File.cpp 实现了重新整合音频包和视频包为MP4文件的线程
lib/include/queue.h 队列的声明与实现
lib/include/circularbuffer.h 环形缓冲区的声明与实现
ffmpeg 库的编译 就是把ffmpeg的库编译成静态库,然后后面咱们使用的话就可以直接用这个库,不需要安装ffmpeg
这里很方便,比如我把我写的这些东西复制给其他人,他们不需要再配置ffmpeg的环境了,可以直接运行起来
(我现在的机子里面就没装ffmpeg,也可以正常运行)
方法:
1.把源码下下来 https://ffmpeg.org/download.html
找一下适合的版本号就好
2.进入根目录创建一个build.sh的空文件
PREFIX="/home/ubuntu2204/ffmpge/out_one" build () { make clean ./configure \ --disable-shared \ --enable-static \ --disable-doc \ --disable-programs \ --enable-avcodec \ --enable-avfilter \ --enable-avformat \ --enable-avutil \ --enable-swscale \ --enable-swresample \ --prefix="$PREFIX " \ --extra-cflags="-fPIC" \ --extra-ldflags="-Wl,-rpath='\$ORIGIN'" make -j$(nproc ) make install gcc -shared -o libffmpeg.so \ -Wl,--whole-archive \ "$PREFIX /lib/libavcodec.a" \ "$PREFIX /lib/libavfilter.a" \ "$PREFIX /lib/libavformat.a" \ "$PREFIX /lib/libswscale.a" \ "$PREFIX /lib/libswresample.a" \ "$PREFIX /lib/libavutil.a" \ -Wl,--no-whole-archive \ -Wl,-Bsymbolic -Wl,--no-undefined \ -lm -lz -pthread -ldl } build
PREFIX 是 输出的目录
里面会有 一个 libffmpeg.so 的文件 还有 一个include文件夹,记得把这些都复制过来
为什么要有include 头文件,因为libffmpeg.so相当于对头文件的实现,必须得有include文件咱们才能在代码里面用
未完待续 先写到这吧,一天复习了整个项目的流程,实在不想写了。等下次有时间再去写吧。不说了背八股去了