android AV同步详解

本文主要介绍 android 多媒体中,音视频同步问题。

以下是详细说明:

先提及一个背景基础知识: 
Stagefright中,audio的数据输出是通过AudioTrack中的callback来不断驱动AudioPlayer::fillBuffer获取数据
video的数据输出,则是由OMX端在解码完毕后,给awesomeplayer发送消息event,在awesomeplayer的onVideoEvent中进行视频的render。
在render之前进行AV同步工作
具体细节不再赘述。读者可自行参考其他文章。
所以本文实际上重点研究的就是AV同步的处理函数:onVideoEvent

一,既然牵扯到AV同步,肯定涉及到一个时间基准的问题。
AV同步目前有两种同步方法: 
1,视频,音频同步到一个第三方系统时钟  
2,以音频时间戳为基准,视频数据同步到音频时间戳上

两种方法各有优缺点, 
第一种方法中,音频视频不能互相感知,如果有一方出现了问题,一样会造成AV时间不同步。尤其是如果AV中一方有一个长时间的累积误差,这种方法就无能为力了。因为他们不能互相修正。
第二种方法中,必须有音频做基准,对于一些没有音频,只有视频的媒体文件,这种方法就无能为力了。

所以android目前对这两种都采用了:
对于音频,视频都健全的文件,把视频同步到音频上。对于没有音频,只有视频的文件,把视频同步到第三方时钟上,即系统时钟

具体来说,就是awesomeplayer选择哪个TimerSoruce为时间基准的问题,是系统时钟?还是audio的时间戳?
那么是怎么选择的呢?

在onVideoEvent中,以下代码做了选择
    TimeSource *ts =  ((mFlags & AUDIO_AT_EOS) || !(mFlags & AUDIOPLAYER_STARTED)) ? &mSystemTimeSource : mTimeSource;
当audio 播放完毕的时候,或者audio没有开始的时候,就选择系统时钟作为TimeSource
否则,就是有audio在播放,就要选择mTimeSource。
这个mTimeSource是啥呢?在AwesomePlayer::createAudioPlayer_l里面,可以看到 mTimeSource = mAudioPlayer;
这样,就把音频选择为TimerSoruce

我们结合这段代码的上下文来研究:
    CHECK(mVideoBuffer->meta_data()->findInt64(kKeyTime, &timeUs));
   TimeSource *ts =
        ((mFlags & AUDIO_AT_EOS) || !(mFlags & AUDIOPLAYER_STARTED))
            ? &mSystemTimeSource : mTimeSource;
    if (mFlags & FIRST_FRAME) {
        modifyFlags(FIRST_FRAME, CLEAR);
        mSinceLastDropped = 0;
        mTimeSourceDeltaUs = ts->getRealTimeUs() - timeUs;
    }
   int64_t realTimeUs, mediaTimeUs;
    if (!(mFlags & AUDIO_AT_EOS) && mAudioPlayer != NULL
        && mAudioPlayer->getMediaTimeMapping(&realTimeUs, &mediaTimeUs)) {
        mTimeSourceDeltaUs = realTimeUs - mediaTimeUs;
    }
这段代码比较绕,我们分成两种情况来分析:
1,如果没有音频数据,如果该视频帧是第一帧,就通过ts->getRealTimeUs()来取系统时钟的值,减去该第一帧在视频文件里的时间戳,这样就能得出视频开始播放的那一霎那,对应的系统时间mTimeSourceDeltaUs,后续的视频帧同步就是用这个播放起始时间作为参考。
这实际上就是我们前面讲到的没有音频的时候,视频数据按照系统时钟来同步
2,如果有音频数据,mTimeSourceDeltaUs会被修改为 realTimeUs - mediaTimeUs;
跟踪getMediaTimeMapping这个函数,你会发现,实际上就是对象AudioPlayer里面的mPositionTimeRealUs-mPositionTimeMediaUs
mPositionTimeRealUs 实际上是在AudioPlayer::fillBuffer里面被赋值   ((mNumFramesPlayed + size_done / mFrameSize) * 1000000)/ mSampleRate;
这实际上就是目前已经播放掉的数据的时间,也就是理想情况下此帧需要被播放的时间。
mPositionTimeMediaUs实际上就是 CHECK(mInputBuffer->meta_data()->findInt64(kKeyTime, &mPositionTimeMediaUs)),即此音频帧在媒体文件中的时间戳。
这样mTimeSourceDeltaUs=realTimeUs - mediaTimeUs;所表示的含义就是该帧音频实际需要播放的时间点,和他所标注的时间戳的一个误差。

