1.H264格式简介(视频解码同步相关)
----------------------
前言
-----------------------
H264是新一代的编码标准,以高压缩高质量和支持多种网络的流媒体传输著称,在编码方面,我理解的他的理论依据是:参照一段时间内图像的统计结果表明,在相邻几幅图像画面中,一般有差别的像素只有10%以内的点,亮度差值变化不超过2%,而色度差值的变化只有1%以内。所以对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小!B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。这段图像我们称为一个序列(序列就是有相同特点的一段数据),当某个图像与之前的图像变化很大,无法参考前面的帧来生成,那我们就结束上一个序列,开始下一段序列,也就是对这个图像生成一个完整帧A1,随后的图像就参考A1生成,只写入与A1的差别内容。
在H264协议里定义了三种帧,完整编码的帧叫I帧,参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。
H264采用的核心算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。
----------------------
序列的说明
----------------------
在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。
一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。 H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。
一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,然后一直P帧、B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧。
-----------------------
帧间预测
-----------------------
I帧:帧内编码帧 ,I帧表示关键帧,你可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)。
P帧:前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。(也就是差别帧,P帧没有完整画面数据,只有与前一帧的画面差别的数据);
P帧的预测与重构:
P帧是以I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动矢量以及预测差值。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与预测差值相加以得到P帧“某点”样值,从而可得到完整的P帧。
B帧:双向预测内插编码帧。B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况,但我这样说简单些),换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU占用会比较高。
B帧的预测与重构:
B帧以前面的I或P帧和后面的P帧为参考帧,“找出”B帧“某点”的预测值和两个运动矢量,接收端根据运动矢量在两个参考帧中的预测值与预测差值求和,来得到B帧“某点”值,从而可得到完整的B帧。
----------------------
帧内预测
----------------------
场和帧:视频的一场或一帧可用来产生一个编码图像。在电视中,为减少大面积闪烁现象,把一帧分成两个隔行的场。
宏块:一个编码图像通常划分成若干宏块组成,一个宏块由一个16×16亮度像素和附加的一个8×8 Cb和一个8×8 Cr彩色像素块组成。
片:每个图象中,若干宏块被排列成片的形式。片分为I片、B片、P片和其他一些片。
I片只包含I宏块,P片可包含P和I宏块,而B片可包含B和I宏块。
I宏块利用从当前片中已解码的像素作为参考进行帧内预测。
P宏块利用前面已编码图象作为参考图象进行帧内预测。
B宏块则利用双向的参考图象(前一帧和后一帧)进行帧内预测。
片的目的是为了限制误码的扩散和传输,使编码片相互间是独立的。
某片的预测不能以其它片中的宏块为参考图像,这样某一片中的预测误差才不会传播到其它片中去。
2.音频相关基础知识
----------------------
采样率与比特率的辨析
----------------------
采样率表示了每秒钟的采样次数。
比特率表示了每秒钟传输的音频数据量,该值也和采样率相关。
采样率类似于动态影像的帧数,比如电影的采样率是24HZ,PAL制式的采样率是25HZ,NTSC制式的采样率是30HZ。当我们把采样到的一个个静止画面再以采样率同样的速度回放时,看到的就是连续的画面。同样的道理,把以44100HZ采样率记录的CD以同样的速率播放时,就能听到连续的声音。显然,这个采样率越高,听到的声音和看到的图像就越连贯。可是人的听觉和视觉器官能分辨的采样率是有限的,基本上高于44100HZ采样的声音,绝大部分人已经觉察不到其中的分别了。
而声音的位数就相当于画面的颜色数(8位就是0-255),表示每个取样的数据量,当然数据量越大,回放的声音越准确,不至于把开水壶的叫声和火车的鸣笛混淆。同样的道理,对于画面来说就是更清晰和准确,不至于把血和西红柿酱混淆。不过受人的器官的机能限制,16位的声音和24位的画面基本已经是普通人类的极限了,更高位数就只能靠仪器才能分辨出来了。比如电话就是3000HZ取样的7位声音,而CD是44100HZ取样的16位声音,所以CD就比电话更清楚。
有了以上这两个概念,比特率就很容易理解了。以电话为例,每秒3000次取样,每个取样是7比特,那么电话的比特率是21000。而CD是每秒44100次取样,两个声道,每个取样是16位PCM编码,所以CD的比特率是44100*2*16=1411200,也就是说CD每秒的数据量大约是176KB。
注意事项:
① Android ffmpeg音频编码器对AAC是默认关闭的,需要通过设置m_pAudioCodecCtx->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL,才能顺利打开编码器。
② 在将mp3转码为AAC音频时,需要利用FIFO Buffer(AVAudioFifoBuffer)的结构,对转换后的每帧采样进行缓冲。
③ 音视频编码最后时刻可能读取数据时返回为-1,但仍有若干包存放在编码器缓存中,需要flush缓存才不会导致最后的几帧丢失,导致视频时间错误。
④ AV_SAMPLE_FMT_S16、AV_SAMPLE_FMT_S16P格式(两者如果都是双通道)区别在于前者解码出的Frame的数组有效数据只有一维,则是交错存储的;而后者的有效数据有两维(两个Plane),一维存储一个通道。在做音频混合时,由于需要存入不同通道,所以需要注意以上问题。
3.ffmpeg音视频同步
----------------------
前言:
----------------------
ffmpeg通过AVStream结构的time_base(有理数结构体——AVRational,由分子和分母两部分组成)可以获取一个参考时间单位,所有音视频流的timestamp都是基于这个时间单位顺序递增,比如time_base.num=1,time_base.den=90000,表示把1秒分成90000等份,音视频包的PTS(显示时间戳)和DTS(解码时间戳)就表示有多少个1/90000 (time_base)单位时间,更简单一点假设time_base.num=1,time_base.den=1000,就表示1秒分成1000等份,相当于1毫秒,那时间戳就表示是以毫秒为单位的,在做音视频处理时候,如果解码的速度比按照时间戳显示的速度快,那就简单直接处理,不用丟帧(Drop Frame),当解码速度很慢时(比如手机设备),就需要丢帧处理,是每两帧丟一帧数据,还是每三帧丟一帧数据,就需要根据延时显示程度来计算丢帧的比率。
----------------------
获取视频帧的PTS:
----------------------
虽然视频流能提供视频流帧率值,但如果简单地通过 帧数/帧率 来同步视频,可能会使音视频不同步。正确的方式是利用视频流中每个包的DTS和PTS,即包的解码时间戳(DTS--Decompressed Time Stamp)和显示时间戳(PTS--Presentation Time Stamp)。为了搞清楚这两个概念,需要知道视频的编码存储方式。正如之前提到的H264编码原理,将视频帧分为I帧、P帧、B帧,这也就是为什么调用avcodec_decode_video可能没有得到完整一帧的原因。
假设有一段视频序列,其帧排序为:I B B P。但是在播放B帧之前需要知道P帧的信息,所以帧的实际存储顺序可能是I P B B。这就是为什么我们会有一个DTS和PTS。DTS告诉我们什么时候解码的,PTS告诉我们什么时候显示。所以,在这个例子中,流可能是这样的:
解码器输入
Stream: I P B B P B B // 存储顺序 PTS: 0312645 DTS: 0123456
解码器输出
Stream: I B B P B B P PTS: 0123456
通常只有当显示B帧时,PTS和DTS才会不一致。
值得注意的是,通过 av_read_frame() 得到的 AVPacket 中的 DTS 通常才有正确的值,而通过 avcodec_decode_video2() 得到的 AVFrame 的 PTS 并没有包含有用信息。第一种方法是利用在解码包时 ffmpeg 会按照 PTS 重新对包进行排序, 因此被 avcodec_decode_video2() 处理过的包的 DTS 和返回的帧的 PTS 是相同的, 这样就可以得到帧的 PTS 了。然而我们并不是总能获得该值(经测试影响不大),所以第二种方法是我们需要保存第一帧的第一包的 PTS(之后的PTS可以根据帧率计算出来), 将其作为这一帧的 PTS。因为当一帧开始发送第一包时,avcodec_decode_video() 会调用相关函数为帧申请存储空间,我们可以重写这个函数,在函数中加入获取包 DTS 的方法,并用全局变量进行保存。通过以上两种方法就计算出了帧的 PTS 。
----------------------
计算视频帧实际的PTS:
----------------------
考虑重复帧的情况(甚至得不到 PTS 的情况),使用 VideoState 的 video_clock 字段时刻记录视频已经播放的时间,通过换算到流的时间基来计算实际正确的 PTS(保存在 VideoPicture 的 PTS 字段中),并重新排序视频帧(queue_picture),这样便可以设置合适的刷新速率(比如通过简单计算前一帧和现在这一帧的时间戳来预测出下一个时间戳的时间)。接下来我们将同步视频到音频。
----------------------
计算音频实际的PTS:
----------------------
虽然音视频流都包含了播放速率的信息,音频使用采样率来表示,而视频则采用帧率来表示,所以我们不能简单使用两个数据来对音视频进行同步,而是需要使用 DTS 和 PTS。
现在看一下怎样得到音频时钟。我们可以在音频解码函数 audio_decode_frame() 中更新音频时间。然而需要注意的是我们并不是每次调用这个函数的时候都在处理新的包,所以更新时钟的时刻有两个:
①当我们得到新的包的时候,我们简单的设置音频时钟为这个包的时间戳 PTS。
②如果一个包里有许多帧,我们通过样本数和采样率来计算:
n = 16/8 * channels; (当采样精度为16位时) audio_clock += data_size / (n * sample_rate)(当缓冲区满时的播放时间);
然而我们不能把 audio_clock 直接作为音频的PTS,因为在 audio_decode_frame()计算的 audio_clock 的是假定缓冲满的情况,而实际上可能缓冲是不满的,所以实际播放时根据缓冲大小和播放速率计算播放时间:需要减去空闲部分的时间:
PTS -= (double)hw_buf_size / bytes_per_sec (hw_buf_size表示空闲缓冲大小,bytes_per_sec表示每秒播放的字节数)
----------------------
同步视频到音频:
----------------------
通过以上方式可以获取到实际的音频时间。有了这个值, 在音频和视频不同步的时候,我们会调整下次刷新的值:如果视频时间戳(类似序号)相比音频时间戳大于一定阈值(即视频播放太快),我们加倍延迟。如果视频时间戳相较于音频时间戳小于一定阈值(即视频播放太慢),我们将尽可能快的刷新。
同步流程(比喻):
有一把尺子 一只蚂蚁(视频)跟着一个标杆(音频)走, 标杆是匀速的 蚂蚁或快或慢,慢了你就抽它 让它跑起来,快了就拽它。这样音视频就能同步了。 这里最大的问题就是音频是匀速的,视频是非线性的。
----------------------
其他同步方法:
----------------------
分别获得音视频的PTS后,我们有三个选择:视频同步音频(计算音视频PTS之差,来判定视频是否有延迟)、音频同步视频(根据音视频PTS差值调整音频取的样值,即改变音频缓冲区的大小)和音频视频同步外部时钟(同前一个),因为调整音频范围过大,会造成令用户不适的尖锐声,所以通常我们选择第一种。
----------------------
time_base(时间基)的基本概念:----------------------
AVRational的结构如下:
typedef struct AVRational{ int num; ///< numerator int den; ///< denominator } AVRational;
AVRational这个结构标识一个分数,num为分数,den为分母。
1.时间基的转换的概念
实际上time_base的意思就是时间的刻度:
比如,编解码器上下文的时间基为(1,25),那么时间刻度就是1/25
文件容器中的流的时间基为(1,90000),那么时间刻度就是1/90000。
那么,在刻度为1/25的体系下的time=5,转换成在刻度为1/90000体系下的时间time为(5*(1/25)) / (1/90000)= 3600*5=18000
【ffmpeg提供了av_rescale_q_rnd函数进行转换。
av_rescale_q_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd)
此函数主要用于对于不同时间戳的转换。具体来说是将原来以 "时间基b" 表示的 数值a 转换成以 "时间基c" 来表示的新值。AVRounding表示取整的策略。】