简介
RTMP推流器(Streamer)的在流媒体系统中的作用可以用下图表示。首先将视频数据以RTMP的形式发送到流媒体服务器端(Server,比如FMS,Red5,Wowza等),然后客户端(一般为Flash Player)通过访问流媒体服务器就可以收看实时流了。
运行本程序之前需要先运行RTMP流媒体服务器,并在流媒体服务器上建立相应的Application。有关流媒体服务器的操作不在本文的论述范围内,在此不再详述。本程序运行后,即可通过RTMP客户端(例如 Flash Player, FFplay等等)收看推送的直播流。
需要要注意的地方
封装格式
RTMP采用的封装格式是FLV。因此在指定输出流媒体的时候需要指定其封装格式为“flv”。同理,其他流媒体协议也需要指定其封装格式。例如采用UDP推送流媒体的时候,可以指定其封装格式为“mpegts”。
延时
发送流媒体的数据的时候需要延时。不然的话,FFmpeg处理数据速度很快,瞬间就能把所有的数据发送出去,流媒体服务器是接受不了的。因此需要按照视频实际的帧率发送数据。本文记录的推流器在视频帧与帧之间采用了av_usleep()函数休眠的方式来延迟发送。这样就可以按照视频的帧率发送数据了,参考代码如下。
//…
int64_t start_time=av_gettime();
while (1) {
//…
//Important:Delay
if(pkt.stream_index==videoindex){
AVRationaltime_base=ifmt_ctx->streams[videoindex]->time_base;
AVRationaltime_base_q={1,AV_TIME_BASE};
int64_tpts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_tnow_time = av_gettime() - start_time;
if(pts_time > now_time)
av_usleep(pts_time- now_time);
}
//…
}
//…
PTS/DTS问题
没有封装格式的裸流(例如H.264裸流)是不包含PTS、DTS这些参数的。在发送这种数据的时候,需要自己计算并写入AVPacket的PTS,DTS,duration等参数。如下所示。
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if(pkt.pts==AV_NOPTS_VALUE){
//WritePTS
AVRationaltime_base1=ifmt_ctx->streams[videoindex]->time_base;
//Durationbetween 2 frames (us)
int64_tcalc_duration=(double)AV_TIME_BASE/av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
//Parameters
pkt.pts=(double)(frame_index*calc_duration)/(double)(av_q2d(time_base1)*AV_TIME_BASE);
pkt.dts=pkt.pts;
pkt.duration=(double)calc_duration/(double)(av_q2d(time_base1)*AV_TIME_BASE);
}
程序流程图
代码
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavformat/avformat.h"
#include "libavutil/mathematics.h"
#include "libavutil/time.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavformat/avformat.h>
#include <libavutil/mathematics.h>
#include <libavutil/time.h>
#ifdef __cplusplus
};
#endif
#endif
int push(char* input_str, char* output_str)
{
AVOutputFormat *ofmt = NULL;
AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
AVPacket pkt;
intret, i;
//FFmpeg av_log() callback
av_log_set_callback(custom_log);
av_register_all();
//Network
avformat_network_init();
//Input
if((ret = avformat_open_input(&ifmt_ctx, input_str, 0, 0)) < 0)
{
printf( "Could not open input file.");
goto end;
}
LOGI("avformat_find_stream_info");
if((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0)
{
printf( "error to retrieve input stream information");
goto end;
}
//Output
avformat_alloc_output_context2(&ofmt_ctx, NULL,"flv",output_str); //RTMP
//avformat_alloc_output_context2(&ofmt_ctx, NULL,"mpegts", output_str);//UDP
if(!ofmt_ctx)
{
printf( "Could not create outputcontext\n");
ret= AVERROR_UNKNOWN;
goto end;
}
ofmt =ofmt_ctx->oformat;
intvideoindex = -1;
for(i =0; i<ifmt_ctx->nb_streams; i++)
if(ifmt_ctx->streams[i]->codec->codec_type ==AVMEDIA_TYPE_VIDEO)
{
videoindex = i;
break;
}
//Create output AVStream according to input AVStream
AVStream *in_stream = ifmt_ctx->streams[videoindex];
AVStream *out_stream = avformat_new_stream(ofmt_ctx,in_stream->codec->codec);
if(!out_stream)
{
printf( "Error occurred when allocating output stream\n");
ret= AVERROR_UNKNOWN;
goto end;
}
//Copythe settings of AVCodecContext
ret =avcodec_copy_context(out_stream->codec, in_stream->codec);
if (ret< 0)
{
printf( "error to copy context from input to output stream codeccontext\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if(ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags | = CODEC_FLAG_GLOBAL_HEADER;
//Openoutput URL
if(!(ofmt->flags & AVFMT_NOFILE))
{
ret= avio_open(&ofmt_ctx->pb, output_str, AVIO_FLAG_WRITE);
if(ret < 0)
{
printf( "Could not open output URL '%s'", output_str);
goto end;
}
}
//Writefile header
ret =avformat_write_header(ofmt_ctx, NULL);
if (ret< 0)
{
printf( "Error occurred when opening output URL : %d\n", ret);
goto end;
}
intframe_index = 0;
int64_tstart_time = av_gettime();
while(1)
{
AVStream *in_stream, *out_stream;
//Get an AVPacket
ret= av_read_frame(ifmt_ctx, &pkt);
if(ret < 0)
break;
if(pkt.stream_index != videoindex)
{
av_free_packet(&pkt);
continue;
}
//FIX:NoPTS (Example: Raw H.264)
//Simple Write PTS
if(pkt.pts == AV_NOPTS_VALUE)
{
//Write PTS
AVRational time_base1 = ifmt_ctx->streams[videoindex]->time_base;
//Duration between 2 frames (us)
int64_t calc_duration =(double)AV_TIME_BASE/av_q2d(ifmt_ctx->streams[videoindex]->r_frame_rate);
//Parameters
pkt.pts =(double)(frame_index*calc_duration)/(double)(av_q2d(time_base1)*AV_TIME_BASE);
pkt.dts = pkt.pts;
pkt.duration =(double)calc_duration/(double)(av_q2d(time_base1)*AV_TIME_BASE);
}
//Important:Delay
if(pkt.stream_index == videoindex)
{
AVRational time_base = ifmt_ctx->streams[videoindex]->time_base;
AVRational time_base_q = {1,AV_TIME_BASE};
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - start_time;
if (pts_time > now_time)
av_usleep(pts_time - now_time);
}
in_stream =ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
/*copy packet */
//Convert PTS/DTS
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base,out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
pkt.dts = pkt.pts;//av_rescale_q_rnd(pkt.dts, in_stream->time_base,out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base,out_stream->time_base);
pkt.pos = -1;
//Print to Screen
if(pkt.stream_index == videoindex)
{
printf("Send %8d video frames to output URL\n",frame_index);
frame_index++;
}
ret= av_interleaved_write_frame(ofmt_ctx, &pkt);
if(ret < 0)
{
printf( "Error muxing packet\n");
break;
}
av_free_packet(&pkt);
}
//Writefile trailer
av_write_trailer(ofmt_ctx);
end:
avformat_close_input(&ifmt_ctx);
/*close output */
if(ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret< 0 && ret != AVERROR_EOF)
{
printf( "Error occurred.\n");
return -1;
}
return0;
}
avformat_open_input ()
该函数用于打开多媒体数据并且获得一些相关的信息。它的声明位于libavformat\avformat.h,如下所示。
/**
* Open aninput stream and read the header. The codecs are not opened.
* Thestream must be closed with avformat_close_input().
*
* @paramps Pointer to user-supplied AVFormatContext (allocated byavformat_alloc_context).
* May be a pointer to NULL, in whichcase an AVFormatContext is allocated by this
* function and written into ps.
* Note that a user-suppliedAVFormatContext will be freed on failure.
* @paramfilename Name of the stream to open.
* @paramfmt If non-NULL, this parameter forces a specific input format.
* Otherwise the format isautodetected.
* @paramoptions A dictionary filled withAVFormatContext and demuxer-private options.
* On return this parameter willbe destroyed and replaced with a dict containing
* options that were not found.May be NULL.
*
* @return0 on success, a negative AVERROR on failure.
*
* @note Ifyou want to use custom IO, preallocate the format context and set its pb field.
*/
int avformat_open_input(AVFormatContext **ps,const char *filename, AVInputFormat *fmt, AVDictionary **options);
代码中的英文注释写的已经比较详细了,在这里拿中文简单叙述一下。
ps:函数调用成功之后处理过的AVFormatContext结构体。
file:打开的视音频流的URL。
fmt:强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat。
dictionay:附加的一些选项,一般情况下可以设置为NULL。
函数执行成功的话,其返回值大于等于0。
函数调用关系图
函数调用结构图如下所示。
avformat_open_input()源码位于libavformat\utils.c中,这里只选择它关键的两个函数进行分析:
init_input():绝大部分初始化工作都是在这里做的。
s->iformat->read_header():读取多媒体数据文件头,根据视音频流创建相应的AVStream。
下面我们逐一看看上述函数。
init_input()
init_input()主要工作就是打开输入的视频数据并且探测视频的格式。该函数的定义位于libavformat\utils.c,
av_probe_input_format2()
av_probe_input_format2()是一个API函数,声明位于libavformat\avformat.h。
av_probe_input_format3()
av_probe_input_format3()是一个API函数,声明位于libavformat\avformat.h。
av_probe_input_format3()根据输入数据查找合适的AVInputFormat。输入的数据位于AVProbeData中。
上述过程中涉及到以下几个知识点:
AVInputFormat->read_probe()
AVInputFormat中包含read_probe()是用于获得匹配函数的函数指针,不同的封装格式包含不同的实现函数。例如,FLV封装格式的AVInputFormat模块定义(位于libavformat\flvdec.c)如下所示。
AVInputFormat ff_flv_demuxer = {
.name = "flv",
.long_name =NULL_IF_CONFIG_SMALL("FLV (Flash Video)"),
.priv_data_size = sizeof(FLVContext),
.read_probe = flv_probe,
.read_header = flv_read_header,
.read_packet = flv_read_packet,
.read_seek = flv_read_seek,
.read_close = flv_read_close,
.extensions = "flv",
.priv_class = &flv_class,
};
其中,read_probe()函数对应的是flv_probe()函数。我们可以看一下flv_probe()函数的定义:
static int flv_probe(AVProbeData *p)
{
returnprobe(p, 0);
}
可见flv_probe()调用了一个probe()函数。
该函数做了如下工作:
(1)获得第6至第9字节的数据(对应Headersize字段)并且做大小端转换,然后存入offset变量。之所以要进行大小端转换是因为FLV是以“大端”方式存储数据,而操作系统是以“小端”方式存储数据,这一转换主要通过AV_RB32()函数实现。AV_RB32()是一个宏定义,其对应的函数是av_bswap32()。
(2)检查开头3个字符(Signature)是否为“FLV”。
(3)第4个字节(Version)小于5。
(4)第6个字节(Headersize的第1个字节?)为0。
(5)offset取值大于8。
参照FLV文件头的格式可以对上述判断有一个更清晰的认识:
av_match_name()
av_match_name()是一个API函数,声明位于libavutil\avstring.h,如下所示。
上述函数还有一点需要注意,其中使用了一个while()循环,用于搜索“,”。这是因为FFmpeg中有些格式是对应多种格式名称的,例如MKV格式的解复用器(Demuxer)的定义如下。
AVInputFormat ff_matroska_demuxer = {
.name ="matroska,webm",
.long_name =NULL_IF_CONFIG_SMALL("Matroska / WebM"),
.extensions ="mkv,mk3d,mka,mks",
.priv_data_size = sizeof(MatroskaDemuxContext),
.read_probe = matroska_probe,
.read_header =matroska_read_header,
.read_packet =matroska_read_packet,
.read_close = matroska_read_close,
.read_seek =matroska_read_seek,
.mime_type ="audio/webm,audio/x-matroska,video/webm,video/x-matroska"
};
从代码可以看出,ff_matroska_demuxer中的name字段对应“matroska,webm”,mime_type字段对应“audio/webm,audio/x-matroska,video/webm,video/x-matroska”。av_match_name()函数对于这样的字符串,会把它按照“,”截断成一个个的名称,然后一一进行比较。
av_match_ext()
av_match_ext()是一个API函数,声明位于libavformat\avformat.h(注意位置和av_match_name()不一样),如下所示。
/**
* Return apositive value if the given filename has one of the given
*extensions, 0 otherwise.
*
* @paramfilename file name to check against thegiven extensions
* @paramextensions a comma-separated list of filename extensions
*/
int av_match_ext(const char *filename, const char*extensions);
av_match_ext()用于比较文件的后缀。该函数首先通过反向查找的方式找到输入文件名中的“.”,就可以通过获取“.”后面的字符串来得到该文件的后缀。然后调用av_match_name(),采用和比较格式名称的方法比较两个后缀。
AVInputFormat-> read_header()
在调用完init_input()完成基本的初始化并且推测得到相应的AVInputFormat之后,avformat_open_input()会调用AVInputFormat的read_header()方法读取媒体文件的文件头并且完成相关的初始化工作。read_header()是一个位于AVInputFormat结构体中的一个函数指针,对于不同的封装格式,会调用不同的read_header()的实现函数。举个例子,当输入视频的封装格式为FLV的时候,会调用FLV的AVInputFormat中的read_header()。FLV的AVInputFormat定义位于libavformat\flvdec.c文件中,如下所示。
AVInputFormat ff_flv_demuxer = {
.name = "flv",
.long_name =NULL_IF_CONFIG_SMALL("FLV (Flash Video)"),
.priv_data_size = sizeof(FLVContext),
.read_probe = flv_probe,
.read_header = flv_read_header,
.read_packet = flv_read_packet,
.read_seek = flv_read_seek,
.read_close = flv_read_close,
.extensions = "flv",
.priv_class = &flv_class,
};
可以看出read_header()指向了flv_read_header()函数。flv_read_header()的实现同样位于libavformat\flvdec.c文件中,函数读取了FLV的文件头并且判断其中是否包含视频流和音频流。如果包含视频流或者音频流,就会调用create_stream()函数。
create_stream()函数定义也位于libavformat\flvdec.c中,create_stream()调用了API函数avformat_new_stream()创建相应的视频流和音频流。
经过上面的步骤AVInputFormat的read_header()完成了视音频流对应的AVStream的创建。至此,avformat_open_input()中的主要代码分析完毕。
avformat_find_stream_info()
本文简单分析FFmpeg中一个常用的函数:avformat_find_stream_info()。该函数可以读取一部分视音频数据并且获得一些相关的信息。avformat_find_stream_info()的声明位于libavformat\avformat.h,如下所示。
/**
* Readpackets of a media file to get stream information. This
* isuseful for file formats with no headers such as MPEG. This
* functionalso computes the real framerate in case of MPEG-2 repeat
* framemode.
* Thelogical file position is not changed by this function;
* examinedpackets may be buffered for later processing.
*
* @paramic media file handle
* @paramoptions If non-NULL, an ic.nb_streamslong array of pointers to
* dictionaries, where i-thmember contains options for
* codec corresponding to i-thstream.
* On return each dictionary willbe filled with options that were not found.
* @return>=0 if OK, AVERROR_xxx on error
*
* @notethis function isn't guaranteed to open all the codecs, so
* options being non-empty at return is aperfectly normal behavior.
*
* @todoLet the user decide somehow what information is needed so that
* we do not waste time getting stuff theuser does not need.
*/
int avformat_find_stream_info(AVFormatContext*ic, AVDictionary **options);
参数的含义:
ic:输入的AVFormatContext。
options:额外的选项。
函数正常执行后返回值大于等于0。
函数调用关系图
函数的调用关系如下图所示。
avformat_find_stream_info()的定义位于libavformat\utils.c。
该函数主要用于给每个媒体流(音频/视频)的AVStream结构体赋值。它实现了解码器的查找,解码器的打开,视音频帧的读取,视音频帧的解码等工作。换句话说,该函数实际上已经“走通”的解码的整个流程。下面看一下除了成员变量赋值之外,该函数的几个关键流程。
1.查找解码器:find_decoder()
2.打开解码器:avcodec_open2()
3.读取完整的一帧压缩编码的数据:read_frame_internal()
注:av_read_frame()内部实际上就是调用的read_frame_internal()。
4.解码一些压缩编码数据:try_decode_frame()
5. 估算AVFormatContext以及AVStream的时长duration:estimate_timings()
avformat_alloc_output_context2()
在基于FFmpeg的视音频编码器程序中,该函数通常是第一个调用的函数(除了组件注册函数av_register_all())。avformat_alloc_output_context2()函数可以初始化一个用于输出的AVFormatContext结构体。它的声明位于libavformat\avformat.h,如下所示。
/**
* Allocatean AVFormatContext for an output format.
*avformat_free_context() can be used to free the context and
*everything allocated by the framework within it.
*
* @param*ctx is set to the created format context, or to NULL in
*@param oformat format to use for allocating the context, if NULL
*format_name and filename are used instead
* @paramformat_name the name of output format to use for allocating the
* context,if NULL filename is used instead
* @paramfilename the name of the filename to use for allocating the
* context,may be NULL
* @return>= 0 in case of success, a negative AVERROR code in case of
*/
intavformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat,
const char*format_name, const char *filename);
代码中的英文注释写的已经比较详细了,在这里拿中文简单叙述一下。
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()的流程如要包含以下2步:
1) 调用avformat_alloc_context()初始化一个默认的AVFormatContext。
2) 如果指定了输入的AVOutputFormat,则直接将输入的AVOutputFormat赋值给AVOutputFormat的oformat。如果没有指定输入的AVOutputFormat,就需要根据文件格式名称或者文件名推测输出的AVOutputFormat。无论是通过文件格式名称还是文件名推测输出格式,都会调用一个函数av_guess_format()。
avformat_alloc_context()首先调用av_malloc()为AVFormatContext分配一块内存。然后调用了一个函数avformat_get_context_defaults()用于给AVFormatContext设置默认值。
avformat_get_context_defaults()首先调用memset()将AVFormatContext的内存置零;然后指定它的AVClass(指定了AVClass之后,该结构体就支持和AVOption相关的功能);最后调用av_opt_set_defaults()给AVFormatContext的成员变量设置默认值(av_opt_set_defaults()就是和AVOption有关的一个函数,专门用于给指定的结构体设定默认值,此处暂不分析)。
av_guess_format()中使用一个整型变量score记录每种输出格式的匹配程度。函数中包含了一个while()循环,该循环利用函数av_oformat_next()遍历FFmpeg中所有的AVOutputFormat,并逐一计算每个输出格式的score。具体的计算过程分成如下几步:
1) 如果封装格式名称匹配,score增加100。匹配中使用了函数av_match_name()。
2) 如果mime类型匹配,score增加10。匹配直接使用字符串比较函数strcmp()。
3) 如果文件名称的后缀匹配,score增加5。匹配中使用了函数av_match_ext()。
while()循环结束后,得到得分最高的格式,就是最匹配的格式。
下面看一下一个AVOutputFormat的实例,就可以理解“封装格式名称”,“mine类型”,“文件名称后缀”这些概念了。下面是flv格式的视音频复用器(Muxer)对应的AVOutputFormat格式的变量ff_flv_muxer。
AVOutputFormat ff_flv_muxer = {
.name = "flv",
.long_name =NULL_IF_CONFIG_SMALL("FLV (Flash Video)"),
.mime_type ="video/x-flv",
.extensions = "flv",
.priv_data_size = sizeof(FLVContext),
.audio_codec =CONFIG_LIBMP3LAME ? AV_CODEC_ID_MP3 : AV_CODEC_ID_ADPCM_SWF,
.video_codec =AV_CODEC_ID_FLV1,
.write_header =flv_write_header,
.write_packet =flv_write_packet,
.write_trailer =flv_write_trailer,
.codec_tag = (constAVCodecTag* const []) {
flv_video_codec_ids,flv_audio_codec_ids, 0
},
.flags =AVFMT_GLOBALHEADER | AVFMT_VARIABLE_FPS |
AVFMT_TS_NONSTRICT,
};
av_match_name()用于比较两个格式的名称。简单地说就是比较字符串。注意该函数的字符串是不区分大小写的:字符都转换为小写进行比较。
av_match_ext()用于比较文件的后缀。该函数首先通过反向查找的方式找到输入文件名中的“.”,就可以通过获取“.”后面的字符串来得到该文件的后缀。然后调用av_match_name(),采用和比较格式名称的方法比较两个后缀。
经过以上几步之后,av_guess_format()最终可以得到最合适的AVOutputFormat并且返回给avformat_alloc_output_context2()。avformat_alloc_output_context2()接下来将获得的AVOutputFormat赋值给刚刚新建的AVFormatContext,即可完成初始化工作。
avio_open2()
该函数用于打开FFmpeg的输入输出文件。avio_open2()的声明位于libavformat\avio.h文件中,如下所示。
/**
* Createand initialize a AVIOContext for accessing the
* resourceindicated by url.
* @noteWhen the resource indicated by url has been opened in
*read+write mode, the AVIOContext can be used only for writing.
*
* @param sUsed to return the pointer to the created AVIOContext.
* In caseof failure the pointed to value is set to NULL.
* @paramurl resource to access
* @paramflags flags which control how the resource indicated by url
* is to beopened
* @paramint_cb an interrupt callback to be used at the protocols level
* @paramoptions A dictionary filled withprotocol-private options. On return
* thisparameter will be destroyed and replaced with a dict containing options
* thatwere not found. May be NULL.
* @return>= 0 in case of success, a negative value corresponding to an
* AVERRORcode
*/
int avio_open2(AVIOContext **s, const char *url,int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options);
avio_open2()函数参数的含义如下:
s:函数调用成功之后创建的AVIOContext结构体。
url:输入输出协议的地址(文件也是一种“广义”的协议,对于文件来说就是文件的路径)。
flags:打开地址的方式。可以选择只读,只写,或者读写。取值如下。
AVIO_FLAG_READ:只读。
AVIO_FLAG_WRITE:只写。
AVIO_FLAG_READ_WRITE:读写。
int_cb:目前还没有用过。
options:目前还没有用过。
函数调用结构图
avio_open()
avio_open()比avio_open2()少了最后2个参数。而它前面几个参数的含义和avio_open2()是一样的。从源代码中可以看出,avio_open()内部调用了avio_open2(),并且把avio_open2()的后2个参数设置成了NULL,因此它的功能实际上和avio_open2()是一样的。avio_open()源代码如下所示。
int avio_open(AVIOContext **s, const char *filename,int flags)
{
returnavio_open2(s, filename, flags, NULL, NULL);
}
avio_open2()主要调用了2个函数:ffurl_open()和ffio_fdopen()。其中ffurl_open()用于初始化URLContext,ffio_fdopen()用于根据URLContext初始化AVIOContext。URLContext中包含的URLProtocol完成了具体的协议读写等工作。AVIOContext则是在URLContext的读写函数外面加上了一层“包装”(通过retry_transfer_wrapper()函数)。
URLProtocol和URLContext
首先查看一下URLContext和URLProtocol的定义。这两个结构体在FFmpeg的早期版本的SDK中是定义在头文件中可以直接使用的。但是近期的FFmpeg的SDK中已经找不到这两个结构体的定义了。FFmpeg把这两个结构体移动到了源代码的内部,变成了内部结构体。
URLProtocol的定义位于libavformat\url.h,如下所示。
typedef struct URLProtocol {
constchar *name;
int (*url_open)( URLContext*h, const char *url, int flags);
int (*url_open2)(URLContext*h, const char *url, int flags, AVDictionary **options);
int (*url_read)( URLContext*h, unsigned char *buf, int size);
int (*url_write)(URLContext*h, const unsigned char *buf, int size);
int64_t(*url_seek)( URLContext *h, int64_t pos, int whence);
int (*url_close)(URLContext*h);
structURLProtocol *next;
int (*url_read_pause)(URLContext*h, int pause);
int64_t(*url_read_seek)(URLContext *h, int stream_index,
int64_t timestamp,int flags);
int(*url_get_file_handle)(URLContext *h);
int(*url_get_multi_file_handle)(URLContext *h, int **handles,
int*numhandles);
int(*url_shutdown)(URLContext *h, int flags);
intpriv_data_size;
constAVClass *priv_data_class;
intflags;
int(*url_check)(URLContext *h, int mask);
} URLProtocol;
从URLProtocol的定义可以看出,其中包含了用于协议读写的函数指针。例如:
url_open():打开协议。
url_read():读数据。
url_write():写数据。
url_close():关闭协议。
每种具体的协议都包含了一个URLProtocol结构体,例如:
FILE协议(“文件”在FFmpeg中也被当做一种协议)的结构体ff_file_protocol的定义如下所示(位于libavformat\file.c)。
URLProtocol ff_file_protocol = {
.name = "file",
.url_open = file_open,
.url_read = file_read,
.url_write = file_write,
.url_seek = file_seek,
.url_close = file_close,
.url_get_file_handle = file_get_handle,
.url_check = file_check,
.priv_data_size =sizeof(FileContext),
.priv_data_class =&file_class,
};
在使用FILE协议进行读写的时候,调用url_open()实际上就是调用了file_open()函数,这里限于篇幅不再对file_open()的源代码进行分析。file_open()函数实际上调用了系统的打开文件函数open()。同理,调用url_read()实际上就是调用了file_read()函数;file_read()函数实际上调用了系统的读取文件函数read()。url_write(),url_seek()等函数的道理都是一样的。
LibRTMP协议的结构体ff_librtmp_protocol的定义如下所示(位于libavformat\librtmp.c)。
URLProtocol ff_librtmp_protocol = {
.name ="rtmp",
.url_open = rtmp_open,
.url_read = rtmp_read,
.url_write = rtmp_write,
.url_close = rtmp_close,
.url_read_pause =rtmp_read_pause,
.url_read_seek =rtmp_read_seek,
.url_get_file_handle = rtmp_get_file_handle,
.priv_data_size =sizeof(LibRTMPContext),
.priv_data_class =&librtmp_class,
.flags = URL_PROTOCOL_FLAG_NETWORK,
};
UDP协议的结构体ff_udp_protocol的定义如下所示(位于libavformat\udp.c)。
URLProtocol ff_udp_protocol = {
.name ="udp",
.url_open = udp_open,
.url_read = udp_read,
.url_write = udp_write,
.url_close = udp_close,
.url_get_file_handle = udp_get_file_handle,
.priv_data_size =sizeof(UDPContext),
.priv_data_class =&udp_context_class,
.flags =URL_PROTOCOL_FLAG_NETWORK,
};
上文中简单介绍了URLProtocol结构体。下面看一下URLContext结构体。URLContext的定义也位于libavformat\url.h,如下所示。
typedef struct URLContext {
constAVClass *av_class; /**< informationfor av_log(). Set by url_open(). */
structURLProtocol *prot;
void*priv_data;
char*filename; /**< specifiedURL */
intflags;
intmax_packet_size; /**< if nonzero, the stream is packetized with this max packet size */
intis_streamed; /**< true ifstreamed (no seek possible), default = false */
intis_connected;
AVIOInterruptCB interrupt_callback;
int64_trw_timeout; /**< maximum timeto wait for (network) read/write operation completion, in mcs */
} URLContext;
从代码中可以看出,URLProtocol结构体是URLContext结构体的一个成员。由于还没有对URLContext结构体进行详细研究,有关该结构体的代码不再做过多分析。
avformat_write_header()
FFmpeg的写文件用到的3个函数:avformat_write_header(),av_write_frame()以及av_write_trailer()。其中av_write_frame()用于写视频数据,avformat_write_header()用于写视频文件头,而av_write_trailer()用于写视频文件尾。
需要注意的是,尽管这3个函数功能是配套的,但是它们的前缀却不一样,写文件头Header的函数前缀是“avformat_”,其他两个函数前缀是“av_”(不太明白其中的原因)。
avformat_write_header()的声明位于libavformat\avformat.h,如下所示。
/**
* Allocatethe stream private data and write the stream header to
* anoutput media file.
*
* @param sMedia file handle, must be allocated with avformat_alloc_context().
* Its oformat field must be set to thedesired output format;
* Its pb field must be set to analready opened AVIOContext.
* @paramoptions An AVDictionary filled withAVFormatContext and muxer-private options.
* On return this parameter willbe destroyed and replaced with a dict containing
* options that were not found.May be NULL.
*
* @return0 on success, negative AVERROR on failure.
*
* @seeav_opt_find, av_dict_set, avio_open, av_oformat_next.
*/
int avformat_write_header(AVFormatContext *s,AVDictionary **options);简单解释一下它的参数的含义:
s:用于输出的AVFormatContext。
options:额外的选项,一般为NULL。
函数正常执行后返回值等于0。
函数调用关系图
avformat_write_header()的调用关系如下图所示。
avformat_write_header()主要完成了以下工作:
(1)调用init_muxer()初始化复用器
(2)调用AVOutputFormat的write_header()
init_muxer()
init_muxer()用于初始化复用器,它所做的工作比较简单,可以概括成两个字:检查。函数的流程可以概括成以下几步:
(1)将传入的AVDictionary形式的选项设置到AVFormatContext
(2)遍历AVFormatContext中的每个AVStream,并作如下检查:
a)AVStream的time_base是否正确设置。如果发现AVStream的time_base没有设置,则会调用avpriv_set_pts_info()进行设置。
b)对于音频,检查采样率设置是否正确;对于视频,检查宽、高、宽高比。
c)其他一些检查,不再详述。
AVOutputFormat->write_header()
avformat_write_header()中最关键的地方就是调用了AVOutputFormat的write_header()。write_header()是AVOutputFormat中的一个函数指针,指向写文件头的函数。不同的AVOutputFormat有不同的write_header()的实现方法。在这里我们举例子看一下FLV封装格式对应的AVOutputFormat,它的定义位于libavformat\flvenc.c,如下所示。
AVOutputFormat ff_flv_muxer = {
.name = "flv",
.long_name =NULL_IF_CONFIG_SMALL("FLV (Flash Video)"),
.mime_type ="video/x-flv",
.extensions = "flv",
.priv_data_size = sizeof(FLVContext),
.audio_codec =CONFIG_LIBMP3LAME ? AV_CODEC_ID_MP3 : AV_CODEC_ID_ADPCM_SWF,
.video_codec =AV_CODEC_ID_FLV1,
.write_header =flv_write_header,
.write_packet =flv_write_packet,
.write_trailer =flv_write_trailer,
.codec_tag = (constAVCodecTag* const []) {
flv_video_codec_ids,flv_audio_codec_ids, 0
},
.flags =AVFMT_GLOBALHEADER | AVFMT_VARIABLE_FPS |
AVFMT_TS_NONSTRICT,
};
从ff_flv_muxer的定义中可以看出,write_header()指向的函数为flv_write_header()。我们继续看一下flv_write_header()函数。flv_write_header()的定义同样位于libavformat\flvenc.c。
flv_write_header()完成了FLV文件头的写入工作。该函数的工作可以大体分为以下两部分:
(1)给FLVContext设置参数
(2)写文件头,以及相关的Tag
写文件头的代码很短,如下所示。
//开始写入
//Signature
avio_write(pb, "FLV", 3);
//Version
avio_w8(pb,1);
//“!!”意思是把非0转换成1
//Flags
avio_w8(pb, FLV_HEADER_FLAG_HASAUDIO * !!flv->audio_enc +
FLV_HEADER_FLAG_HASVIDEO * !!flv->video_enc);
//Header size
avio_wb32(pb, 9);
//Header结束
//Previous Tag Size
avio_wb32(pb,0);
可以参考下图中FLV文件头的定义比对一下上面的代码。
av_read_frame()
ffmpeg中的av_read_frame()的作用是读取码流中的音频若干帧或者视频一帧。例如,解码视频的时候,每解码一个视频帧,需要先调用av_read_frame()获得一帧视频的压缩数据,然后才能对该数据进行解码(例如H.264中一帧压缩数据通常对应一个NAL)。
av_read_frame()的声明位于libavformat\avformat.h,如下所示。
/**
* Returnthe next frame of a stream.
* Thisfunction returns what is stored in the file, and does not validate
* thatwhat is there are valid frames for the decoder. It will split what is
* storedin the file into frames and return one for each call. It will not
* omitinvalid data between valid frames so as to give the decoder the maximum
*information possible for decoding.
*
* Ifpkt->buf is NULL, then the packet is valid until the next
*av_read_frame() or until avformat_close_input(). Otherwise the packet
* is validindefinitely. In both cases the packet must be freed with
*av_free_packet when it is no longer needed. For video, the packet contains
* exactlyone frame. For audio, it contains an integer number of frames if each
* framehas a known fixed size (e.g. PCM or ADPCM data). If the audio frames
* have avariable size (e.g. MPEG audio), then it contains one frame.
*
*pkt->pts, pkt->dts and pkt->duration are always set to correct
* valuesin AVStream.time_base units (and guessed if the format cannot
* providethem). pkt->pts can be AV_NOPTS_VALUE if the video format
* hasB-frames, so it is better to rely on pkt->dts if you do not
*decompress the payload.
*
* @return0 if OK, < 0 on error or end of file
*/
int av_read_frame(AVFormatContext *s, AVPacket*pkt);av_read_frame()使用方法在注释中写得很详细,用中文简单描述一下它的两个参数:
s:输入的AVFormatContext
pkt:输出的AVPacket
如果返回0则说明读取正常。
函数调用结构图
函数调用结构图如下所示。
read_frame_internal()
av_read_frame()调用了read_frame_internal()。有2步是十分关键的:
(1)调用了ff_read_packet()从相应的AVInputFormat读取数据。
(2)如果媒体频流需要使用AVCodecParser,则调用parse_packet()解析相应的AVPacket。
下面我们分成分别看一下ff_read_packet()和parse_packet()的源代码。
ff_read_packet()
ff_read_packet()中最关键的地方就是调用了AVInputFormat的read_packet()方法。AVInputFormat的read_packet()是一个函数指针,指向当前的AVInputFormat的读取数据的函数。在这里我们以FLV封装格式对应的AVInputFormat为例,看看read_packet()的实现函数是什么样子的。
FLV封装格式对应的AVInputFormat的定义位于libavformat\flvdec.c,如下所示。
AVInputFormat ff_flv_demuxer = {
.name = "flv",
.long_name =NULL_IF_CONFIG_SMALL("FLV (Flash Video)"),
.priv_data_size = sizeof(FLVContext),
.read_probe = flv_probe,
.read_header = flv_read_header,
.read_packet = flv_read_packet,
.read_seek = flv_read_seek,
.read_close = flv_read_close,
.extensions = "flv",
.priv_class = &flv_class,
};
从ff_flv_demuxer的定义可以看出,read_packet()对应的是flv_read_packet()函数。在看flv_read_packet()函数之前,我们先回顾一下FLV封装格式的结构,如下图所示。
从图中可以看出,FLV文件体部分是由一个一个的Tag连接起来的(中间间隔着PreviousTag Size)。每个Tag包含了Tag Header和Tag Data两个部分。Tag Data根据Tag的Type不同而不同:可以分为音频TagData,视频TagData以及ScriptTag Data。下面简述一下音频TagData和视频TagData。
Audio Tag Data
Audio Tag在官方标准中定义如下。
Audio Tag开始的第1个字节包含了音频数据的参数信息,从第2个字节开始为音频流数据。
第1个字节的前4位的数值表示了音频数据格式:
0 = Linear PCM, platform endian
1 = ADPCM
2 = MP3
3 = Linear PCM, little endian
4 = Nellymoser 16-kHz mono
5 = Nellymoser 8-kHz mono
6 = Nellymoser
7 = G.711 A-law logarithmic PCM
8 = G.711 mu-law logarithmic PCM
9 = reserved
10 = AAC
14 = MP3 8-Khz
15 = Device-specific sound
第1个字节的第5-6位的数值表示采样率:0 = 5.5kHz,1 = 11KHz,2 = 22 kHz,3 = 44 kHz。
第1个字节的第7位表示采样精度:0 = 8bits,1 = 16bits。
第1个字节的第8位表示音频类型:0 = sndMono,1 = sndStereo。
其中,当音频编码为AAC的时候,第一个字节后面存储的是AACAUDIODATA,格式如下所示。
Video Tag Data
Video Tag在官方标准中的定义如下。
Video Tag也用开始的第1个字节包含视频数据的参数信息,从第2个字节为视频流数据。
第1个字节的前4位的数值表示帧类型(FrameType):
1: keyframe (for AVC, a seekableframe)(关键帧)
2: inter frame (for AVC, a nonseekableframe)
3: disposable inter frame (H.263only)
4: generated keyframe (reservedfor server useonly)
5: video info/command frame
第1个字节的后4位的数值表示视频编码ID(CodecID):
1: JPEG (currently unused)
2: Sorenson H.263
3: Screen video
4: On2 VP6
5: On2 VP6 with alpha channel
6: Screen video version 2
7: AVC
其中,当音频编码为AVC(H.264)的时候,第一个字节后面存储的是AVCVIDEOPACKET,格式如下所示。
关键代码:
/* pkt size is repeated at end. skip it */
for (;;avio_skip(s->pb, 4)) {
pos = avio_tell(s->pb);
//解析Tag Header==========
//Tag类型
type = (avio_r8(s->pb) & 0x1F);
//Datasize数据大小
size = avio_rb24(s->pb);
//Timstamp时间戳
dts = avio_rb24(s->pb);
dts|= avio_r8(s->pb) << 24;
…
avio_skip(s->pb, 3); /* stream id, always 0 */
…
if (type == FLV_TAG_TYPE_AUDIO) {
//Type是音频
stream_type = FLV_STREAM_TYPE_AUDIO;
//Tag Data的第一个字节
flags = avio_r8(s->pb);
size--;
}else if (type == FLV_TAG_TYPE_VIDEO) {
//Type是音频
stream_type = FLV_STREAM_TYPE_VIDEO;
//Tag Data的第一个字节
flags = avio_r8(s->pb);
size--;
if ((flags & FLV_VIDEO_FRAMETYPE_MASK) == FLV_FRAME_VIDEO_INFO_CMD)
goto skip;
}
…
//几种特殊的格式
if(st->codec->codec_id == AV_CODEC_ID_AAC ||
st->codec->codec_id == AV_CODEC_ID_H264 ||
st->codec->codec_id == AV_CODEC_ID_MPEG4) {
//对应AACPacketType或者AVCPacketType
inttype = avio_r8(s->pb);
size--;
//H.264
if(st->codec->codec_id == AV_CODEC_ID_H264 || st->codec->codec_id ==AV_CODEC_ID_MPEG4) {
// sign extension
//对应CompositionTime
int32_t cts = (avio_rb24(s->pb) + 0xff800000) ^ 0xff800000;
//计算PTS
pts = dts + cts;
if (cts < 0) { // dts might be wrong
…
flv->wrong_dts = 1;
} else if (FFABS(dts - pts) > 1000*60*15) {
…
dts = pts = AV_NOPTS_VALUE;
}
}
//如果编码器是AAC或者H.264
if(type == 0 && (!st->codec->extradata || st->codec->codec_id== AV_CODEC_ID_AAC ||
st->codec->codec_id ==AV_CODEC_ID_H264)) {
AVDictionaryEntry *t;
if (st->codec->extradata) {
if ((ret = flv_queue_extradata(flv, s->pb, stream_type, size)) <0)
return ret;
ret = AVERROR(EAGAIN);
goto leave;
}
if ((ret = flv_get_extradata(s, st, size)) < 0)
return ret;
/* Workaround for buggy Omnia A/XE encoder */
t = av_dict_get(s->metadata, "Encoder", NULL, 0);
if (st->codec->codec_id == AV_CODEC_ID_AAC && t &&!strcmp(t->value, "Omnia A/XE"))
st->codec->extradata_size = 2;
//AAC
if (st->codec->codec_id == AV_CODEC_ID_AAC && 0) {
MPEG4AudioConfig cfg;
if (avpriv_mpeg4audio_get_config(&cfg, st->codec->extradata,
st->codec->extradata_size * 8, 1) >= 0) {
st->codec->channels =cfg.channels;
st->codec->channel_layout = 0;
if (cfg.ext_sample_rate)
st->codec->sample_rate = cfg.ext_sample_rate;
else
st->codec->sample_rate = cfg.sample_rate;
av_dlog(s, "mp4a config channels %d sample rate %d\n",
st->codec->channels, st->codec->sample_rate);
}
}
ret = AVERROR(EAGAIN);
goto leave;
}
}
…
ret =av_get_packet(s->pb, pkt, size);
av_write_frame()
av_write_frame()用于输出一帧视音频数据,它的声明位于libavformat\avformat.h,如下所示。
/**
* Write apacket to an output media file.
*
* Thisfunction passes the packet directly to the muxer, without any buffering
* orreordering. The caller is responsible for correctly interleaving the
* packetsif the format requires it. Callers that want libavformat to handle
* theinterleaving should call av_interleaved_write_frame() instead of this
*function.
*
* @param smedia file handle
* @parampkt The packet containing the data to be written. Note that unlike
* av_interleaved_write_frame(), thisfunction does not take
* ownership of the packet passed toit (though some muxers may make
* an internal reference to the inputpacket).
* <br>
* This parameter can be NULL (at anytime, not just at the end), in
* order to immediately flush databuffered within the muxer, for
* muxers that buffer up datainternally before writing it to the
* output.
* <br>
* Packet's @ref AVPacket.stream_index"stream_index" field must be
* set to the index of thecorresponding stream in @ref
* AVFormatContext.streams"s->streams". It is very strongly
* recommended that timing information(@ref AVPacket.pts "pts", @ref
* AVPacket.dts "dts", @refAVPacket.duration "duration") is set to
* correct values.
* @return< 0 on error, = 0 if OK, 1 if flushed and there is no more data to flush
*
* @seeav_interleaved_write_frame()
*/
int av_write_frame(AVFormatContext *s, AVPacket*pkt);
简单解释一下它的参数的含义:
s:用于输出的AVFormatContext。
pkt:等待输出的AVPacket。
函数正常执行后返回值等于0。
函数调用关系图
av_write_frame()的调用关系如下图所示。
av_write_frame()主要完成了以下几步工作:
(1)调用prepare_input_packet()-> check_packet()做一些简单的检测
(2)调用compute_pkt_fields2()设置AVPacket的一些属性值
(3)调用write_packet()写入数据
下面分别看一下flv_write_packet()。需要先回顾一下前文的FLV封装格式。
flv_write_packet()
下面我们看一下FLV格式中write_packet()对应的实现函数flv_write_packet(),位于libavformat\flvenc.c。
flv_write_packet()在写入H.264/AAC时候的流程:
(1)写入Tag Header的Type,如果是视频,代码如下:
avio_w8(pb, FLV_TAG_TYPE_VIDEO);
如果是音频,代码如下:
avio_w8(pb, FLV_TAG_TYPE_AUDIO);
(2)写入Tag Header的Datasize,Timestamp和StreamID(至此完成Tag Header):
//TagHeader - Datasize
avio_wb24(pb, size + flags_size);
//TagHeader - Timestamp
avio_wb24(pb, ts & 0xFFFFFF);
avio_w8(pb, (ts >> 24) & 0x7F); // timestamps are 32 bits_signed_
//StreamID
avio_wb24(pb,flv->reserved);
(3)写入Tag Data的第一字节(其中flag已经在前面的代码中设置完毕):
//FirstByte of Tag Data
avio_w8(pb,flags);
(4)如果编码格式VP6作相应的处理(不研究);编码格式为AAC,写入AACAUDIODATA;编码格式为H.264,写入AVCVIDEOPACKET:
if(enc->codec_id == AV_CODEC_ID_VP6F || enc->codec_id == AV_CODEC_ID_VP6A){
if (enc->extradata_size)
avio_w8(pb, enc->extradata[0]);
else
avio_w8(pb, ((FFALIGN(enc->width, 16) - enc->width) << 4) |
(FFALIGN(enc->height, 16) - enc->height));
}else if (enc->codec_id == AV_CODEC_ID_AAC)
avio_w8(pb, 1); // AAC raw
else if (enc->codec_id == AV_CODEC_ID_H264 || enc->codec_id ==AV_CODEC_ID_MPEG4) {
//AVCVIDEOPACKET-AVCPacketType
avio_w8(pb, 1); // AVC NALU
//AVCVIDEOPACKET-CompositionTime
avio_wb24(pb, pkt->pts - pkt->dts);
}
(5)写入数据:
//Data
avio_write(pb, data ? data : pkt->data, size);
(6) 写入previous tag size:
avio_wb32(pb, size + flags_size + 11); //previous tag size
至此,flv_write_packet()就完成了一个Tag的写入。
av_write_trailer()
av_write_trailer()用于输出文件尾,它的声明位于libavformat\avformat.h,如下所示。
/**
* Writethe stream trailer to an output media file and free the
* fileprivate data.
*
* May onlybe called after a successful call to avformat_write_header.
*
* @param smedia file handle
* @return0 if OK, AVERROR_xxx on error
*/
int av_write_trailer(AVFormatContext *s);
它只需要指定一个参数,即用于输出的AVFormatContext。
函数正常执行后返回值等于0。
函数调用关系图
av_write_trailer()的调用关系如下图所示。
av_write_trailer()
av_write_trailer()的定义位于libavformat\mux.c,主要完成了以下两步工作:
(1)循环调用interleave_packet()以及write_packet(),将还未输出的AVPacket输出出来。
(2)调用AVOutputFormat的write_trailer(),输出文件尾。
其中第一步和av_write_frame()中的步骤大致是一样的(interleave_packet()这一部分在并不包含在av_write_frame()中,而是包含在av_interleaved_write_frame()中,这一部分源代码还没有分析)。
flv_write_trailer()
flv_write_trailer()函数的定义位于libavformat\flvenc.c。该函数做了以下两步工作:
(1)如果视频流是H.264,则添加包含EOS(End Of Stream) NALU的Tag。
(2)更新FLV的时长信息,以及文件大小信息。
其中,put_avc_eos_tag()函数用于添加包含EOS NALU的Tag(包含结尾的一个PreviousTagSize),),如下所示。
static void put_avc_eos_tag(AVIOContext *pb,unsigned ts)
{
avio_w8(pb, FLV_TAG_TYPE_VIDEO);
avio_wb24(pb, 5); /*Tag Data Size */
avio_wb24(pb, ts); /*lower 24 bits of timestamp in ms */
avio_w8(pb, (ts >> 24) & 0x7F); /* MSB of ts in ms */
avio_wb24(pb, 0); /*StreamId = 0 */
avio_w8(pb, 23); /*ub[4] FrameType = 1, ub[4] CodecId = 7 */
avio_w8(pb, 2); /*AVC end of sequence */
avio_wb24(pb, 0); /*Always 0 for AVC EOS. */
avio_wb32(pb, 16); /*Size of FLV tag */
}
可以参考FLV封装格式理解上述函数。由于前面的文章中已经描述过FLV封装格式,在这里不再重复叙述,在这里仅在此记录一下AVCVIDEOPACKET的格式,如下所示。
可以看出包含EOSNALU的AVCVIDEOPACKET的AVCPacketType为2。在这种情况下,AVCVIDEOPACKET的CompositionTime字段取0,并且无需包含Data字段。