直播APP源码是如何实现音视频同步的

简介: 直播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 &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 仅作分享科普用,如有侵权欢迎联系作者删除。

相关文章
|
10月前
|
存储 小程序 Java
热门小程序源码合集:微信抖音小程序源码支持PHP/Java/uni-app完整项目实践指南
小程序已成为企业获客与开发者创业的重要载体。本文详解PHP、Java、uni-app三大技术栈在电商、工具、服务类小程序中的源码应用,提供从开发到部署的全流程指南,并分享选型避坑与商业化落地策略,助力开发者高效构建稳定可扩展项目。
|
11月前
|
存储 Java PHP
轻量化短视频电商直播带货APP源码全解析:核心功能与设计流程​
在电商直播热潮下,开发专属直播带货APP成为抢占市场关键。本文详解原生开发轻量化APP的核心功能与全流程设计,涵盖用户登录、商品浏览、直播互动、购物车、订单及售后功能,并介绍安卓端Java、苹果端Object-C、后台PHP的技术实现,助力打造高效优质的直播电商平台。
|
小程序 Java 关系型数据库
圈子系统公众号app小程序系统源码圈子系统带即时通讯 多级圈子系统源码 兴趣小组系统开源 私密圈子系统代码 会员制社区系统
本圈子系统解决方案提供即时通讯、多级圈子、兴趣小组、私密社区及会员制管理功能。支持开源与商业方案,推荐ThinkSNS+、EasyClub及OpenFire等系统,并提供前后端技术选型建议,助力快速搭建社交平台。
648 0
不封号的外卖抢单神器,美团抢单辅助器app,autojs版本源码
这个代码提供了基础框架,包含主循环、订单检测和点击功能。实际使用时需要根据美团骑手AP
|
消息中间件 缓存 小程序
婚恋交友相亲公众号app小程序系统源码「脱单神器」婚恋平台全套代码 - 支持快速二次开发
这是一套基于SpringBoot + Vue3开发的婚恋交友系统,支持微信公众号、Uniapp小程序和APP端。系统包含实名认证、智能匹配、视频相亲、会员体系等功能,适用于婚恋社交平台和相亲交友应用。后端采用SpringBoot 3.x与MyBatis-Plus,前端使用Vue3与Uniapp,支持快速部署和二次开发。适合技术团队或有经验的个人创业者使用。
967 8
|
存储 文件存储 Android开发
仿第八区APP分发下载打包封装系统源码
该系统为仿第八区APP分发下载打包封装系统源码,支持安卓、iOS及EXE程序分发,自动判断并稳定安装。智能提取应用信息,自动生成PLIST文件和图标,提供合理的点数扣除机制。支持企业签名在线提交、专属下载页面生成、云端存储(阿里云、七牛云),并优化签名流程,支持中文包及合并分发,确保高效稳定的下载体验。 [点击查看源码](https://download.csdn.net/download/huayula/90463452)
893 22
|
小程序 搜索推荐
2025同城线下陪玩APP开发/电竞游戏平台搭建游戏陪玩APP源码/语音APP开发
线下陪玩约玩APP旨在满足现代人的社交、兴趣分享、专业指导及休闲娱乐需求。用户可通过平台结识新朋友、找到志同道合的伙伴,并享受高质量的陪玩服务。平台提供用户注册登录、陪玩师筛选与预约、实时沟通等功能,支持个性化游戏体验和高效匹配。
831 0
2025同城线下陪玩APP开发/电竞游戏平台搭建游戏陪玩APP源码/语音APP开发
|
安全 JavaScript 前端开发
小游戏源码开发之可跨app软件对接是如何设计和开发的
小游戏开发团队常需应对跨平台需求,为此设计了成熟的解决方案。流程涵盖游戏设计、技术选型、接口设计等。首先明确游戏功能与特性,选择合适的技术架构和引擎(如Unity或Cocos2d-x)。接着设计通用接口,确保与不同App的无缝对接,并制定接口规范。开发过程中实现游戏逻辑和界面,完成登录、分享及数据对接功能。最后进行测试优化,确保兼容性和性能,发布后持续维护更新。
|
前端开发 Java 测试技术
语音app系统软件源码开发搭建新手启蒙篇
在移动互联网时代,语音App已成为生活和工作的重要工具。本文为新手开发者提供语音App系统软件源码开发的启蒙指南,涵盖需求分析、技术选型、界面设计、编码实现、测试部署等关键环节。通过明确需求、选择合适的技术框架、优化用户体验、严格测试及持续维护更新,帮助开发者掌握开发流程,快速搭建功能完善的语音App。
|
Web App开发 前端开发 安全
语音交友app系统源码功能及技术研发流程剖析
语音交友App核心功能包括语音聊天(一对一、群聊、语音消息)、语音房间(直播、主题房、管理)、社交互动(好友、关注、打赏)、内容发现、音效美化、通知提醒及安全隐私等。开发流程涵盖需求分析、技术选型(前端、后端、数据库、实时通信)、UI/UX设计、前后端开发、实时通信集成、音效处理、测试优化、部署上线及运营维护,确保稳定高效运行并持续优化用户体验。

热门文章

最新文章