Android 音视频同步(A/V Sync)
1. 音视频同步原理
1)时间戳
音视频同步主要用于在音视频流的播放过程中,让同一时刻录制的声音和图像在播放的时候尽可能的在同一个时间输出。
解决音视频同步问题的最佳方案就是时间戳:首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);生成数据流时依据参考时钟上的时间给每个数据块都打上时间戳(一般包括开始时间和结束时间);在播放时,读取数据块上的时间戳,同时参考当前参考时钟上的时间来安排播放(如果数据块的开始时间大于当前参考时钟上的时间,则不急于播放该数据块,直到参考时钟达到数据块的开始时间;如果数据块的开始时间小于当前参考时钟上的时间,则“尽快”播放这块数据或者索性将这块数据“丢弃”,以使播放进度追上参考时钟)。
Android音视频同步,主要是以audio的时间轴作为参考时钟,在没有audio的情况下,以系统的时间轴作为参考时钟。这是因为audio丢帧很容易就能听出来,而video丢帧却不容易被察觉。
避免音视频不同步现象有两个关键因素 —— 一是在生成数据流时要打上正确的时间戳;二是在播放时基于时间戳对数据流的控制策略,也就是对数据块早到或晚到采取不同的处理方法。
2) 录制同步
在视频录制过程中,音视频流都必须要打上正确的时间戳。假如,视频流内容是从0s开始的,假设10s时有人开始说话,要求配上音频流,那么音频流的起始时间应该是10s,如果时间戳从0s或其它时间开始打,则这个混合的音视频流在时间同步上本身就存在问题。
3) 播放同步
带有声音和图像的视频,在播放的时候都需要处理音视频同步的问题。Android平台,是在render图像之前,进行音视频同步的。
单独的音频或者视频流,不需要进行音视频同步处理,音视频同步只针对既有视频又有音频的流。
由于Android是以audio的时间轴作为参考时钟,音视频播放同步处理主要有如下几个关键因素:
(1)计算audio时间戳;
(2)计算video时间戳相对于audio时间戳的delay time;
(3)依据delay time判断video是早到,晚到,采取不同处理策略。
2. Android音视频播放框架
在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从系统中去掉。
3. Nuplayer音视频同步
1) Nuplayer音视同步简介
关于Nuplayer的音视频同步,基于Android M版本进行分析。
NuplayerRender在onQueueBuffer中收到解码后的buffer,判断是音频流还是视频流,将bufferPush到对应的buffer queue,然后分别调用postDrainAudioQueue_l和postDrainVideoQueue进行播放处理。
同步处理分散在postDrainVideoQueue、onDrainVideoQueue以及onRenderBuffer中,音频流的媒体时间戳在onDrainAudioQueue中获得。
2) 计算音频流时间戳
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;
}
3)视频流同步策略
1)postDrainVideoQueue()
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;
}
2)onDrainVideoQueue()
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);
}
}
Android 音视频同步(A/V Sync)的更多相关文章
- Android 音视频同步机制
一.概述 音视频同步(avsync),是影响多媒体应用体验质量的一个重要因素.而我们在看到音视频同步的时候,最先想到的就是对齐两者的pts,但是实际使用中的各类播放器,其音视频同步机制都比这些复杂的多 ...
- android音视频点/直播模块开发
音视频 版权声明:本文为博主原创文章,未经博主允许不得转载. 前言 随着音视频领域的火热,在很多领域(教育,游戏,娱乐,体育,跑步,餐饮,音乐等)尝试做音视频直播/点播功能,那么作为开发一个小白, ...
- Android音视频点/直播模块开发实践总结-zz
随着音视频领域的火热,在很多领域(教育,游戏,娱乐,体育,跑步,餐饮,音乐等)尝试做音视频直播/点播功能.那么作为开发一个小白,如何快速学习音视频基础知识,了解音视频编解码的传输协议,编解码方式,以及 ...
- ffplay(2.0.1)中的音视频同步
最近在看ffmpeg相关的一些东西,以及一些播放器相关资料和代码. 然后对于ffmpeg-2.0.1版本下的ffplay进行了大概的代码阅读,其中这里把里面的音视频同步,按个人的理解,暂时在这里作个笔 ...
- libstagefright 音视频同步方案
1:音视频数据都有一个list,用于存放解码后的数据: List mFilledBuffers; 2:解码后的音视频数据不断的往list中存放,不做音视频同步方面的时间上控制 mFille ...
- 直播APP源码是如何实现音视频同步的
1. 音视频同步原理 1)时间戳 直播APP源码音视频同步主要用于在音视频流的播放过程中,让同一时刻录制的声音和图像在播放的时候尽可能的在同一个时间输出. 解决直播APP源码音视频同步问题的最佳方案 ...
- 手机Android音视频采集与直播推送,实现单兵、移动监控类应用
从安卓智能手机.平板,到可穿戴的Android Ware.眼镜.手表.再到Android汽车.智能家居.电视,甚至最近看新闻,日本出的几款机器人都是Android系统的,再把目光放回监控行业,传统监控 ...
- Android 音视频开发学习思路
Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...
- Android 音视频开发(一) : 通过三种方式绘制图片
版权声明:转载请说明出处:http://www.cnblogs.com/renhui/p/7456956.html 在 Android 音视频开发学习思路 里面,我们写到了,想要逐步入门音视频开发,就 ...
随机推荐
- Android使用http协议与服务器通信
网上介绍Android上http通信的文章很多,不过大部分只给出了实现代码的片段,一些注意事项和如何设计一个合理的类用来处理所有的http请求以及返回结果,一般都不会提及.因此,自己对此做了些总结,给 ...
- 原来找字也可以这样用ElseIf FindStr 手机按键精灵 跟大漠的区别
原来找字也可以这样用ElseIf FindStr(646, 1109, 776, 1261, "公告小叉", "FFFFFF-333333", 0.9, in ...
- phpstorm + x-debug 进行php调试
参照http://www.cnblogs.com/tippoint/p/3429092.html 进行安装xdebug: 首先自己写一个打印php的页面,将phpinfo 拷贝到下面的框内进行分析. ...
- 如何让form表单在enter键入时不提交
今天在做我的一个小玩意 在线聊天工具的时候 form表单只有一个text和一个button每当我键入enter的时候就刷新.很是郁闷,直接在form上onsumbit=false.才行. 下面是我查询 ...
- PHPCMS V9 SQL查询篇
1.添加查询条件 {php $sql5 = " pay_type_int = 24"} {pc:content action="lists" catid=&qu ...
- LINQ to Entities does not recognize the method 'Int32 ToInt32(System.String)' method, and this method cannot be translated into a store expression
if (!string.IsNullOrEmpty(FarmWorkId)) { data = data.Where(p => p.TypeId == Convert.ToInt32(FarmW ...
- 5 -- Hibernate的基本用法 --4 7 二级缓存相关属性
Hibernate的SessionFactory可持有一个可选的二级缓存,通过使用这种二级缓存可以提高Hibernate的持久化访问的性能. Hibernate的二级缓存属性: ⊙ hibernate ...
- office系列调节背景主题
更改背景主题可以参考:https://jingyan.baidu.com/article/ff42efa9332adec19e220200.html 但是这种方法只是改变了整个软件外框架的背景颜色.以 ...
- 【Android】Android中如何取消调转界面后EditText默认获取聚焦问题
参考资料: https://www.cnblogs.com/dream-cichan/p/aaaa.html http://blog.csdn.net/u013703461/article/detai ...
- 【代码审计】QYKCMS_v4.3.2 任意文件删除漏洞分析
0x00 环境准备 QYKCMS官网:http://www.qykcms.com/ 网站源码版本:QYKCMS_v4.3.2(企业站主题) 程序源码下载:http://bbs.qingyunke. ...