直播APP源码是如何实现音视频同步的
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", ×tampNs));
err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
} else {
mNumOutputFramesDropped += !mIsAudio;
//该帧video太迟,直接丢弃
err = mCodec->releaseOutputBuffer(bufferIx);
}
}
以上就是直播APP源码实现音视频同步的流程。
本文转自https://www.cnblogs.com/dyufei/p/8018440.html 仅作分享科普用,如有侵权欢迎联系作者删除。
直播APP源码是如何实现音视频同步的的更多相关文章
- 如何理解直播APP源码开发中的音视频同步
视频 直播APP源码的视频的播放过程可以简单理解为一帧一帧的画面按照时间顺序呈现出来的过程,就像在一个本子的每一页画上画,然后快速翻动的感觉. 但是在实际应用中,并不是每一帧都是完整的画面,因为如果直 ...
- 直播带货APP源码开发为什么选择云服务器
云服务器可以为直播带货APP源码提供弹性计算以及更高的运行效率,避免资源浪费,随着直播带货APP源码业务需求的变化,可以实时扩展或缩减计算资源.CVM支持按实际使用的资源计费,可以节约计算成本. 一. ...
- PHP直播平台源码搭建教程
直播源码市场火爆,但是PHP直播平台源码的搭建过程较为复杂,本文就简单为大家概述一下直播的实现过程以及PHP直播平台源码是如何搭建的. 一.直播的定义 如今PHP直播平台源码绝大部分情况下是指在现场架 ...
- 方维 o2o app源码出售
方维 o2o app源码出售 方维o2oapp源码出售 1.本人官方5万购买,现把方维o2o app 源码低价出售: 2.包括网站源码本地搭建包成功提供指导 3.包括网站说明文档,不包含app说明文档 ...
- android 在线升级借助开源中国App源码
android 在线升级借助开源中国App源码 http://www.cnblogs.com/luomingui/p/3949429.html android 在线升级借助开源中国App源码分析如下: ...
- APP源码集中打包大放送!十一个千万级别APP源码随意处置!
小伙伴们还在一个一个苦苦寻找各类APP源码吗?此贴集中打包最常用APP的源码,你想得到的APP,这里都有! 想做商城?这里有天猫! 想做同城服务?这里有大众点评! 想做外卖?这里有饿了么! 想做视频? ...
- iOS高仿app源码:纯代码打造高仿优质《内涵段子》
iOS高仿app源码:纯代码打造高仿优质<内涵段子>收藏下来 字数1950 阅读4999 评论173 喜欢133 Github 地址 https://github.com/Charlesy ...
- android动画源码合集、动态主题框架、社交app源码等
Android精选源码 仿MIUI果冻视图-BouncingJellyView 一个快速易用的动态主题框架 android动画效果集合源码 android使用Kotlin开发的Dribbb ...
- 导入android源码中的APP源码到eclipse
导入android源码中的APP源码到eclipse 一般最简单的办法就是创建新的android工程,选择create project from existing source选项,直接导入源码就OK ...
随机推荐
- python中np.arrange用法
转载:https://www.cnblogs.com/ghllfl/p/8487692.html np.arange()经常用,其用法总结如下: np.arange(0,60,2) 生成从0到60的步 ...
- P4821 [中山市选]生成树
题目链接 我们可以看一下题目中给的这张图. 首先,树是没有环的,所以我们要把所有的环上的边都删去一条. 我们可以现在每个五边形上删去一条边. 但删完之后我们会发现,里面还有一圈. 这时候,我们就要在这 ...
- 对于dijkstra最短路算法的复习
好久没有看图论了,就从最短路算法开始了. dijkstra算法的本质是贪心.只适用于不含负权的图中.因为出现负权的话,贪心会出错. 一般来说,我们用堆(优先队列)来优化,将它O(n2)的复杂度优化为O ...
- PHP添加新扩展包的步骤
1.找到PHP解压包,将 php.ini-development 这个文件复制一份,并修改后缀名为 .ini 2.将这个文件打开,将此处注释解开, 3.配置你扩展的该包的位置 4.如果显示不 ...
- DevOps元素周期表—2号元素Kibana
Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一,设计用于和 Elasticsearch 协作.您可以使用 Kibana 对 Elasticsearch 索引 ...
- 多测师讲解selenium _滚动条定位_高级讲师肖sir
from selenium import webdriverfrom time import sleepdrvier=webdriver.Chrome()drvier.get('http://www. ...
- 【python】python返回结果多了none(递归时)
把每个返回值的print使用return替代即可 例子: def trim(s): if s[:1]==" ": s=s[1:] retrim(s) elif s[-1:]==&q ...
- 发布MeteoInfo 1.2.6
增加了对AWX卫星数据格式的支持(和C#版本的功能相当).在MeteoInfo中打开AWX数据: 在MeteoInfoLab中打开AWX数据:
- linux创建www用户组和用户
linux创建www用户组和用户 wdcp中的nginx服务启动需要依赖www用户,因此若没有此用户就可能会启动失败.创建这个用户的方法: [root@bogon local]# id www [ro ...
- Codeforces Round #676 (Div. 2) A - D个人题解(E题待补)
1421A. XORwice 题目链接:Click Here // Author : RioTian // Time : 20/10/18 #include <bits/stdc++.h> ...