继续在onVideoEvent中分析
        int64_t nowUs = ts->getRealTimeUs() - mTimeSourceDeltaUs;
        int64_t latenessUs = nowUs - timeUs;
我们依旧是分成两种情况来分析这两行代码
1,如果没有音频数据, ts->getRealTimeUs() 获取到当前的系统时钟,mTimeSourceDeltaUs是前面讲到的文件开始播放的系统时钟, 二者相减,表示的是视频播了多久。是一个实际值。this is a real value,
timeUs表示的是这一帧视频在文件中的时间戳。表示的就是当前时刻,视频应该播放到哪一微秒。 this is a supposed value.
二者相减,就表示实际值和应该值之间的误差。
2,如果有音频数据, ts->getRealTimeUs() 获取到的是音频为基准的时间, 实际上是从AudioPlayer::getRealTimeUsLocked()里面得到, 得到的是从整个文件开始播放,到现在的一个累积时间。(当然,getRealTimeUsLocked里面经过了一些修正,主要是修正了buffer长度造成的延时,以及硬件延时。具体参考http://blog.csdn.net/njuitjf/article/details/7637003)
前面交代过, mTimeSourceDeltaUs =realTimeUs - mediaTimeUs=mPositionTimeRealUs-mPositionTimeMediaUs,
这样  nowUs= ts->getRealTimeUs() - mTimeSourceDeltaUs = ts->getRealTimeUs()- ( mPositionTimeRealUs-mPositionTimeMediaUs)=修正过的播放累积时间-(理想情况播放累积时间-此帧音频数据的理论时间戳)=  (audio latency)+此帧音频数据的理论时间戳
这样得到的值,就是对这一帧音频的时间戳,做了一个修正。把硬件时延以及buffer播放带来的时延误差给修正上了。得到的就是预估出来的这个buffer真正应该播放的时间戳
所以,latenessUs = nowUs - timeUs;就是这一帧视频的时间戳,与目前正在播放的音频时间戳的差值。  由于以音频时间戳为基准,所以这里算出来的就是视频帧与时间基准的误差
有了这个误差,就可以对这个视频帧做相应处理
void AwesomePlayer::onVideoEvent()
{
         if (latenessUs > 500000ll ) 
{
            if (mWVMExtractor == NULL) {
                mVideoBuffer->release();
                mSeeking = SEEK_VIDEO_ONLY;
                mSeekTimeUs = mediaTimeUs;
                postVideoEvent_l();
                return;
            } 
        }
  if (latenessUs > 40000)
  {
    mVideoBuffer->release();
    postVideoEvent_l();
    return;
  }
  if (latenessUs < -10000)
  {
    postVideoEvent_l(10000);
    return;
  }
  mVideoRenderer->render(mVideoBuffer);
  ...
}
这段代码的意思就是:
   1:如果视频数据过慢,慢于音频时间500ms,则丢弃当前帧,并且需要丢弃一段视频帧,直接对video部分(SEEK_VIDEO_ONLY)seek到音频当前帧的时间戳;
   2:如果视频数据过慢,慢于音频时间40ms,则丢弃当前帧,然后再去取数据;
   3:如果视频数据过快,超过音频时间10ms,则显示当前帧后,将读取数据的事件延时(慢的时间-10ms)后放入队列;
   4,如果时间合适,误差不大,直接render

你可能感兴趣的:(android,编解码)