有关基础知识
复用器中最难的点是 时间戳概念下边是有关的一些基本概念:
采样率
音视频现在采用的数字编码方法,简单说就是把音视频这种波形和图像进行采集,量化,编码,传输,解码。所以采样率就是每秒钟抽取图像或者声波幅度样本的次数。比如音频采样率8k,就是表示把波形进行每秒8000次采样。
我们看到一秒的采样频率其实挺大的,至于这个值是多少合理,其实无论视频还是音频都和人的视觉特征和听觉特征有关系。
对于人的视觉而言,只要1秒钟播放的视频达到25帧以上,我们就看到了连续的图像即为视频。如果低于这个值,我们人眼就能感觉出来卡顿。
对于人的听觉而言,正常的听觉频率范围在20Hz-20kHz,根据奎斯特采样理论,为了保证音频不失真,我们的采样频率应该在40kHz左右。为什么采样率不是越高越好呢,因为采样率越高意味着你传输的数据量越多,这样给编码和传输都带了极大的负担,成本也是个重要考虑因素。
帧率
帧率就是每秒显示的帧数,比如30fps就是1秒显示30帧图像。但是对于音频可能理解帧率不太好理解,这有点抽象。对于音频,不同的编码方式比如AAC和mp3分别就规定1024采样sample,mp3每帧为1152采样,如果一个采样用一个字节表示,那就是1024字节AAC编码音频为一帧,1152字节为MP3编码方式的音频一帧。
时间戳单位
前面我们提到采样率,感觉到采样率是个很大的单位,一般标准的音频AAC采样率达到了44kHz,视频采样率也规定在90000Hz.所以我们衡量时间的单位不能再是秒,毫秒这种真实的时间单位,我们的单位应该转换为采样率,也就是一个采样的时间为音视频的时间单位,这就是时间戳的真实值。当我们要播放和控制时,我们再将时间戳根据采样率转换为真实的时间即可。
一句话,时间戳不是真实的时间是采样次数。比如时间戳是160,我们不能认为是160秒或者160毫秒,应该是160个采样。要换算真实时间,我们必须知道采样率,比如8000,那么说明1秒被划分成8000分之一,如果你要明确160个采样占用的时间,则160*(1/8000)即可,即20毫秒。
时间戳增量
就是一帧图像和另外一帧图像之间的时间戳差值,或者一帧音频和一帧音频的时间戳差值。同理时间戳增量也是采样个数的差值不是真实时间差值,还是要根据采样率才能换算成真实时间。
所以对于视频和音频的时间戳计算要一定明确帧率是多少,采样率是多少。
比如视频而言,帧率25,那么对于90000的采样率来说,一帧占用的采样数就是90000/25也就是3600,说明每帧图像的时间戳增量应该是3600,换算成实际时间就是3600*(1/90000)=0.04秒=40毫秒,这也和1/25=0.04秒=40毫秒一致。
对于AAC音频,一帧1024个采样,采样频率是44kHz,所以一帧的播放时间应该是1024*(1/44100)=0.0232秒=23.22毫秒。
同步方法:
上面说了时间戳重要的功能就是来为了音视频的同步,那么这个时间戳到底是如何让音视频同步的呢?
播放器本地需要建立一个系统时钟,这个时钟一般是根据CPU时间计算出来的,当播放开始时时钟时间为0,时间戳决定了一帧解码和渲染的时刻。当播放开始,时钟时间会进行增加,播放器会用系统时钟和当前视频和音频的时间戳进行比较,如果音视频的时间戳小于当前系统时钟,那么就要理解解码和渲染播放。
可以看到播放能否准确进行需要编码器打的时间戳必须精确,同时播放器端的系统时钟也精确,因为播放时要基于时间戳和这个系统时钟对数据流进行控制,也就是对数据块要根据时间戳来采取不同的处理方法。实际无论编码器还是本地播放器都不能非常精确,所以我们说固定帧率25,也有可能编码器一遍打24帧的现象出现。为了解决这个累计误差问题,一般我们需要在播放端有一套反馈机制,能够消除这种误差。其实,同步是一个动态的过程,是一个有人等待、有人追赶的过程。同步只是暂时的,而不同步才是常态。人们总是在同步的水平线上振荡波动,但不会偏离这条基线太远。
PTS和DTS:
上面通过介绍基本概念就是为了引出实际使用过程中时间戳的表现形式PTS和DTS.其中DTS就是Decoding Time Stamp即解码时间戳,这个时间戳的意义告诉播放器该在什么时候解码这一帧的数据;
PTS即Presentation TimeStamp即显示时间戳,这个时间戳用来告诉播放器在什么时候显示这一帧的数据。
虽然这两个时间戳都是为了指导播放端的行为,但是他们都是由编码器生成的。正常情况下,我们一般解码出来一帧后,就需要立即进行播放,至于什么时候解码和什么时候播放,这个用一个时间戳来决定就可以,为啥现在引入了两个时间戳?当然这里说的DTS和PTS都是对视频而言的,因为视频而言才会用两个时间戳,音频还是用一个时间戳。换句话说播放器到了音频的时间戳就立即解码和播放,中间也不能有什么延时。视频之所以比较复杂是因为你视频存在三种类型的帧,I P B.
I 帧:帧内编码帧,又称为intra picture.I帧通常是每个GOP的第一个帧,采用的帧内压缩,经过适度压缩,作为随机访问的参考点,可以独立不依赖任何帧进行解码和显示。数据量最大,可以将其看为一张压缩的图像。
P 帧:前向预测编码帧,又称为prdictive frame,通过充分将低于图像序列中前面已经编码帧的时间冗余信息来压缩传输数据的编码图像,其采用了帧间预测技术来进行编码。
B帧:则是双向预测内插编码帧,又称为bi-directionalinterpolated frame,相比较P帧依赖前面的帧还依赖后面的P帧进行利用帧间的
冗余信息来压缩数据。
通过上面的比较,帧的压缩率B帧 > P 帧 > I 帧,数据量则刚好相反。
如果没有B帧,假设传输的视频帧是 I P P P,那我们就根据每个帧的时间戳进行解码和显示即可,因为后面帧的时间戳总是大于前面的时间戳,我们用一个时间戳即可。但是有了B帧则解码和显示变得复杂起来。
-
我们实际应该展示帧的顺序是: I B B P 帧解码后的顺序;
-
但实际上,这些帧到达后,我们根据I帧和B帧的特点,实际在缓存的顺序为:I P B B;
-
实际解码的顺序: 1 4 2 3;
-
最终展示的顺序是:1 2 3 4;
即先播放I帧,然后第一个B帧,第二个B帧,最后是P帧。
综上我们看到上面的帧,解码顺序和播放显示顺序是不一致的,为了控制他们到底应该是什么时候解码,什么时候播放,则我们建立了DTS和PTS的概念。注意的是解码后的帧则只有PTS概念,未解码的帧才有DTS和PTS的概念。
对于I帧则PTS=DTS,P帧的PTS > DTS,B帧PTS < DTS。当然这里的大于和小于是相对BP帧而言。时间戳小则意味着先解码或者先显示,值越大意味着后处理。
复用器代码实现
#include <stdio.h>
#include <iostream>
extern "C"
{
#include "libavformat/avformat.h"
};
int main(int argc, char* argv[])
{
AVFormatContext* ifmtCtxVideo = NULL, * ifmtCtxAudio = NULL, * ofmtCtx = NULL;
AVCodecContext* video_ctx = NULL;
AVPacket packet;
AVCodec* video_codec = NULL;
//AVBSFContext* bsf_ctx = nullptr;
const AVBitStreamFilter* pfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBitStreamFilterContext* h264bsfc = av_bitstream_filter_init("h264_mp4toannexb");
//av_bsf_alloc(pfilter, &bsf_ctx);
int inVideoIndex = -1, inAudioIndex = -1;
int outVideoIndex = -1, outAudioIndex = -1;
int audioindex = 0;
int videoindex = 0;
int64_t curPstVideo = 0, curPstAudio = 0;
int ret = 0;
unsigned int i = 0;
const char* inFilenameVideo = "Titanic.h264";
const char* inFilenameAudio = "Titanic.aac";
const char* outFilename = "test.mp4";
//打开输入视频文件
ret = avformat_open_input(&ifmtCtxVideo, inFilenameVideo, 0, 0);
if (ret < 0)
{
printf("can't open input video file\n");
goto end;
}
//查找输入流
ret = avformat_find_stream_info(ifmtCtxVideo, 0);
if (ret < 0)
{
printf("failed to retrieve input video stream information\n");
goto end;
}
//打开输入音频文件
ret = avformat_open_input(&ifmtCtxAudio, inFilenameAudio, 0, 0);
if (ret < 0)
{
printf("can't open input audio file\n");
goto end;
}
//查找输入流
ret = avformat_find_stream_info(ifmtCtxAudio, 0);
if (ret < 0)
{
printf("failed to retrieve input audio stream information\n");
goto end;
}
printf("===========Input Information==========\n");
av_dump_format(ifmtCtxVideo, 0, inFilenameVideo, 0);
av_dump_format(ifmtCtxAudio, 0, inFilenameAudio, 0);
printf("======================================\n");
//新建输出上下文
avformat_alloc_output_context2(&ofmtCtx, NULL, NULL, outFilename);
if (!ofmtCtx)
{
printf("can't create output context\n");
goto end;
}
//视频输入流
for (i = 0; i < ifmtCtxVideo->nb_streams; ++i)
{
if (ifmtCtxVideo->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
AVStream* inStream = ifmtCtxVideo->streams[i];
AVStream* outStream = avformat_new_stream(ofmtCtx, NULL);
//av_dump_format(ofmtCtx, 0, outFilename, 1);
inVideoIndex = i;
if (!outStream)
{
printf("failed to allocate output stream\n");
goto end;
}
outVideoIndex = outStream->index;
if (avcodec_parameters_copy(outStream->codecpar, inStream->codecpar) < 0)
{
printf("faild to copy context from input to output stream");
goto end;
}
outStream->codecpar->codec_tag = 0;
//av_dump_format(ofmtCtx, 0, outFilename, 1);
break;
}
}
// 解码器解码
video_ctx = avcodec_alloc_context3(video_codec);
video_codec = avcodec_find_decoder(ifmtCtxVideo->streams[0]->codecpar->codec_id);
video_ctx = ifmtCtxVideo->streams[0]->codec;
//音频输入流
for (i = 0; i < ifmtCtxAudio->nb_streams; ++i)
{
if (ifmtCtxAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
AVStream* inStream = ifmtCtxAudio->streams[i];
AVStream* outStream = avformat_new_stream(ofmtCtx, NULL);
inAudioIndex = i;
if (!outStream)
{
printf("failed to allocate output stream\n");
goto end;
}
if (avcodec_parameters_copy(outStream->codecpar, inStream->codecpar) < 0)
{
printf("faild to copy context from input to output stream");
goto end;
}
outAudioIndex = outStream->index;
break;
}
}
printf("==========Output Information==========\n");
av_dump_format(ofmtCtx, 0, outFilename, 1);
printf("======================================\n");
//打开输入文件
if (!(ofmtCtx->oformat->flags & AVFMT_NOFILE))
{
if (avio_open(&ofmtCtx->pb, outFilename, AVIO_FLAG_WRITE) < 0)
{
printf("can't open out file\n");
goto end;
}
}
//写文件头
if (avformat_write_header(ofmtCtx, NULL) < 0)
{
printf("Error occurred when opening output file\n");
goto end;
}
while (1)
{
AVFormatContext* ifmtCtx = NULL;
AVStream* inStream, * outStream;
int streamIndex = 0;
if (av_compare_ts(curPstVideo, ifmtCtxVideo->streams[inVideoIndex]->time_base, curPstAudio, ifmtCtxAudio->streams[inAudioIndex]->time_base) < 0)
{
ifmtCtx = ifmtCtxVideo;
streamIndex = outVideoIndex;
if (av_read_frame(ifmtCtx, &packet) >= 0)
{
//printf("Video start packet.pts = %d \n\n", packet.pts);
inStream = ifmtCtx->streams[packet.stream_index];
//printf("Video sample_rate = %\n", inStream->codecpar->sample_rate);
outStream = ofmtCtx->streams[streamIndex];
if (packet.stream_index == inVideoIndex)
{
av_bitstream_filter_filter(h264bsfc, ifmtCtxVideo->streams[0]->codec, NULL, &packet.data, &packet.size, packet.data, packet.size, 0);
// Fix: No PTS(Example: Raw H.264
// Simple Write PTS
if (packet.pts == AV_NOPTS_VALUE)
{
//write PTS
AVRational timeBase1 = inStream->time_base;
//Duration between 2 frames
double calcDuration = (double)1.0 / av_q2d(inStream->r_frame_rate);
//Parameters 转化为
printf("Video calcDuration = %lf\n", calcDuration);
packet.pts = (double)(videoindex * calcDuration) / (double)(av_q2d(timeBase1));
packet.dts = packet.pts;
packet.duration = (double)calcDuration / (double)(av_q2d(timeBase1));
videoindex++;
//printf("Video PTS: %ld\n", packet.pts);
//printf("Video DTS: %ld\n", packet.dts);
}
curPstVideo = packet.pts;
}
}
else
{
break;
}
}
else
{
ifmtCtx = ifmtCtxAudio;
streamIndex = outAudioIndex;
if (av_read_frame(ifmtCtx, &packet) >= 0)
{
//printf("Audio start packet.pts = %d \n\n", packet.pts);
inStream = ifmtCtx->streams[packet.stream_index];
outStream = ofmtCtx->streams[streamIndex];
if (packet.stream_index == inAudioIndex)
{
//Fix: No PTS(Example: Raw H.264
//Simple Write PTS
AVRational timeBase1 = inStream->time_base;
//Duration between 2 frames
double calcDuration = (double)1024.0 / inStream->codecpar->sample_rate;
printf("Audio calcDuration = %lf\n", calcDuration);
packet.pts = (double)(audioindex * calcDuration) / (double)(av_q2d(timeBase1));
packet.dts = packet.pts;
packet.duration = (double)calcDuration / (double)(av_q2d(timeBase1));
audioindex ++;
//printf("Audio PTS: %ld\n", packet.pts);
//printf("Audio DTS: %ld\n", packet.dts);
curPstAudio = packet.pts;
}
}
else
{
break;
}
}
//FIX:Bitstream Filter
//Convert PTS/DTS
packet.pts = av_rescale_q_rnd(packet.pts, inStream->time_base, outStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
packet.dts = av_rescale_q_rnd(packet.dts, inStream->time_base, outStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
packet.duration = av_rescale_q(packet.duration, inStream->time_base, outStream->time_base);
packet.pos = -1;
packet.stream_index = streamIndex;
//write
printf("Audio sample_rate = %d \n\n", inStream->codecpar->sample_rate);
if (av_interleaved_write_frame(ofmtCtx, &packet) < 0)
{
printf("error muxing packet");
break;
}
av_packet_unref(&packet);
}
av_write_trailer(ofmtCtx);//写文件尾
end:
avformat_close_input(&ifmtCtxVideo);
avformat_close_input(&ifmtCtxAudio);
if (ofmtCtx && !(ofmtCtx->oformat->flags & AVFMT_NOFILE))
avio_close(ofmtCtx->pb);
avformat_free_context(ofmtCtx);
return 0;
}