音视频同步主要用于在音视频流的播放过程当中,让同一时刻录制的声音和图像在播放的时候尽量的在同一个时间输出。框架
解决音视频同步问题的最佳方案就是时间戳:首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);生成数据流时依据参考时钟上的时间给每一个数据块都打上时间戳(通常包括开始时间和结束时间);在播放时,读取数据块上的时间戳,同时参考当前参考时钟上的时间来安排播放(若是数据块的开始时间大于当前参考时钟上的时间,则不急于播放该数据块,直到参考时钟达到数据块的开始时间;若是数据块的开始时间小于当前参考时钟上的时间,则“尽快”播放这块数据或者索性将这块数据“丢弃”,以使播放进度追上参考时钟)。ide
Android音视频同步,主要是以audio的时间轴做为参考时钟,在没有audio的状况下,以系统的时间轴做为参考时钟。这是由于audio丢帧很容易就能听出来,而video丢帧却不容易被察觉。函数
避免音视频不一样步现象有两个关键因素 —— 一是在生成数据流时要打上正确的时间戳;二是在播放时基于时间戳对数据流的控制策略,也就是对数据块早到或晚到采起不一样的处理方法。oop
在视频录制过程当中,音视频流都必需要打上正确的时间戳。假如,视频流内容是从0s开始的,假设10s时有人开始说话,要求配上音频流,那么音频流的起始时间应该是10s,若是时间戳从0s或其它时间开始打,则这个混合的音视频流在时间同步上自己就存在问题。post
带有声音和图像的视频,在播放的时候都须要处理音视频同步的问题。Android平台,是在render图像以前,进行音视频同步的。ui
单独的音频或者视频流,不须要进行音视频同步处理,音视频同步只针对既有视频又有音频的流。this
因为Android是以audio的时间轴做为参考时钟,音视频播放同步处理主要有以下几个关键因素:google
(1)计算audio时间戳;spa
(2)计算video时间戳相对于audio时间戳的delay time;rest
(3)依据delay time判断video是早到,晚到,采起不一样处理策略。
在Android 2.3版本以前,音视频播放框架主要采用OpenCORE,OpenCORE的音视频同步作法是设置一个主
时钟,音频流和视频流分别以主时钟做为输出的依据。
从Android 2.0版本开始,Google引入了stagefright框架,到2.3版本,彻底替代了OpenCORE。Stagefright框架的音视频同步作法是以音频流的时间戳做为参考时钟,视频流在render前进行同步处理。
从Android 4.0版本开始,Google引入了nuplayer框架,nuplayer主要负责rtsp、hls等流媒体的播放;而stagefright负责本地媒体以及 http媒体的播放。nuplayer框架的音视频同步作法任然是以音频流的时间戳做为参考时钟。
在Android 4.1版本上,添加了一个系统属性media.stagefright.use-nuplayer,代表google用nuplayer替代stagefight的意图。
直到Android 6.0版本,nuplayer才彻底替代了stagefight。StagefrightPlayer从系统中去掉。
关于Nuplayer的音视频同步,基于Android M版本进行分析。
NuplayerRender在onQueueBuffer中收到解码后的buffer,判断是音频流仍是视频流,将bufferPush到对应的buffer queue,而后分别调用postDrainAudioQueue_l和postDrainVideoQueue进行播放处理。
同步处理分散在postDrainVideoQueue、onDrainVideoQueue以及onRenderBuffer中,音频流的媒体时间戳在onDrainAudioQueue中得到。
A:在onDrainAudioQueue()中获取并更新音频时间戳
bool NuPlayer::Renderer::onDrainAudioQueue() { uint32_t numFramesPlayed; while (!mAudioQueue.empty()) { QueueEntry *entry = &*mAudioQueue.begin(); if (entry->mOffset == 0 && entry->mBuffer->size() > 0) { int64_t mediaTimeUs; //获取并更新音频流的媒体时间戳 CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs)); onNewAudioMediaTime(mediaTimeUs); } size_t copy = entry->mBuffer->size() - entry->mOffset; ssize_t written = mAudioSink->write(entry->mBuffer->data() + entry->mOffset, copy, false /* blocking */); size_t copiedFrames = written / mAudioSink->frameSize(); mNumFramesWritten += copiedFrames; } int64_t maxTimeMedia; { Mutex::Autolock autoLock(mLock); //计算并更新maxTimeMedia maxTimeMedia = mAnchorTimeMediaUs + (int64_t)(max((long long)mNumFramesWritten - mAnchorNumFramesWritten, 0LL) * 1000LL * mAudioSink->msecsPerFrame()); } mMediaClock->updateMaxTimeMedia(maxTimeMedia); bool reschedule = !mAudioQueue.empty() && (!mPaused || prevFramesWritten != mNumFramesWritten); return reschedule; }
B:onNewAudioMediaTime()将时间戳更新到MediaClock
在onNewAudioMediaTime()中,将音频流的媒体时间戳、当前播放时间戳及系统时间更新到MediaClock用来计算视频流的显示时间戳。
void NuPlayer::Renderer::onNewAudioMediaTime(int64_t mediaTimeUs) { Mutex::Autolock autoLock(mLock); if (mediaTimeUs == mAnchorTimeMediaUs) { return; } setAudioFirstAnchorTimeIfNeeded_l(mediaTimeUs); int64_t nowUs = ALooper::GetNowUs(); //将当前播放音频流时间戳、系统时间、音频流当前媒体时间戳更新到mMediaClock int64_t nowMediaUs = mediaTimeUs - getPendingAudioPlayoutDurationUs(nowUs); mMediaClock->updateAnchor(nowMediaUs, nowUs, mediaTimeUs); //用于计算maxTimeMedia mAnchorNumFramesWritten = mNumFramesWritten; mAnchorTimeMediaUs = mediaTimeUs; }
MediaClock::updateAnchor()
void MediaClock::updateAnchor( int64_t anchorTimeMediaUs, int64_t anchorTimeRealUs, int64_t maxTimeMediaUs) { if (anchorTimeMediaUs < 0 || anchorTimeRealUs < 0) { return; } Mutex::Autolock autoLock(mLock); int64_t nowUs = ALooper::GetNowUs(); //从新计算当前播放的音频流的时间戳 int64_t nowMediaUs = anchorTimeMediaUs + (nowUs - anchorTimeRealUs) * (double)mPlaybackRate; if (nowMediaUs < 0) { return; } //系统时间更新到mAnchorTimeRealUs mAnchorTimeRealUs = nowUs; //音频播放时间戳更新到mAnchorTimeMediaUs mAnchorTimeMediaUs = nowMediaUs; //音频媒体时间戳更新到mMaxTimeMediaUs mMaxTimeMediaUs = maxTimeMediaUs; }
postDrainVideoQueue()中进行了大部分同步处理
1)调用getRealTimeUs(),根据视频流的媒体时间戳获取显示时间戳;
2)经过VideoFrameScheduler来判断何时执行onDrainVideoQueue()
void NuPlayer::Renderer::postDrainVideoQueue() { QueueEntry &entry = *mVideoQueue.begin(); sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this); int64_t delayUs; int64_t nowUs = ALooper::GetNowUs(); int64_t realTimeUs; //获取当前视频流的媒体时间戳 int64_t mediaTimeUs; CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs)); { Mutex::Autolock autoLock(mLock); if (mAnchorTimeMediaUs < 0) { //音频流处理时,会更新该时间戳。若是没有音频流,视频流以系统时间为参考顺序播放 mMediaClock->updateAnchor(mediaTimeUs, nowUs, mediaTimeUs); mAnchorTimeMediaUs = mediaTimeUs; realTimeUs = nowUs; } else { //根据视频流的媒体时间戳和系统时间,获取显示时间戳 realTimeUs = getRealTimeUs(mediaTimeUs, nowUs); } } if (!mHasAudio) { //没有音频流的状况下,以当前视频流的媒体时间戳+100ms做为maxTimeMedia // smooth out videos >= 10fps mMediaClock->updateMaxTimeMedia(mediaTimeUs + 100000); } delayUs = realTimeUs - nowUs; //视频早了500ms,延迟进行下次处理 if (delayUs > 500000) { if (mHasAudio && (mLastAudioBufferDrained - entry.mBufferOrdinal) <= 0) { postDelayUs = 10000; } msg->setWhat(kWhatPostDrainVideoQueue); msg->post(postDelayUs); mVideoScheduler->restart(); mDrainVideoQueuePending = true; return; } //依据Vsync调整显示时间戳,预留2个Vsync间隔的时间进行render处理 realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000; int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000); delayUs = realTimeUs - nowUs; msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0); mDrainVideoQueuePending = true; }
A: NuPlayer::Renderer::getRealTimeUs()
根据视频流的媒体时间戳、系统时间,从mMediaClock获取视频流的显示时间戳
int64_t NuPlayer::Renderer::getRealTimeUs(int64_t mediaTimeUs, int64_t nowUs) { int64_t realUs; if (mMediaClock->getRealTimeFor(mediaTimeUs, &realUs) != OK) { // If failed to get current position, e.g. due to audio clock is // not ready, then just play out video immediately without delay. return nowUs; } return realUs; }
B:MediaClock::getRealTimeFor()
计算视频流的显示时间戳 = (视频流的媒体时间戳 - 音频流的显示时间戳)/ 除以播放速率 + 当前系统时间
status_t MediaClock::getRealTimeFor( int64_t targetMediaUs, int64_t *outRealUs) const { ...... int64_t nowUs = ALooper::GetNowUs(); int64_t nowMediaUs; //获取当前系统时间对应音频流的显示时间戳即当前音频流播放位置 status_t status = getMediaTime_l(nowUs, &nowMediaUs, true /* allowPastMaxTime */); if (status != OK) { return status; } //视频流的媒体时间戳与音频流的显示时间戳的差值除以播放速率,再加上当前系统时间,做为视频流的显示时间戳 *outRealUs = (targetMediaUs - nowMediaUs) / (double)mPlaybackRate + nowUs; return OK; }
A:onDrainVideoQueue()
在onDrainVideoQueue()中,更新了视频流的显示时间戳,并判断视频延迟是否超过40ms。而后将这些信息通知NuPlayerDecoder在onRenderBuffer()中调用渲染函数渲染视频流。
void NuPlayer::Renderer::onDrainVideoQueue() { QueueEntry *entry = &*mVideoQueue.begin(); int64_t mediaTimeUs; CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs)); nowUs = ALooper::GetNowUs(); //从新计算视频流的显示时间戳 realTimeUs = getRealTimeUs(mediaTimeUs, nowUs); if (!mPaused) { if (nowUs == -1) { nowUs = ALooper::GetNowUs(); } setVideoLateByUs(nowUs - realTimeUs); 当前视频流延迟小于40ms就显示 tooLate = (mVideoLateByUs > 40000); } entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000ll); entry->mNotifyConsumed->setInt32("render", !tooLate); //通知NuPlayerDecoder entry->mNotifyConsumed->post(); mVideoQueue.erase(mVideoQueue.begin()); entry = NULL; }
B:Decoder::onRenderBuffer()
void NuPlayer::Decoder::onRenderBuffer(const sp<AMessage> &msg) { //由render去显示 并释放video buffer if (msg->findInt32("render", &render) && render) { int64_t timestampNs; CHECK(msg->findInt64("timestampNs", ×tampNs)); err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs); } else { mNumOutputFramesDropped += !mIsAudio; //该帧video太迟,直接丢弃 err = mCodec->releaseOutputBuffer(bufferIx); } }