1.  音视频同步原理

1)时间戳

直播APP源码音视频同步主要用于在音视频流的播放过程中,让同一时刻录制的声音和图像在播放的时候尽可能的在同一个时间输出。

解决直播APP源码音视频同步问题的最佳方案就是时间戳:首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);生成数据流时依据参考时钟上的时间给每个数据块都打上时间戳(一般包括开始时间和结束时间);在播放时,读取数据块上的时间戳,同时参考当前参考时钟上的时间来安排播放(如果数据块的开始时间大于当前参考时钟上的时间,则不急于播放该数据块,直到参考时钟达到数据块的开始时间;如果数据块的开始时间小于当前参考时钟上的时间,则“尽快”播放这块数据或者索性将这块数据“丢弃”,以使播放进度追上参考时钟)。

直播APP源码音视频同步,主要是以audio的时间轴作为参考时钟,在没有audio的情况下,以系统的时间轴作为参考时钟。这是因为audio丢帧很容易就能听出来,而video丢帧却不容易被察觉。

避免直播APP源码音视频不同步现象有两个关键因素 —— 一是在生成数据流时要打上正确的时间戳;二是在播放时基于时间戳对数据流的控制策略,也就是对数据块早到或晚到采取不同的处理方法。

2) 录制同步

在直播APP源码视频录制过程中,音视频流都必须要打上正确的时间戳。假如,视频流内容是从0s开始的,假设10s时有人开始说话,要求配上音频流,那么音频流的起始时间应该是10s,如果时间戳从0s或其它时间开始打,则这个混合的音视频流在时间同步上本身就存在问题。

3)  播放同步

带有声音和图像的视频,在播放的时候都需要处理音视频同步的问题。Android平台,是在render图像之前,进行音视频同步的。

单独的音频或者视频流,不需要进行音视频同步处理,音视频同步只针对既有视频又有音频的流。

由于Android是以audio的时间轴作为参考时钟,音视频播放同步处理主要有如下几个关键因素:

(1)计算audio时间戳;

(2)计算video时间戳相对于audio时间戳的delay time;

(3)依据delay time判断video是早到,晚到,采取不同处理策略。

2.直播APP源码音视频播放框架

在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", &timestampNs));
err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
} else {
mNumOutputFramesDropped += !mIsAudio;
//该帧video太迟,直接丢弃
err = mCodec->releaseOutputBuffer(bufferIx);
}
}

以上就是直播APP源码实现音视频同步的流程。

本文转自https://www.cnblogs.com/dyufei/p/8018440.html 仅作分享科普用,如有侵权欢迎联系作者删除。

直播APP源码是如何实现音视频同步的的更多相关文章

  1. 如何理解直播APP源码开发中的音视频同步

    视频 直播APP源码的视频的播放过程可以简单理解为一帧一帧的画面按照时间顺序呈现出来的过程,就像在一个本子的每一页画上画,然后快速翻动的感觉. 但是在实际应用中,并不是每一帧都是完整的画面,因为如果直 ...

  2. 直播带货APP源码开发为什么选择云服务器

    云服务器可以为直播带货APP源码提供弹性计算以及更高的运行效率,避免资源浪费,随着直播带货APP源码业务需求的变化,可以实时扩展或缩减计算资源.CVM支持按实际使用的资源计费,可以节约计算成本. 一. ...

  3. PHP直播平台源码搭建教程

    直播源码市场火爆,但是PHP直播平台源码的搭建过程较为复杂,本文就简单为大家概述一下直播的实现过程以及PHP直播平台源码是如何搭建的. 一.直播的定义 如今PHP直播平台源码绝大部分情况下是指在现场架 ...

  4. 方维 o2o app源码出售

    方维 o2o app源码出售 方维o2oapp源码出售 1.本人官方5万购买,现把方维o2o app 源码低价出售: 2.包括网站源码本地搭建包成功提供指导 3.包括网站说明文档,不包含app说明文档 ...

  5. android 在线升级借助开源中国App源码

    android 在线升级借助开源中国App源码 http://www.cnblogs.com/luomingui/p/3949429.html android 在线升级借助开源中国App源码分析如下: ...

  6. APP源码集中打包大放送!十一个千万级别APP源码随意处置!

    小伙伴们还在一个一个苦苦寻找各类APP源码吗?此贴集中打包最常用APP的源码,你想得到的APP,这里都有! 想做商城?这里有天猫! 想做同城服务?这里有大众点评! 想做外卖?这里有饿了么! 想做视频? ...

  7. iOS高仿app源码:纯代码打造高仿优质《内涵段子》

    iOS高仿app源码:纯代码打造高仿优质<内涵段子>收藏下来 字数1950 阅读4999 评论173 喜欢133 Github 地址 https://github.com/Charlesy ...

  8. android动画源码合集、动态主题框架、社交app源码等

    Android精选源码 仿MIUI果冻视图-BouncingJellyView   一个快速易用的动态主题框架   android动画效果集合源码   android使用Kotlin开发的Dribbb ...

  9. 导入android源码中的APP源码到eclipse

    导入android源码中的APP源码到eclipse 一般最简单的办法就是创建新的android工程,选择create project from existing source选项,直接导入源码就OK ...

随机推荐

  1. c++中CreateEvent函数

    参考:https://blog.csdn.net/u011642774/article/details/52789969 函数原型: HANDLE CreateEvent( LPSECURITY_AT ...

  2. Matlab中imagesc用法

    来源:https://ww2.mathworks.cn/help/matlab/ref/imagesc.html?searchHighlight=imagesc&s_tid=doc_srcht ...

  3. Win10安装MongoDB

    1. 下载安装包:mongodb-win32-x86_64-2012plus-4.2.7-signed.msi 2. 安装,注意选择安装目录 3. 新建配置文件mongo.conf: ​``` #数据 ...

  4. Mybatis的学习

    mybatis: 1.初识mybatis mybatis是一个数据库框架. 1.导包 <dependency> <groupId>org.mybatis</groupId ...

  5. Oracle - ascii为0的陷阱

    一.概述 ascii0是个空字符,如果将这个字符插入到oracle数据库中会是什么现象,是null吗? 二.正式实验 创建一张测试表 create table test(id int, name va ...

  6. 《Android逆向反编译代码注入》 - 逆向安全入门必看视频教程

      适合人群: Android开发人员.逆向反编译开发人员.以及对Android逆向安全感兴趣的朋友. 视频地址: 51CTO学院:https://edu.51cto.com/course/24485 ...

  7. Acticiti流程引擎在已知当前流程定义id的情况下获取当前流程的所有信息(包括:节点和连线)

    这里我们已知流程已经部署,我的需求是获取当前流程的所有任务节点,我使用instanceof关键字来进行匹配 private List<UserTask> getProcessUserTas ...

  8. MeteoInfoLab脚本示例:水汽通量散度计算

    用ncep数据计算水汽通量散度的脚本.需要air, uwnd, vwnd和rhum变量.数据是4维数据,需要固定时间维和高度维,数据中纬度维的数据是反向的,因此读取时需要特殊的设置(::-1).脚本中 ...

  9. centos8使用systemd/systemctl管理系统/服务

    一,systemd的用途? Systemd 是 Linux 系统工具,用来启动守护进程,已成为大多数发行版的标准配置 Systemd 的优点是功能强大,使用方便, 缺点是体系庞大,非常复杂 在cent ...

  10. Linux下批量kill掉进程

    ps -ef|grep java|grep -v grep|cut -c 9-15|xargs kill -9 管道符"|"用来隔开两个命令,管道符左边命令的输出会作为管道符右边命 ...