音视频处理之FFmpeg封装格式20180510

时间:2022-08-29 07:22:31

一、FFMPEG的封装格式转换器(无编解码)

1.封装格式转换

所谓的封装格式转换,就是在AVI,FLV,MKV,MP4这些格式之间转换(对应.avi,.flv,.mkv,.mp4文件)。

需要注意的是,本程序并不进行视音频的编码和解码工作。而是直接将视音频压缩码流从一种封装格式文件中获取出来然后打包成另外一种封装格式的文件。

本程序的工作原理如下图1所示:

音视频处理之FFmpeg封装格式20180510

由图可见,本程序并不进行视频和音频的编解码工作,因此本程序和普通的转码软件相比,有以下两个特点:

处理速度极快。视音频编解码算法十分复杂,占据了转码的绝大部分时间。因为不需要进行视音频的编码和解码,所以节约了大量的时间。

视音频质量无损。因为不需要进行视音频的编码和解码,所以不会有视音频的压缩损伤。

2.基于FFmpeg的Remuxer的流程图

下面附上基于FFmpeg的Remuxer的流程图。图2中使用浅红色标出了关键的数据结构,浅蓝色标出了输出视频数据的函数。

可见成个程序包含了对两个文件的处理:读取输入文件(位于左边)和写入输出文件(位于右边)。中间使用了一个avcodec_copy_context()拷贝输入的AVCodecContext到输出的AVCodecContext。

音视频处理之FFmpeg封装格式20180510

简单介绍一下流程中关键函数的意义:

输入文件操作:

avformat_open_input():打开输入文件,初始化输入视频码流的AVFormatContext。

av_read_frame():从输入文件中读取一个AVPacket。

输出文件操作:

avformat_alloc_output_context2():初始化输出视频码流的AVFormatContext。

avformat_new_stream():创建输出码流的AVStream。

avcodec_copy_context():拷贝输入视频码流的AVCodecContex的数值t到输出视频的AVCodecContext。

avio_open():打开输出文件。

