视频的显示和存放原理
对于一个电影,帧是这样来显示的:I B B P。现在我们需要在显示B帧之前知道P帧中的信息。因此,帧可能会按照这样的方式来存储:IPBB。这就是为什么我们会有一个解码时间戳和一个显示时间戳的原因。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示。所以,在这种情况下,我们的流可以是这样的:
PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B
通常PTS和DTS只有在流中有B帧的时候会不同。
DTS和PTS
音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面。音频流有采样,视频流有每秒的帧率。然而,如果我们只是简单的通过数帧和乘以帧率的方式来同步视频,那么就很有可能会失去同步。于是作为一种补充,在流中的包有种叫做DTS(解码时间戳)和PTS(显示时间戳)的机制。为了这两个参数,你需要了解电影存放的方式。像MPEG等格式,使用被叫做B帧(B表示双向bidrectional)的方式。另外两种帧被叫做I帧和P帧(I表示关键帧,P表示预测帧)。I帧包含了某个特定的完整图像。P帧依赖于前面的I帧和P帧并且使用比较或者差分的方式来编码。B帧与P帧有点类似,但是它是依赖于前面和后面的帧的信息的。这也就解释了为什么我们可能在调用avcodec_decode_video以后会得不到一帧图像。
ffmpeg中的时间单位
AV_TIME_BASE
ffmpeg中的内部计时单位(时间基),ffmepg中的所有时间都是于它为一个单位,比如AVStream中的duration即以为着这个流的长度为duration个AV_TIME_BASE。AV_TIME_BASE定义为:
#define AV_TIME_BASE 1000000
AV_TIME_BASE_Q
ffmpeg内部时间基的分数表示,实际上它是AV_TIME_BASE的倒数。从它的定义能很清楚的看到这点:
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
AVRatioal的定义如下:
typedef struct AVRational{
int num; // numerator
int den; // denominator
} AVRational;
ffmpeg提供了一个把AVRatioal结构转换成double的函数:
static inline double av_q2d (AVRational a){
/* *
* Convert rational to double.
* @param a rational to convert
* */
return a.num / (double ) a.den;
}
现在可以根据pts来计算一桢在整个视频中的时间位置:
timestamp(秒) = pts * av_q2d (st->time_base)
计算视频长度的方法:
time(秒) = st->duration * av_q2d (st->time_base)
这里的st是一个AVStream对象指针。
时间基转换公式
timestamp(ffmpeg内部时间戳) = AV_TIME_BASE * time(秒) time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg内部时间戳)
所以当需要把视频跳转到N秒的时候可以使用下面的方法:
int64_t timestamp = N * AV_TIME_BASE;
2
av_seek_frame(fmtctx, index_of_video, timestamp, AVSEEK_FLAG_BACKWARD);
ffmpeg同样为我们提供了不同时间基之间的转换函数:
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)
这个函数的作用是计算a * bq / cq,来把时间戳从一个时基调整到另外一个时基。在进行时基转换的时候,我们应该首选这个函数,因为它可以避免溢出的情况发生。
FFmpeg里有两种时间戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。 顾名思义,前者是解码的时间,后者是显示的时间。要仔细理解这两个概念,需要先了解FFmpeg中的packet和frame的概念。
FFmpeg中用AVPacket结构体来描述解码前或编码后的压缩包,用AVFrame结构体来描述解码后或编码前的信号帧。 对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。 如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如H.264或HEVC)中,编码顺序和输入顺序并不一致。 于是才会需要PTS和DTS这两种不同的时间戳。
FFMPEG之TimeBase成员理解
FFMPEG的很多结构中有AVRational time_base;这样的一个成员,它是AVRational结构的
typedef struct AVRational{ int num; ///< numerator int den; ///< denominator } AVRational;
AVRational这个结构标识一个分数,num为分数,den为分母。
实际上time_base的意思就是时间的刻度:
如(1,25),那么时间刻度就是1/25
(1,9000),那么时间刻度就是1/90000
那么,在刻度为1/25的体系下的time=5,转换成在刻度为1/90000体系下的时间time为(5*1/25)/(1/90000) = 3600*5=18000
ffmpeg中做pts计算时,存在大量这种转换
在以下结构中都有
AVCodecContext:编解码上下文。
AVStream:文件或其它容器中的某一个track。
如果由某个解码器产生固定帧率的码流
AVCodecContext中的AVRational根据帧率来设定,如25帧,那么num = 1,den=25
AVStream中的time_base一般根据其采样频率设定,如(1,90000)
在某些场景下涉及到PTS的计算时,就涉及到两个Time的转换,以及到底取哪里的time_base进行转换:
场景1:编码器产生的帧,直接存入某个容器的AVStream中,那么此时packet的Time要从AVCodecContext的time转换成目标AVStream的time
场景2:从一种容器中demux出来的源AVStream的frame,存入另一个容器中某个目的AVStream。
此时的时间刻度应该从源AVStream的time,转换成目的AVStream timebase下的时间。
其实,问题的关键还是要理解,不同的场景下取到的数据帧的time是相对哪个时间体系的。
demux出来的帧的time:是相对于源AVStream的timebase
编码器出来的帧的time:是相对于源AVCodecContext的timebase
mux存入文件等容器的time:是相对于目的AVStream的timebase
这里的time指pts。
2014-10-09 00:47
18367人阅读
收藏
举报
分类:
FFMPEG(137)
我的开源项目(66)
版权声明:本文为博主原创文章,未经博主允许不得转载。
=====================================================
最简单的基于FFmpeg的封装格式处理系列文章列表:
最简单的基于FFmpeg的封装格式处理:视音频分离器简化版(demuxer-simple)
最简单的基于FFmpeg的封装格式处理:视音频分离器(demuxer)
最简单的基于FFmpeg的封装格式处理:视音频复用器(muxer)
最简单的基于FFMPEG的封装格式处理:封装格式转换(remuxer)
=====================================================
简介
打算记录一下基于FFmpeg的封装格式处理方面的例子。包括了视音频分离,复用,封装格式转换。这是第3篇。
本文记录一个基于FFmpeg的视音频复用器(Simplest FFmpeg muxer)。视音频复用器(Muxer)即是将视频压缩数据(例如H.264)和音频压缩数据(例如AAC)合并到一个封装格式数据(例如MKV)中去。如图所示。在这个过程中并不涉及到编码和解码。
本文记录的程序将一个H.264编码的视频码流文件和一个MP3编码的音频码流文件,合成为一个MP4封装格式的文件。
流程 程序的流程如下图所示。从流程图中可以看出,一共初始化了3个AVFormatContext,其中2个用于输入,1个用于输出。3个AVFormatContext初始化之后,通过avcodec_copy_context()函数可以将输入视频/音频的参数拷贝至输出视频/音频的AVCodecContext结构体。然后分别调用视频输入流和音频输入流的av_read_frame(),从视频输入流中取出视频的AVPacket,音频输入流中取出音频的AVPacket,分别将取出的AVPacket写入到输出文件中即可。其间用到了一个不太常见的函数av_compare_ts(),是比较时间戳用的。通过该函数可以决定该写入视频还是音频。
本文介绍的视音频复用器,输入的视频不一定是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():写入文件尾。
代码
下面贴上代码:
#include <stdio.h> #define __STDC_CONSTANT_MACROS #ifdef _WIN32 extern "C" { #include "libavformat/avformat.h" }; #else #ifdef __cplusplus extern "C" { #endif #include <libavformat/avformat.h> #ifdef __cplusplus }; #endif #endif #define USE_H264BSF 0 #define USE_AACBSF 0 int main( int argc, char * argv[]) { AVOutputFormat *ofmt = NULL; AVFormatContext *ifmt_ctx_v = NULL, *ifmt_ctx_a = NULL,*ofmt_ctx = NULL; AVPacket pkt; int ret, i; int videoindex_v=-1,videoindex_out=-1; int audioindex_a=-1,audioindex_out=-1; int frame_index=0; int64_t cur_pts_v=0,cur_pts_a=0; const char *in_filename_v = "cuc_ieschool.h264" ; const char *in_filename_a = "huoyuanjia.mp3" ; const char *out_filename = "cuc_ieschool.mp4" ; av_register_all(); if ((ret = avformat_open_input(&ifmt_ctx_v, in_filename_v, 0, 0)) < 0) { printf( "Could not open input file." ); goto end; } if ((ret = avformat_find_stream_info(ifmt_ctx_v, 0)) < 0) { printf( "Failed to retrieve input stream information" ); goto end; } if ((ret = avformat_open_input(&ifmt_ctx_a, in_filename_a, 0, 0)) < 0) { printf( "Could not open input file." ); goto end; } if ((ret = avformat_find_stream_info(ifmt_ctx_a, 0)) < 0) { printf( "Failed to retrieve input stream information" ); goto end; } printf("===========Input Information==========\n" ); av_dump_format(ifmt_ctx_v, 0, in_filename_v, 0); av_dump_format(ifmt_ctx_a, 0, in_filename_a, 0); printf("======================================\n" ); avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename); if (!ofmt_ctx) { printf( "Could not create output context\n" ); ret = AVERROR_UNKNOWN; goto end; } ofmt = ofmt_ctx->oformat; for (i = 0; i < ifmt_ctx_v->nb_streams; i++) { if (ifmt_ctx_v->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){ AVStream *in_stream = ifmt_ctx_v->streams[i]; AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec); videoindex_v=i; if (!out_stream) { printf( "Failed allocating output stream\n" ); ret = AVERROR_UNKNOWN; goto end; } videoindex_out=out_stream->index; if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) { printf( "Failed to copy context from input to output stream codec context\n" ); goto end; } out_stream->codec->codec_tag = 0; if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; break ; } } for (i = 0; i < ifmt_ctx_a->nb_streams; i++) { if (ifmt_ctx_a->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO){ AVStream *in_stream = ifmt_ctx_a->streams[i]; AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec); audioindex_a=i; if (!out_stream) { printf( "Failed allocating output stream\n" ); ret = AVERROR_UNKNOWN; goto end; } audioindex_out=out_stream->index; if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) { printf( "Failed to copy context from input to output stream codec context\n" ); goto end; } out_stream->codec->codec_tag = 0; if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; break ; } } printf("==========Output Information==========\n" ); av_dump_format(ofmt_ctx, 0, out_filename, 1); printf("======================================\n" ); if (!(ofmt->flags & AVFMT_NOFILE)) { if (avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE) < 0) { printf( "Could not open output file '%s'" , out_filename); goto end; } } if (avformat_write_header(ofmt_ctx, NULL) < 0) { printf( "Error occurred when opening output file\n" ); goto end; } #if USE_H264BSF AVBitStreamFilterContext* h264bsfc = av_bitstream_filter_init("h264_mp4toannexb" ); #endif #if USE_AACBSF AVBitStreamFilterContext* aacbsfc = av_bitstream_filter_init("aac_adtstoasc" ); #endif while (1) { AVFormatContext *ifmt_ctx; int stream_index=0; AVStream *in_stream, *out_stream; if (av_compare_ts(cur_pts_v,ifmt_ctx_v->streams[videoindex_v]->time_base,cur_pts_a,ifmt_ctx_a->streams[audioindex_a]->time_base) <= 0){ ifmt_ctx=ifmt_ctx_v; stream_index=videoindex_out; if (av_read_frame(ifmt_ctx, &pkt) >= 0){ do { in_stream = ifmt_ctx->streams[pkt.stream_index]; out_stream = ofmt_ctx->streams[stream_index]; if (pkt.stream_index==videoindex_v){ if (pkt.pts==AV_NOPTS_VALUE){ AVRational time_base1=in_stream->time_base; int64_t calc_duration=(double )AV_TIME_BASE/av_q2d(in_stream->r_frame_rate); 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); frame_index++; } cur_pts_v=pkt.pts; break ; } }while (av_read_frame(ifmt_ctx, &pkt) >= 0); }else { break ; } }else { ifmt_ctx=ifmt_ctx_a; stream_index=audioindex_out; if (av_read_frame(ifmt_ctx, &pkt) >= 0){ do { in_stream = ifmt_ctx->streams[pkt.stream_index]; out_stream = ofmt_ctx->streams[stream_index]; if (pkt.stream_index==audioindex_a){ if (pkt.pts==AV_NOPTS_VALUE){ AVRational time_base1=in_stream->time_base; int64_t calc_duration=(double )AV_TIME_BASE/av_q2d(in_stream->r_frame_rate); 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); frame_index++; } cur_pts_a=pkt.pts; break ; } }while (av_read_frame(ifmt_ctx, &pkt) >= 0); }else { break ; } } #if USE_H264BSF av_bitstream_filter_filter(h264bsfc, in_stream->codec, NULL, &pkt.data, &pkt.size, pkt.data, pkt.size, 0); #endif #if USE_AACBSF av_bitstream_filter_filter(aacbsfc, out_stream->codec, NULL, &pkt.data, &pkt.size, pkt.data, pkt.size, 0); #endif pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX)); pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(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; pkt.stream_index=stream_index; printf("Write 1 Packet. size:%5d\tpts:%lld\n" ,pkt.size,pkt.pts); if (av_interleaved_write_frame(ofmt_ctx, &pkt) < 0) { printf( "Error muxing packet\n" ); break ; } av_free_packet(&pkt); } av_write_trailer(ofmt_ctx); #if USE_H264BSF av_bitstream_filter_close(h264bsfc); #endif #if USE_AACBSF av_bitstream_filter_close(aacbsfc); #endif end: avformat_close_input(&ifmt_ctx_v); avformat_close_input(&ifmt_ctx_a); 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; } return 0; }
结果 输入文件为:
视频:cuc_ieschool.ts
音频:huoyuanjia.mp3
输出文件为:
cuc_ieschool.mp4
输出的文件视频为“cuc_ieschool”,配合“霍元甲”的音频。
下载
simplest ffmpeg format
项目主页
SourceForge:https://sourceforge.net/projects/simplestffmpegformat/
Github:https://github.com/leixiaohua1020/simplest_ffmpeg_format
开源中国:http://git.oschina.net/leixiaohua1020/simplest_ffmpeg_format
CSDN下载:
http://download.csdn.net/detail/leixiaohua1020/8005317
工程中包含4个例子:
simplest_ffmpeg_demuxer_simple:视音频分离器(简化版)。
simplest_ffmpeg_demuxer:视音频分离器。
simplest_ffmpeg_muxer:视音频复用器。
simplest_ffmpeg_remuxer:封装格式转换器。
更新-1.1==================================================
修复了以下问题:
(1)Release版本下的运行问题
(2)simplest_ffmpeg_muxer封装H.264裸流的时候丢失声音的错误
关于simplest_ffmpeg_muxer封装H.264裸流的时候丢失声音的问题目前已经解决。根源在于H.264裸流没有PTS,因此必须手动写入PTS。写入PTS的代码在在旧版本中已经包含:
if (pkt.pts==AV_NOPTS_VALUE){ AVRational time_base1=in_stream->time_base; int64_t calc_duration=(double )AV_TIME_BASE/av_q2d(in_stream->r_frame_rate); 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); frame_index++; }
但是旧版本中这段代码的位置放错了,应该放在av_read_frame()之后,cur_pts_a/cur_pts_v赋值之前。换句话说,也就说要把这段代码“前移”。修改后问题解决。
CSDN下载地址:
http://download.csdn.net/detail/leixiaohua1020/8284309
更新-1.2 (2015.2.13)=========================================
这次考虑到了跨平台的要求,调整了源代码。经过这次调整之后,源代码可以在以下平台编译通过:
VC++:打开sln文件即可编译,无需配置。
cl.exe:打开compile_cl.bat即可命令行下使用cl.exe进行编译,注意可能需要按照VC的安装路径调整脚本里面的参数。编译命令如下。
::VS2010 Environment call "D:\Program Files\Microsoft Visual Studio 10.0\VC\vcvarsall.bat" ::include @set INCLUDE=include;%INCLUDE% ::lib @set LIB=lib;%LIB% ::compile and link cl simplest_ffmpeg_muxer.cpp /link avcodec.lib avformat.lib avutil.lib ^ avdevice.lib avfilter.lib postproc.lib swresample.lib swscale.lib /OPT:NOREF
MinGW:MinGW命令行下运行compile_mingw.sh即可使用MinGW的g++进行编译。编译命令如下。
g++ simplest_ffmpeg_muxer.cpp -g -o simplest_ffmpeg_muxer.exe \ -I /usr/local/include -L /usr/local/lib -lavformat -lavcodec -lavutil
GCC:Linux或者MacOS命令行下运行compile_gcc.sh即可使用GCC进行编译。编译命令如下。
视频、音频打时间戳的方法
一 固定帧率
1. 视频时间戳
pts = inc++ *(1000/fps); 其中inc是一个静态的,初始值为0,每次打完时间戳inc加1.
在ffmpeg,中的代码 为
pkt.pts= m_nVideoTimeStamp++ * (m_VCtx->time_base.num * 1000 / m_VCtx->time_base.den);
2. 音频时间戳
pts = inc++ * (frame_size * 1000 / sample_rate)
在ffmpeg中的代码为
pkt.pts= m_nAudioTimeStamp++ * (m_ACtx->frame_size * 1000 / m_ACtx->sample_rate);
采样频率是指将模拟声音波形进行数字化时,每秒钟抽取声波幅度样本的次数。
。正常人听觉的频率范围大约在20Hz~20kHz之间,根据奈奎斯特采样理论,为了保证声音不失真,采样频率应该在40kHz左右。常用的音频采样频率有8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz等,如果采用更高的采样频率,还可以达到DVD的音质
对采样率为44.1kHz的AAC音频进行解码时,一帧的解码时间须控制在23.22毫秒内。
背景知识:
(一个AAC原始帧包含一段时间内1024个采样及相关数据)
分析:
1 AAC
音频帧的播放时间=一个AAC帧对应的采样样本的个数/采样频率(单位为s)
一帧 1024个 sample。采样率 Samplerate 44100KHz,每秒44100个sample, 所以 根据公式 音频帧的播放时间=一个AAC帧对应的采样样本的个数/采样频率
当前AAC一帧的播放时间是= 1024*1000000/44100= 22.32ms(单位为ms)
2 MP3
mp3 每帧均为1152个字节, 则:
frame_duration = 1152 * 1000000 / sample_rate
例如:sample_rate = 44100HZ时, 计算出的时长为26.122ms,这就是经常听到的mp3每帧播放时间固定为26ms的由来。
二 可变帧率
有很多的采集卡,摄像头,在做采集的时候,明明设置的25FPS,但实际采集数据回调过来,发现并不是40毫秒的间隔,而是50,60,甚至100不等的时间间隔。
这就给编码后打时间戳带来很大的困难。
在libav里,我们的默认编码参数都是:
ptAvEncoder->ptAvStreamVideo->codec->time_base.den = s32Fps;
ptAvEncoder->ptAvStreamVideo->codec->time_base.num = 1;
这样在编码后的时间戳以1递增,只适合于固定帧率。
我们来改一下:
ptAvEncoder->ptAvStreamVideo->codec->time_base.den = s32Fps * 1000;
ptAvEncoder->ptAvStreamVideo->codec->time_base.num = 1* 1000;
这样就把时间戳的scale变成了毫秒,就可以以毫秒为单位进行计算了,如下:
tAvPacket.pts = ((s64)u32TimeStamp * (s64)s32Fps);
u32TimeStamp是从开始记录的时间差值,以毫秒为单位;s32Fps是帧率。
对于音频,mp4文件默认是采样率为tick的,时间戳计算为:
tAvPacket.pts = (AvEncoderAudioInSizeGet(hHandle) * ( (s64)(u32TimeStamp)) / (AvEncoderAudioInSizeGet(hHandle) * 1000 / ptAvEncoder->ptAvStreamAudio->codec->sample_rate);
AvEncoderAudioInSizeGet(hHandle) 每次编码器需要的PCM数据长度。
u32TimeStamp是从开始记录的时间差值,以毫秒为单位。
ptAvEncoder->ptAvStreamAudio->codec->sample_rate PCM采样率,代表一秒的数据量。
因为乘以了1000,所以也化成了毫秒单位。
对于mp4,视频直接用绝对时间,音频用数据量,对rtmp,视频是毫秒计算,音频也换算成毫秒计算