avformat_write_header():写文件头(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

av_interleaved_write_frame():将AVPacket(存储视频压缩码流数据)写入文件。

av_write_trailer():写文件尾(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

二、FFmpegRemuxer代码

基于FFmpeg的封装格式转换器,取了个名字称为FFmpegRemuxer

主要是FFmpegRemuxer.cpp文件,代码如下(基本上每一行都有注释):

 /*****************************************************************************
* Copyright (C) 2017-2020 Hanson Yu All rights reserved.
------------------------------------------------------------------------------
* File Module : FFmpegRemuxer.cpp
* Description : FFmpegRemuxer Demo 输出结果:
Input #0, flv, from 'cuc_ieschool1.flv':
Metadata:
metadatacreator : iku
hasKeyframes : true
hasVideo : true
hasAudio : true
hasMetadata : true
canSeekToEnd : false
datasize : 932906
videosize : 787866
audiosize : 140052
lasttimestamp : 34
lastkeyframetimestamp: 30
lastkeyframelocation: 886498
encoder : Lavf55.19.104
Duration: 00:00:34.20, start: 0.042000, bitrate: 394 kb/s
Stream #0:0: Video: h264 (High), yuv420p, 512x288 [SAR 1:1 DAR 16:9], 15.17 fps, 15 tbr, 1k tbn, 30 tbc
Stream #0:1: Audio: mp3, 44100 Hz, stereo, s16p, 128 kb/s
Output #0, mp4, to 'cuc_ieschool1.mp4':
Stream #0:0: Video: h264, yuv420p, 512x288 [SAR 1:1 DAR 16:9], q=2-31, 90k tbn, 30 tbc
Stream #0:1: Audio: mp3, 44100 Hz, stereo, s16p, 128 kb/s
Write 0 frames to output file
Write 1 frames to output file
Write 2 frames to output file
Write 3 frames to output file
.
.
. * Created : 2017.09.21.
* Author : Yu Weifeng
* Function List :
* Last Modified :
* History :
* Modify Date Version Author Modification
* -----------------------------------------------
* 2017/09/21 V1.0.0 Yu Weifeng Created
******************************************************************************/
#include <stdio.h> /*
__STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS are a workaround to allow C++ programs to use stdint.h
macros specified in the C99 standard that aren't in the C++ standard. The macros, such as UINT8_MAX, INT64_MIN,
and INT32_C() may be defined already in C++ applications in other ways. To allow the user to decide
if they want the macros defined as C99 does, many implementations require that __STDC_LIMIT_MACROS
and __STDC_CONSTANT_MACROS be defined before stdint.h is included. This isn't part of the C++ standard, but it has been adopted by more than one implementation.
*/
#define __STDC_CONSTANT_MACROS #ifdef _WIN32//Windows
extern "C"
{
#include "libavformat/avformat.h"
};
#else//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavformat/avformat.h>
#ifdef __cplusplus
};
#endif
#endif /*****************************************************************************
-Fuction : main
-Description : main
-Input :
-Output :
-Return :
* Modify Date Version Author Modification
* -----------------------------------------------
* 2017/09/21 V1.0.0 Yu Weifeng Created
******************************************************************************/
int main(int argc, char* argv[])
{
AVOutputFormat * ptOutputFormat = NULL;//The output container format.Muxing only, must be set by the caller before avformat_write_header().
AVFormatContext * ptInFormatContext = NULL;//输入文件的封装格式上下文,内部包含所有的视频信息
AVFormatContext * ptOutFormatContext = NULL;//输出文件的封装格式上下文,内部包含所有的视频信息
AVPacket tOutPacket ={};//存储一帧压缩编码数据给输出文件
const char * strInFileName=NULL, * strOutFileName = NULL;//输入文件名和输出文件名
int iRet, i;
int iFrameCount = ;//输出的帧个数
AVStream * ptInStream=NULL,* ptOutStream=NULL;//输入音视频流和输出音视频流 if(argc!=)//argc包括argv[0]也就是程序名称
{
printf("Usage:%s InputFileURL OutputFileURL\r\n",argv[]);
printf("For example:\r\n");
printf("%s InputFile.flv OutputFile.mp4\r\n",argv[]);
return -;
}
strInFileName = argv[];//Input file URL
strOutFileName = argv[];//Output file URL av_register_all();//注册FFmpeg所有组件 /*------------Input------------*/
if ((iRet = avformat_open_input(&ptInFormatContext, strInFileName, , )) < )
{//打开输入视频文件
printf("Could not open input file\r\n");
}
else
{
if ((iRet = avformat_find_stream_info(ptInFormatContext, )) < )
{//获取视频文件信息
printf("Failed to find input stream information\r\n");
}
else
{
av_dump_format(ptInFormatContext, , strInFileName, );//手工调试的函数,内部是log,输出相关的格式信息到log里面 /*------------Output------------*/ /*初始化一个用于输出的AVFormatContext结构体
*ctx:函数调用成功之后创建的AVFormatContext结构体。
*oformat:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,
可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。
PS:使用该参数需要自己手动获取AVOutputFormat,相对于使用后两个参数来说要麻烦一些。
*format_name:指定输出格式的名称。根据格式名称,FFmpeg会推测输出格式。输出格式可以是“flv”,“mkv”等等。
*filename:指定输出文件的名称。根据文件名称,FFmpeg会推测输出格式。文件名称可以是“xx.flv”,“yy.mkv”等等。
函数执行成功的话,其返回值大于等于0
*/
avformat_alloc_output_context2(&ptOutFormatContext, NULL, NULL, strOutFileName);
if (!ptOutFormatContext)
{
printf("Could not create output context\r\n");
iRet = AVERROR_UNKNOWN;
}
else
{
ptOutputFormat = ptOutFormatContext->oformat;
for (i = ; i < ptInFormatContext->nb_streams; i++)
{
//Create output AVStream according to input AVStream
ptInStream = ptInFormatContext->streams[i];
ptOutStream = avformat_new_stream(ptOutFormatContext, ptInStream->codec->codec);//给ptOutFormatContext中的流数组streams中的
if (!ptOutStream) //一条流(数组中的元素)分配空间,也正是由于这里分配了空间,后续操作直接拷贝编码数据(pkt)就可以了。
{
printf("Failed allocating output stream\r\\n");
iRet = AVERROR_UNKNOWN;
break;
}
else
{
if (avcodec_copy_context(ptOutStream->codec, ptInStream->codec) < ) //Copy the settings of AVCodecContext
{
printf("Failed to copy context from input to output stream codec context\r\n");
iRet = AVERROR_UNKNOWN;
break;
}
else
{
ptOutStream->codec->codec_tag = ;
if (ptOutFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
ptOutStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; }
}
}
if(AVERROR_UNKNOWN == iRet)
{
}
else
{
av_dump_format(ptOutFormatContext, , strOutFileName, );//Output information------------------
//Open output file
if (!(ptOutputFormat->flags & AVFMT_NOFILE))
{ /*打开FFmpeg的输入输出文件,使后续读写操作可以执行
*s:函数调用成功之后创建的AVIOContext结构体。
*url:输入输出协议的地址(文件也是一种“广义”的协议,对于文件来说就是文件的路径)。
*flags:打开地址的方式。可以选择只读,只写,或者读写。取值如下。
AVIO_FLAG_READ:只读。AVIO_FLAG_WRITE:只写。AVIO_FLAG_READ_WRITE:读写。*/
iRet = avio_open(&ptOutFormatContext->pb, strOutFileName, AVIO_FLAG_WRITE);
if (iRet < )
{
printf("Could not open output file %s\r\n", strOutFileName);
}
else
{
//Write file header
if (avformat_write_header(ptOutFormatContext, NULL) < ) //avformat_write_header()中最关键的地方就是调用了AVOutputFormat的write_header()
{//不同的AVOutputFormat有不同的write_header()的实现方法
printf("Error occurred when opening output file\r\n");
}
else
{
while ()
{
//Get an AVPacket
iRet = av_read_frame(ptInFormatContext, &tOutPacket);//从输入文件读取一帧压缩数据
if (iRet < )
break; ptInStream = ptInFormatContext->streams[tOutPacket.stream_index];
ptOutStream = ptOutFormatContext->streams[tOutPacket.stream_index];
//Convert PTS/DTS
tOutPacket.pts = av_rescale_q_rnd(tOutPacket.pts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
tOutPacket.dts = av_rescale_q_rnd(tOutPacket.dts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
tOutPacket.duration = av_rescale_q(tOutPacket.duration, ptInStream->time_base, ptOutStream->time_base);
tOutPacket.pos = -;
//Write
/*av_interleaved_write_frame包括interleave_packet()以及write_packet(),将还未输出的AVPacket输出出来
*write_packet()函数最关键的地方就是调用了AVOutputFormat中写入数据的方法。write_packet()实际上是一个函数指针,
指向特定的AVOutputFormat中的实现函数*/
if (av_interleaved_write_frame(ptOutFormatContext, &tOutPacket) < )
{
printf("Error muxing packet\r\n");
break;
}
printf("Write %8d frames to output file\r\n", iFrameCount);
av_free_packet(&tOutPacket);//释放空间
iFrameCount++;
}
//Write file trailer//av_write_trailer()中最关键的地方就是调用了AVOutputFormat的write_trailer()
av_write_trailer(ptOutFormatContext);//不同的AVOutputFormat有不同的write_trailer()的实现方法
}
if (ptOutFormatContext && !(ptOutputFormat->flags & AVFMT_NOFILE))
avio_close(ptOutFormatContext->pb);//该函数用于关闭一个AVFormatContext->pb,一般情况下是和avio_open()成对使用的。
}
}
}
avformat_free_context(ptOutFormatContext);//释放空间
}
}
avformat_close_input(&ptInFormatContext);//该函数用于关闭一个AVFormatContext,一般情况下是和avformat_open_input()成对使用的。
}
return ;
}

FFmpegRemuxer.cpp

具体代码见github:

https://github.com/fengweiyu/FFmpegFormat/FFmpegRemuxer

三、FFmpeg的封装格式处理:视音频复用器(muxer)

1.封装格式处理

视音频复用器(Muxer)即是将视频压缩数据(例如H.264)和音频压缩数据(例如AAC)合并到一个封装格式数据(例如MKV)中去。

如图3所示。在这个过程中并不涉及到编码和解码。

音视频处理之FFmpeg封装格式20180510

2.基于FFmpeg的muxer的流程图

程序的流程如下图4所示。从流程图中可以看出,一共初始化了3个AVFormatContext,其中2个用于输入,1个用于输出。3个AVFormatContext初始化之后,通过avcodec_copy_context()函数可以将输入视频/音频的参数拷贝至输出视频/音频的AVCodecContext结构体。

然后分别调用视频输入流和音频输入流的av_read_frame(),从视频输入流中取出视频的AVPacket,音频输入流中取出音频的AVPacket,分别将取出的AVPacket写入到输出文件中即可。

其间用到了一个不太常见的函数av_compare_ts(),是比较时间戳用的。通过该函数可以决定该写入视频还是音频。

音视频处理之FFmpeg封装格式20180510

本文介绍的视音频复用器,输入的视频不一定是H.264裸流文件,音频也不一定是纯音频文件。可以选择两个封装过的视音频文件作为输入。程序会从视频输入文件中“挑”出视频流,音频输入文件中“挑”出音频流,再将“挑选”出来的视音频流复用起来。

PS1:对于某些封装格式(例如MP4/FLV/MKV等)中的H.264,需要用到名称为“h264_mp4toannexb”的bitstream filter。

PS2:对于某些封装格式(例如MP4/FLV/MKV等)中的AAC,需要用到名称为“aac_adtstoasc”的bitstream filter。

简单介绍一下流程中各个重要函数的意义:

avformat_open_input():打开输入文件。

avcodec_copy_context():赋值AVCodecContext的参数。

avformat_alloc_output_context2():初始化输出文件。

avio_open():打开输出文件。

avformat_write_header():写入文件头。

av_compare_ts():比较时间戳,决定写入视频还是写入音频。这个函数相对要少见一些。

av_read_frame():从输入文件读取一个AVPacket。

av_interleaved_write_frame():写入一个AVPacket到输出文件。

av_write_trailer():写入文件尾。

3.优化为可以从内存中读取音视频数据

打开文件的函数是avformat_open_input(),直接将文件路径或者流媒体URL的字符串传递给该函数就可以了。

但其是否支持从内存中读取数据呢?

分析ffmpeg的源代码,发现其竟然是可以从内存中读取数据的,代码很简单,如下所示:

ptInFormatContext = avformat_alloc_context();

pbIoBuf = (unsigned char *)av_malloc(IO_BUFFER_SIZE);

ptAVIO = avio_alloc_context(pbIoBuf, IO_BUFFER_SIZE, 0, NULL, FillIoBuffer, NULL, NULL);

ptInFormatContext->pb = ptAVIO;

ptInputFormat = av_find_input_format("h264");//得到ptInputFormat以便后面打开使用

if ((iRet = avformat_open_input(&ptInFormatContext, "", ptInputFormat, NULL)) < 0)

{

printf("Could not open input file\r\n");

}

else

{

}

关键要在avformat_open_input()之前初始化一个AVIOContext,而且将原本的AVFormatContext的指针pb(AVIOContext类型)指向这个自行初始化AVIOContext。

当自行指定了AVIOContext之后,avformat_open_input()里面的URL参数就不起作用了。示例代码开辟了一块空间iobuffer作为AVIOContext的缓存。

FillIoBuffer则是将数据读取至iobuffer的回调函数。FillIoBuffer()形式(参数,返回值)是固定的,是一个回调函数,如下所示(只是个例子,具体怎么读取数据可以自行设计)。

示例中回调函数将文件中的内容通过fread()读入内存。

int FillIoBuffer(void *opaque, unsigned char *o_pbBuf, int i_iMaxSize)

{

int iRet=-1;

if (!feof(g_fileH264))

{

iRet = fread(o_pbBuf, 1, i_iMaxSize, g_fileH264);

}

else

{

}

return iRet;

}

整体结构大致如下:

FILE *fp_open;

int fill_iobuffer(void *opaque, uint8_t *buf, int buf_size){

...

}

int main(){

...

fp_open=fopen("test.h264","rb+");

AVFormatContext *ic = NULL;

ic = avformat_alloc_context();

unsigned char * iobuffer=(unsigned char *)av_malloc(32768);

AVIOContext *avio =avio_alloc_context(iobuffer, 32768,0,NULL,fill_iobuffer,NULL,NULL);

ic->pb=avio;

err = avformat_open_input(&ic, "nothing", NULL, NULL);

...//解码

}

4.将音视频数据输出到内存

同时再说明一下,和从内存中读取数据类似,ffmpeg也可以将处理后的数据输出到内存。

回调函数如下示例,可以将输出到内存的数据写入到文件中。

//写文件的回调函数

int write_buffer(void *opaque, uint8_t *buf, int buf_size){

if(!feof(fp_write)){

int true_size=fwrite(buf,1,buf_size,fp_write);

return true_size;

}else{

return -1;

}

}

主函数如下所示:

FILE *fp_write;

int write_buffer(void *opaque, uint8_t *buf, int buf_size){

...

}

main(){

...

fp_write=fopen("src01.h264","wb+"); //输出文件

...

AVFormatContext* ofmt_ctx=NULL;

avformat_alloc_output_context2(&ofmt_ctx, NULL, "h264", NULL);

unsigned char* outbuffer=(unsigned char*)av_malloc(32768);

AVIOContext *avio_out =avio_alloc_context(outbuffer, 32768,0,NULL,NULL,write_buffer,NULL);

ofmt_ctx->pb=avio_out;

ofmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO;

...

}

从上述可以很明显的看到,知道把写回调函数放到avio_alloc_context函数对应的位置就可以了。

四、FFmpegMuxer代码

基于FFmpeg的视音频复用器,取了个名字称为FFmpegMuxer

主要是FFmpegMuxer.cpp文件,代码如下(基本上每一行都有注释):

 /*****************************************************************************
* Copyright (C) 2017-2020 Hanson Yu All rights reserved.
------------------------------------------------------------------------------
* File Module : FFmpegMuxer.cpp
* Description : FFmpegMuxer Demo *先将H.264文件读入内存,
*再输出封装格式文件。 输出结果:
book@book-desktop:/work/project/FFmpegMuxer$ make clean;make
rm FFmpegMuxer
g++ FFmpegMuxer.cpp -I ./include -rdynamic ./lib/libavformat.so.57 ./lib/libavcodec.so.57 ./lib/libavutil.so.55 ./lib/libswresample.so.2 -o FFmpegMuxer
book@book-desktop:/work/project/FFmpegMuxer$ export LD_LIBRARY_PATH=./lib
book@book-desktop:/work/project/FFmpegMuxer$ ./FFmpegMuxer sintel.h264 sintel.mp4
Input #0, h264, from 'sintel.h264':
Duration: N/A, bitrate: N/A
Stream #0:0: Video: h264 (High), yuv420p(progressive), 640x360, 25 fps, 25 tbr, 1200k tbn, 50 tbc
Output #0, mp4, to 'sintel.mp4':
Stream #0:0: Unknown: none
[mp4 @ 0x9352d80] Using AVStream.codec.time_base as a timebase hint to the muxer is deprecated. Set AVStream.time_base instead.
[mp4 @ 0x9352d80] Using AVStream.codec to pass codec parameters to muxers is deprecated, use AVStream.codecpar instead.
Write iFrameIndex:1,stream_index:0,num:25,den:1
Write iFrameIndex:2,stream_index:0,num:25,den:1
Write iFrameIndex:3,stream_index:0,num:25,den:1
.
.
. * Created : 2017.09.21.
* Author : Yu Weifeng
* Function List :
* Last Modified :
* History :
* Modify Date Version Author Modification
* -----------------------------------------------
* 2017/09/21 V1.0.0 Yu Weifeng Created
******************************************************************************/
#include <stdio.h> /*
__STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS are a workaround to allow C++ programs to use stdint.h
macros specified in the C99 standard that aren't in the C++ standard. The macros, such as UINT8_MAX, INT64_MIN,
and INT32_C() may be defined already in C++ applications in other ways. To allow the user to decide
if they want the macros defined as C99 does, many implementations require that __STDC_LIMIT_MACROS
and __STDC_CONSTANT_MACROS be defined before stdint.h is included. This isn't part of the C++ standard, but it has been adopted by more than one implementation.
*/
#define __STDC_CONSTANT_MACROS #ifdef _WIN32//Windows
extern "C"
{
#include "libavformat/avformat.h"
};
#else//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavformat/avformat.h>
#ifdef __cplusplus
};
#endif
#endif #define IO_BUFFER_SIZE 32768 //缓存32k static FILE * g_fileH264=NULL; /*****************************************************************************
-Fuction : FillIoBuffer
-Description : FillIoBuffer *在avformat_open_input()中会首次调用该回调函数,
*第二次一直到最后一次都是在avformat_find_stream_info()中循环调用,
*文件中的数据每次IO_BUFFER_SIZE字节读入到内存中,
*经过ffmpeg处理,所有数据被有序地逐帧存储到AVPacketList中。
*以上是缓存设为32KB的情况,缓存大小设置不同,调用机制也有所不同。 -Input :
-Output :
-Return : 返回读取的长度
* Modify Date Version Author Modification
* -----------------------------------------------
* 2017/09/21 V1.0.0 Yu Weifeng Created
******************************************************************************/
int FillIoBuffer(void *opaque, unsigned char *o_pbBuf, int i_iMaxSize)
{
int iRet=-;
if (!feof(g_fileH264))
{
iRet = fread(o_pbBuf, , i_iMaxSize, g_fileH264);
}
else
{
}
return iRet;
} /*****************************************************************************
-Fuction : main
-Description : main
关键要在avformat_open_input()之前初始化一个AVIOContext,
而且将原本的AVFormatContext的指针pb(AVIOContext类型)指向这个自行初始化AVIOContext
-Input :
-Output :
-Return :
* Modify Date Version Author Modification
* -----------------------------------------------
* 2017/09/21 V1.0.0 Yu Weifeng Created
******************************************************************************/
int main(int argc, char* argv[])
{
AVInputFormat * ptInputFormat = NULL;//The output container format.Muxing only, must be set by the caller before avformat_write_header().
AVOutputFormat * ptOutputFormat = NULL;//The output container format.Muxing only, must be set by the caller before avformat_write_header().
AVFormatContext * ptInFormatContext = NULL;//输入文件的封装格式上下文,内部包含所有的视频信息
AVFormatContext * ptOutFormatContext = NULL;//输出文件的封装格式上下文,内部包含所有的视频信息
AVPacket tOutPacket ={};//存储一帧压缩编码数据给输出文件
const char * strInVideoFileName=NULL, * strOutFileName = NULL;//输入文件名和输出文件名
int iRet, i;
int iVideoStreamIndex = -;//视频流应该处在的位置
int iFrameIndex = ;
long long llCurrentPts = ;
int iOutVideoStreamIndex = -; //输出流中的视频流所在的位置
AVStream * ptInStream=NULL,* ptOutStream=NULL;//输入音视频流和输出音视频流
unsigned char * pbIoBuf=NULL;//io数据缓冲区
AVIOContext * ptAVIO=NULL;//AVIOContext管理输入输出数据的结构体 if(argc!=)//argc包括argv[0]也就是程序名称
{
printf("Usage:%s InputVideoFileURL OutputFileURL\r\n",argv[]);
printf("For example:\r\n");
printf("%s InputFile.h264 OutputFile.mp4\r\n",argv[]);
return -;
}
strInVideoFileName = argv[];//Input file URL
strOutFileName = argv[];//Output file URL av_register_all();//注册FFmpeg所有组件 /*------------Input:填充ptInFormatContext------------*/
g_fileH264 = fopen(strInVideoFileName, "rb+");
ptInFormatContext = avformat_alloc_context();
pbIoBuf = (unsigned char *)av_malloc(IO_BUFFER_SIZE);
//FillIoBuffer则是将数据读取至pbIoBuf的回调函数。FillIoBuffer()形式(参数,返回值)是固定的,是一个回调函数,
ptAVIO = avio_alloc_context(pbIoBuf, IO_BUFFER_SIZE, , NULL, FillIoBuffer, NULL, NULL); //当系统需要数据的时候,会自动调用该回调函数以获取数据
ptInFormatContext->pb = ptAVIO; //当自行指定了AVIOContext之后,avformat_open_input()里面的URL参数就不起作用了 ptInputFormat = av_find_input_format("h264");//得到ptInputFormat以便后面打开使用
//ps:函数调用成功之后处理过的AVFormatContext结构体;file:打开的视音频流的文件路径或者流媒体URL;fmt:强制指定AVFormatContext中AVInputFormat的,为NULL,FFmpeg通过文件路径或者流媒体URL自动检测;dictionay:附加的一些选项,一般情况下可以设置为NULL
//内部主要调用两个函数:init_input():绝大部分初始化工作都是在这里做的。s->iformat->read_header():读取多媒体数据文件头,根据视音频流创建相应的AVStream
if ((iRet = avformat_open_input(&ptInFormatContext, "", ptInputFormat, NULL)) < ) //其中的init_input()如果指定了fmt(第三个参数,比如当前就有指定)就直接返回,如果没有指定就调用av_probe_input_buffer2()推测AVInputFormat
{//打开输入视频源//自定义了回调函数FillIoBuffer()。在使用avformat_open_input()打开媒体数据的时候,就可以不指定文件的URL了,即其第2个参数为NULL(因为数据不是靠文件读取,而是由FillIoBuffer()提供)
printf("Could not open input file\r\n");
}
else
{
if ((iRet = avformat_find_stream_info(ptInFormatContext, )) < )
{//获取视频文件信息
printf("Failed to find input stream information\r\n");
}
else
{
av_dump_format(ptInFormatContext, , strInVideoFileName, );//手工调试的函数,内部是log,输出相关的格式信息到log里面 /*------------Output------------*/ /*初始化一个用于输出的AVFormatContext结构体
*ctx:函数调用成功之后创建的AVFormatContext结构体。
*oformat:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,
可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。
PS:使用该参数需要自己手动获取AVOutputFormat,相对于使用后两个参数来说要麻烦一些。
*format_name:指定输出格式的名称。根据格式名称,FFmpeg会推测输出格式。输出格式可以是“flv”,“mkv”等等。
*filename:指定输出文件的名称。根据文件名称,FFmpeg会推测输出格式。文件名称可以是“xx.flv”,“yy.mkv”等等。
函数执行成功的话,其返回值大于等于0
*/
avformat_alloc_output_context2(&ptOutFormatContext, NULL, NULL, strOutFileName);
if (!ptOutFormatContext)
{
printf("Could not create output context\r\n");
iRet = AVERROR_UNKNOWN;
}
else
{
ptOutputFormat = ptOutFormatContext->oformat;
//for (i = 0; i < ptInFormatContext->nb_streams; i++)
{
//Create output AVStream according to input AVStream
ptInStream = ptInFormatContext->streams[];//0 video
ptOutStream = avformat_new_stream(ptOutFormatContext, ptInStream->codec->codec);//给ptOutFormatContext中的流数组streams中的
if (!ptOutStream) //一条流(数组中的元素)分配空间,也正是由于这里分配了空间,后续操作直接拷贝编码数据(pkt)就可以了。
{
printf("Failed allocating output stream\r\\n");
iRet = AVERROR_UNKNOWN;
//break;
}
else
{
iVideoStreamIndex=;
iOutVideoStreamIndex = ptOutStream->index; //保存视频流所在数组的位置
if (avcodec_copy_context(ptOutStream->codec, ptInStream->codec) < ) //Copy the settings of AVCodecContext
{//avcodec_copy_context()函数可以将输入视频/音频的参数拷贝至输出视频/音频的AVCodecContext结构体
printf("Failed to copy context from input to output stream codec context\r\n");
iRet = AVERROR_UNKNOWN;
//break;
}
else
{
ptOutStream->codec->codec_tag = ;
if (ptOutFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
ptOutStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; }
}
}
if(AVERROR_UNKNOWN == iRet)
{
}
else
{
av_dump_format(ptOutFormatContext, , strOutFileName, );//Output information------------------
//Open output file
if (!(ptOutputFormat->flags & AVFMT_NOFILE))
{ /*打开FFmpeg的输入输出文件,使后续读写操作可以执行
*s:函数调用成功之后创建的AVIOContext结构体。
*url:输入输出协议的地址(文件也是一种“广义”的协议,对于文件来说就是文件的路径)。
*flags:打开地址的方式。可以选择只读,只写,或者读写。取值如下。
AVIO_FLAG_READ:只读。AVIO_FLAG_WRITE:只写。AVIO_FLAG_READ_WRITE:读写。*/
iRet = avio_open(&ptOutFormatContext->pb, strOutFileName, AVIO_FLAG_WRITE);
if (iRet < )
{
printf("Could not open output file %s\r\n", strOutFileName);
}
else
{
//Write file header
if (avformat_write_header(ptOutFormatContext, NULL) < ) //avformat_write_header()中最关键的地方就是调用了AVOutputFormat的write_header()
{//不同的AVOutputFormat有不同的write_header()的实现方法
printf("Error occurred when opening output file\r\n");
}
else
{
while ()
{
int iStreamIndex = -;//用于标识当前是哪个流
iStreamIndex = iOutVideoStreamIndex;
//Get an AVPacket//从视频输入流中取出视频的AVPacket
iRet = av_read_frame(ptInFormatContext, &tOutPacket);//从输入文件读取一帧压缩数据
if (iRet < )
break;
else
{
do{
ptInStream = ptInFormatContext->streams[tOutPacket.stream_index];
ptOutStream = ptOutFormatContext->streams[iStreamIndex];
if (tOutPacket.stream_index == iVideoStreamIndex)
{ //H.264裸流没有PTS,因此必须手动写入PTS,应该放在av_read_frame()之后
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if (tOutPacket.pts == AV_NOPTS_VALUE)
{
//Write PTS
AVRational time_base1 = ptInStream->time_base;
//Duration between 2 frames (μs) 。假设25帧,两帧间隔40ms //AV_TIME_BASE表示1s,所以用它的单位为us,也就是ffmpeg中都是us
//int64_t calc_duration = AV_TIME_BASE*1/25;//或40*1000;//(double)AV_TIME_BASE / av_q2d(ptInStream->r_frame_rate);//ptInStream->r_frame_rate.den等于0所以注释掉
//帧率也可以从h264的流中获取,前面dump就有输出,但是不知道为何同样的变量前面r_frame_rate打印正常,这里使用的时候却不正常了,所以这个间隔时间只能使用avg_frame_rate或者根据假设帧率来写
int64_t calc_duration =(double)AV_TIME_BASE / av_q2d(ptInStream->avg_frame_rate);
//Parameters pts(显示时间戳)*pts单位(时间基*时间基单位)=真实显示的时间(所谓帧的显示时间都是相对第一帧来的)
tOutPacket.pts = (double)(iFrameIndex*calc_duration) / (double)(av_q2d(time_base1)*AV_TIME_BASE);//AV_TIME_BASE为1s,所以其单位为us
tOutPacket.dts = tOutPacket.pts;
tOutPacket.duration = (double)calc_duration / (double)(av_q2d(time_base1)*AV_TIME_BASE);
iFrameIndex++;
printf("Write iFrameIndex:%d,stream_index:%d,num:%d,den:%d\r\n",iFrameIndex, tOutPacket.stream_index,ptInStream->avg_frame_rate.num,ptInStream->avg_frame_rate.den);
}
llCurrentPts = tOutPacket.pts;
break;
}
} while (av_read_frame(ptInFormatContext, &tOutPacket) >= );
} //Convert PTS/DTS
tOutPacket.pts = av_rescale_q_rnd(tOutPacket.pts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
tOutPacket.dts = av_rescale_q_rnd(tOutPacket.dts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
tOutPacket.duration = av_rescale_q(tOutPacket.duration, ptInStream->time_base, ptOutStream->time_base);
tOutPacket.pos = -;
tOutPacket.stream_index = iStreamIndex;
//printf("Write 1 Packet. size:%5d\tpts:%lld\n", tOutPacket.size, tOutPacket.pts);
//Write
/*av_interleaved_write_frame包括interleave_packet()以及write_packet(),将还未输出的AVPacket输出出来
*write_packet()函数最关键的地方就是调用了AVOutputFormat中写入数据的方法。write_packet()实际上是一个函数指针,
指向特定的AVOutputFormat中的实现函数*/
if (av_interleaved_write_frame(ptOutFormatContext, &tOutPacket) < )
{
printf("Error muxing packet\r\n");
break;
}
av_free_packet(&tOutPacket);//释放空间
}
//Write file trailer//av_write_trailer()中最关键的地方就是调用了AVOutputFormat的write_trailer()
av_write_trailer(ptOutFormatContext);//不同的AVOutputFormat有不同的write_trailer()的实现方法
}
if (ptOutFormatContext && !(ptOutputFormat->flags & AVFMT_NOFILE))
avio_close(ptOutFormatContext->pb);//该函数用于关闭一个AVFormatContext->pb,一般情况下是和avio_open()成对使用的。
}
}
}
avformat_free_context(ptOutFormatContext);//释放空间
}
}
avformat_close_input(&ptInFormatContext);//该函数用于关闭一个AVFormatContext,一般情况下是和avformat_open_input()成对使用的。
}
if(NULL!=g_fileH264)
fclose(g_fileH264);
return ;
}

FFmpegMuxer.cpp

具体代码见github:

https://github.com/fengweiyu/FFmpegFormat/FFmpegMuxer

五、参考原文:

https://blog.csdn.net/leixiaohua1020/article/details/25422685

https://blog.csdn.net/leixiaohua1020/article/details/39802913

https://blog.csdn.net/leixiaohua1020/article/details/12980423

https://blog.csdn.net/leixiaohua1020/article/details/39759163