NuPlayer源码分析四渲染模块音视频同步
NuPlayer源码分析四:渲染模块&音视频同步
渲染模块&音视频同步
文章目录
系列文章分为如下几个模块:
渲染模块的作用是,将音频、视频数据安装一定的同步策略通过对应的设备输出。这是所有的播放器都不可或缺的模块。
NuPlayer
的渲染类为
Renderer
,定义在NuPlayerRenderer.h文件中。它的主要功能有:
- 缓存数据
- 音频设备初始化&数据播放
- 视频数据播放
- 音视频同步功能
缓存数据
在表明缓存逻辑之前,先介绍一下
NuPlayerRenderer
缓存数据的结构:
struct QueueEntry {
sp<MediaCodecBuffer> mBuffer; // 如果该字段不为NULL,则包含了真实数据
sp<AMessage> mMeta;
sp<AMessage> mNotifyConsumed; // 如果该字段为NULL,则表示当前QueueEntry是最后一个(EOS)。
size_t mOffset;
status_t mFinalResult;
int32_t mBufferOrdinal;
};
List<QueueEntry> mAudioQueue; // 用以缓存音频解码数据的队列,队列实体为QueueEntry
List<QueueEntry> mVideoQueue; // 用以缓存视频解码数据的队列,队列实体为QueueEntry
来看看逻辑部分,看两个队列是如何被填满的。
NuPlayerRenderer
渲染器的创建是在解码模块初始化之前实现的,解码模块在实例化并启动(start)后,如果已经有了解码数据,通过一些列调用后,会调用到
NuPlayer::Renderer::onQueueBuffer
,将解码后的数据存放到缓存队列中去,调用链条如下:
NuPlayer::Decoder::onMessageReceived
==>
handleAnOutputBuffer
==>
NuPlayer:::Renderer::queueBuffer
==>
NuPlayer::Renderer::onQueueBuffer
。
void NuPlayer::Renderer::onQueueBuffer(const sp<AMessage> &msg) {
int32_t audio;
CHECK(msg->findInt32("audio", &audio));
if (audio) {
mHasAudio = true; // 需要缓存的是解码后的音频数据
} else {
mHasVideo = true; // 需要缓存的是解码后的视频数据
}
if (mHasVideo) {
if (mVideoScheduler == NULL) {
mVideoScheduler = new VideoFrameScheduler(); // 用于调整视频渲染计划
mVideoScheduler->init();
}
}
sp<RefBase> obj;
CHECK(msg->findObject("buffer", &obj));
// 获取需要被缓存的解码数据
sp<MediaCodecBuffer> buffer = static_cast<MediaCodecBuffer *>(obj.get());
QueueEntry entry; // 创建队列实体对象,并将解码后的buffer传递进去
entry.mBuffer = buffer;
entry.mNotifyConsumed = notifyConsumed;
entry.mOffset = 0;
entry.mFinalResult = OK;
entry.mBufferOrdinal = ++mTotalBuffersQueued; // 当前队列实体在队列中的序号
if (audio) { // 音频
Mutex::Autolock autoLock(mLock);
mAudioQueue.push_back(entry); // 将包含了解码数据的队列实体添加到音频队列队尾。
postDrainAudioQueue_l(); // 刷新/播放音频
} else { // 视频
mVideoQueue.push_back(entry); // 将包含了解码数据的队列实体添加到音频队列队尾。
postDrainVideoQueue(); // 刷新/播放视频
}
sp<MediaCodecBuffer> firstAudioBuffer = (*mAudioQueue.begin()).mBuffer;
sp<MediaCodecBuffer> firstVideoBuffer = (*mVideoQueue.begin()).mBuffer;
// ...
int64_t firstAudioTimeUs;
int64_t firstVideoTimeUs;
CHECK(firstAudioBuffer->meta()->findInt64("timeUs", &firstAudioTimeUs));
CHECK(firstVideoBuffer->meta()->findInt64("timeUs", &firstVideoTimeUs));
// 计算队列中第一帧视频和第一帧音频的时间差值
int64_t diff = firstVideoTimeUs - firstAudioTimeUs;
ALOGV("queueDiff = %.2f secs", diff / 1E6);
if (diff > 100000ll) {
// 如果音频播放比视频播放的时间超前大于0.1秒,则丢弃掉音频数据
(*mAudioQueue.begin()).mNotifyConsumed->post();
mAudioQueue.erase(mAudioQueue.begin()); // 从音频队列中删掉队首音频数据
return;
}
syncQueuesDone_l(); // 刷新/播放音视频数据
}
这里,对于音视频设备刷新和播放的函数并没有做太多的解读,留到下节来说。
音频设备初始化&数据播放
音频设备初始化
对于Android系统来说,音频的播放最终都绕不开AudioSink对象。NuPlayer中的AudioSink对象早在NuPlayer播放器创建时就已经创建,并传入NuPlayer体系中,可以回过头去看看 NuPlayer播放器创建 一节。
接下来在创建解码器的过程中,也就是NuPlayer::instantiateDecoder函数调用创建音频解码器的同时,会触发一系列对
AudioSink
的初始化和启动动作。调用链如下:
NuPlayer::instantiateDecoder
==>
NuPlayer::determineAudioModeChange
==>
NuPlayer::tryOpenAudioSinkForOffload
==>
NuPlayer::Renderer::openAudioSink
==>
NuPlayer::Renderer::onOpenAudioSink
status_t NuPlayer::Renderer::onOpenAudioSink(
const sp<AMessage> &format,
bool offloadOnly,
bool hasVideo,
uint32_t flags,
bool isStreaming) {
ALOGV("openAudioSink: offloadOnly(%d) offloadingAudio(%d)",
offloadOnly, offloadingAudio());
bool audioSinkChanged = false;
int32_t numChannels;
CHECK(format->findInt32("channel-count", &numChannels)); // 获取声道数
int32_t sampleRate;
CHECK(format->findInt32("sample-rate", &sampleRate)); // 获取采样率
if (!offloadOnly && !offloadingAudio()) { // 非offload模式打开AudioSink
audioSinkChanged = true;
mAudioSink->close();
mCurrentOffloadInfo = AUDIO_INFO_INITIALIZER;
status_t err = mAudioSink->open( // 打开AudioSink
sampleRate, // 采样率
numChannels, // 声道数
(audio_channel_mask_t)channelMask,
AUDIO_FORMAT_PCM_16_BIT, // 音频格式
0 /* bufferCount - unused */,
mUseAudioCallback ? &NuPlayer::Renderer::AudioSinkCallback : NULL,
mUseAudioCallback ? this : NULL,
(audio_output_flags_t)pcmFlags,
NULL,
doNotReconnect,
frameCount);
mCurrentPcmInfo = info;
if (!mPaused) { // for preview mode, don't start if paused
mAudioSink->start(); // 启动AudioSink
}
}
mAudioTornDown = false;
return OK;
}
在这个函数执行完启动AudioSink的操作后,只需要往AudioSink中写数据,音频数据便能够得到输出。
音频数据输出
音频数据输出的触发函数是
postDrainAudioQueue_l
,在
缓存数据
一节中分析
NuPlayer::Renderer::onQueueBuffer
函数执行时,当数据被缓存在音频队列后,
postDrainAudioQueue_l
便会执行,让数据最终写入到
AudioSink
中播放。而
postDrainAudioQueue_l
函数简单处理后,就通过Nativehandler机制,将调用传递到了
NuPlayer::Renderer::onMessageReceived
的
kWhatDrainAudioQueue
case中:
case kWhatDrainAudioQueue:
{
if (onDrainAudioQueue()) { // 真正往AudioSink中写数据的函数
uint32_t numFramesPlayed;
CHECK_EQ(mAudioSink->getPosition(&numFramesPlayed), (status_t)OK);
uint32_t numFramesPendingPlayout = mNumFramesWritten - numFramesPlayed;
// AudioSink已经缓存的可用于播放数据的时间长度
int64_t delayUs = mAudioSink->msecsPerFrame()
* numFramesPendingPlayout * 1000ll;
if (mPlaybackRate > 1.0f) {
delayUs /= mPlaybackRate; // 计算当前播放速度下的可播放时长
}
// 计算一半播放时长的延迟,刷新数据
delayUs /= 2;
postDrainAudioQueue_l(delayUs); // 重新调用刷新数据的循环
}
break;
}
下面重点照顾一下真正往
AudioSink
中写数据的函数:
bool NuPlayer::Renderer::onDrainAudioQueue() {
// ...
uint32_t prevFramesWritten = mNumFramesWritten;
while (!mAudioQueue.empty()) { // 如果音频的缓冲队列中还有数据,循环就不停止
QueueEntry *entry = &*mAudioQueue.begin(); // 取出队首队列实体
// ...
mLastAudioBufferDrained = entry->mBufferOrdinal;
size_t copy = entry->mBuffer->size() - entry->mOffset;
// 写入AudioSink,此时应该能可以听到声音了。
ssize_t written = mAudioSink->write(entry->mBuffer->data() + entry->mOffset,
copy, false /* blocking */);
// ...
entry->mNotifyConsumed->post(); // 通知解码器数据已经消耗
mAudioQueue.erase(mAudioQueue.begin()); // 从队列中删掉已经播放的数据实体
// ...
}
// 计算我们是否需要重新安排另一次写入。
bool reschedule = !mAudioQueue.empty() && (!mPaused
|| prevFramesWritten != mNumFramesWritten); // permit pause to fill
return reschedule;
}
函数看着很短,其实很长,有需要的,可以自己去研究一下。
视频数据播放
视频数据输出的时机几乎和音频数据输出是一样的,即在播放器创建完成并启动后便开始了。区别只是,音频执行了
postDrainAudioQueue_l
,而视频执行的是:
postDrainVideoQueue
。
void NuPlayer::Renderer::postDrainVideoQueue() {
QueueEntry &entry = *mVideoQueue.begin(); // 从队列中取数据
sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this);
msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);
mDrainVideoQueuePending = true;
}
这里的代码自然不会这么简单,我几乎全部删掉,这些被删掉的代码基本都是同步相关,我准备留在下一步讲。
回来看代码执行到哪儿了:
void NuPlayer::Renderer::onDrainVideoQueue() {
QueueEntry *entry = &*mVideoQueue.begin();
entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000ll);
entry->mNotifyConsumed->setInt32("render", !tooLate);
entry->mNotifyConsumed->post(); // 通知解码器已经消耗数据
mVideoQueue.erase(mVideoQueue.begin()); // 删掉已经处理的数据
entry = NULL;
if (!mPaused) {
if (!mVideoRenderingStarted) {
mVideoRenderingStarted = true;
notifyVideoRenderingStart();
}
Mutex::Autolock autoLock(mLock);
notifyIfMediaRenderingStarted_l(); // 向上层(播放器)通知渲染开始
}
}
同样有删除了和同步相关的代码
可能有人有疑问,这里并没有类似于向AudioSink中写数据的操作啊!怎么就渲染了?
相较于音频而言,显示视频数据的设备(
Surface
)和
MediaCodec
高度绑定,这个函数能做的,只是将数据实体通过NativeHandler消息的机制,通过mNotifyConsumed传递给MediaCodec,告诉解码器就可以了。所以,在
entry->mNotifyConsumed->post()
函数执行后,回调函数将最终执行到
NuPlayer::Decoder::onRenderBuffer
随后便会播放。
音视频同步功能
音视频同步的目的是 :让音频数据和视频数据能够在同一时间输出到对应设备中去。
音视频同步对于任何一个播放器而言,都是重中之重,在实际环境中,音视频同步问题的Bug,也是音视频项目中出现的一类大问题。
在本小结,将从原理讲起,同时分析NuPlayer中关于同步部分的代码。
在音频和视频输出的相关部分,删除了很多有关音视频同步的代码,在这一节都会补上。
时间戳
因为音频、视频等数据在漫长的处理流程中,无法保证同时到达输出设备。为了达到 同时 的目的,就出现了 时间戳 的概念:标定一段数据流的解码、和在设备上的显示时间。接下来我会重点分析在设备上的显示时间,也就是通常所说的PTS时间。
参考时钟
参考时钟 是一条线性递增的时间线,通常选择系统时钟来作为参考时钟。
在制作音频视频数据时,会根据参考时钟上的时间为每个数据块打上时间戳,以便在播放时可以再指定的时间输出。
在播放时,会从数据块中取出时间戳,对比当前参考时钟,进行策略性播放。这种策略可能是音频为基准、也可能是视频为基准。
Android NuPlayer同步方案
音视频同步方案有很多,NuPlayer选择了最常用的一种: 音频同步
音频同步 的意思是:以音频数据的播放时间为参考时钟,视频数据根据音频数据的播放时间做参考,如果视频超前将会被延迟播放,如果落后将会被快速播放或者丢弃。
当然音视频同步只有在既有音频也有视频的情况下才成立,如果仅有其中一方,NuPlayer会按照它们自己的时间播放的。
接下来,我们回到NuPlayer的源码,来分析NuPlayer是如何做好音频同步方案的。
NuPlayer同步实现
在分析音视频同步代码之前,先来看看一个比较重要的类
MediaClock
,它完成了参考时钟的功能。
MediaClock::媒体时钟
struct MediaClock : public RefBase {
// 在暂停状态下,需要使用刚渲染帧的时间戳作为锚定时间。
void updateAnchor(
int64_t anchorTimeMediaUs,
int64_t anchorTimeRealUs,
int64_t maxTimeMediaUs = INT64_MAX);
// 查询与实时| realUs |对应的媒体时间,并将结果保存到| outMediaUs |中。
status_t getMediaTime(
int64_t realUs,
int64_t *outMediaUs,
bool allowPastMaxTime = false) const;
// 查询媒体时间对应的实时时间| targetMediaUs |。 结果保存在| outRealUs |中
status_t getRealTimeFor(int64_t targetMediaUs, int64_t *outRealUs) const;
private:
status_t getMediaTime_l(
int64_t realUs,
int64_t *outMediaUs,
bool allowPastMaxTime) const;
int64_t mAnchorTimeMediaUs; // 锚定媒体时间:数据块中的媒体时间
int64_t mAnchorTimeRealUs; // 锚定显示时间:数据块的实时显示时间
int64_t mMaxTimeMediaUs; // 最大媒体时间
int64_t mStartingTimeMediaUs; // 开始播放时的媒体时间
float mPlaybackRate; // 播放速率
DISALLOW_EVIL_CONSTRUCTORS(MediaClock);
};
其中比较重要的就是几个时间、和处理时间的函数。下面逐个分析一下这几个函数。
updateAnchor
函数的作用是,将当前正在播放的时间更新的
MediaClock
中。
void MediaClock::updateAnchor(
int64_t anchorTimeMediaUs, // 数据流的时间戳
int64_t anchorTimeRealUs, // 计算出的媒体数据显示真实时间
int64_t maxTimeMediaUs) { // 最大媒体时间
int64_t nowUs = ALooper::GetNowUs(); // 获取当前系统时间
int64_t nowMediaUs = // 重新计算数据显示的真实时间
anchorTimeMediaUs + (nowUs - anchorTimeRealUs) * (double)mPlaybackRate;
if (nowMediaUs < 0) { // 如果时间已经超过当前系统时间就不更新时间了
ALOGW("reject anchor time since it leads to negative media time.");
return;
}
if (maxTimeMediaUs != -1) {
mMaxTimeMediaUs = maxTimeMediaUs;
}
if (mAnchorTimeRealUs != -1) {
int64_t oldNowMediaUs =
mAnchorTimeMediaUs + (nowUs - mAnchorTimeRealUs) * (double)mPlaybackRate;
if (nowMediaUs < oldNowMediaUs
&& nowMediaUs > oldNowMediaUs - kAnchorFluctuationAllowedUs) {
return;
}
}
mAnchorTimeRealUs = nowUs; // 以当前时间更新播放时间
mAnchorTimeMediaUs = nowMediaUs; // 以数据流的时间戳更新锚定媒体时间
}
getMediaTime
查询与实时| realUs |对应的媒体时间,并将结果保存到| outMediaUs |中。
status_t MediaClock::getMediaTime(
int64_t realUs, int64_t *outMediaUs, bool allowPastMaxTime) const {
if (outMediaUs == NULL) {
return BAD_VALUE;
}
Mutex::Autolock autoLock(mLock);
return getMediaTime_l(realUs, outMediaUs, allowPastMaxTime);
}
status_t MediaClock::getMediaTime_l(
int64_t realUs, int64_t *outMediaUs, bool allowPastMaxTime) const {
if (mAnchorTimeRealUs == -1) {
return NO_INIT;
}
int64_t mediaUs = mAnchorTimeMediaUs
+ (realUs - mAnchorTimeRealUs) * (double)mPlaybackRate;
if (mediaUs > mMaxTimeMediaUs && !allowPastMaxTime) {
mediaUs = mMaxTimeMediaUs;
}
if (mediaUs < mStartingTimeMediaUs) {
mediaUs = mStartingTimeMediaUs;
}
if (mediaUs < 0) {
mediaUs = 0;
}
*outMediaUs = mediaUs;
return OK;
}
getRealTimeFor
查询媒体时间对应的实时时间| targetMediaUs |。 结果保存在| outRealUs |中,通常被视频播放时调用查询视频数据真实的显示时间。
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;
}
status_t MediaClock::getMediaTime_l(
int64_t realUs, int64_t *outMediaUs, bool allowPastMaxTime) const {
// 媒体时间 = 锚点媒体时间 + (系统时间 - 锚点媒体时间)*播放速度
int64_t mediaUs = mAnchorTimeMediaUs + (realUs - mAnchorTimeRealUs) * (double)mPlaybackRate;
// 媒体时间,不能超过mMaxTimeMediaUs
if (mediaUs > mMaxTimeMediaUs && !allowPastMaxTime) {
mediaUs = mMaxTimeMediaUs;
}
// 媒体时间,不能小于mMaxTimeMediaUs
if (mediaUs < mStartingTimeMediaUs) {
mediaUs = mStartingTimeMediaUs;
}
if (mediaUs < 0) {
mediaUs = 0;
}
*outMediaUs = mediaUs;
return OK;
}
音视同步-音频
音频数据对音视同步中的贡献,就是提供自己的播放时间,用以更新
MediaClock
。
而音频数据播放的时间已经在
渲染模块—音频数据
输出一节中讲到,是在
NuPlayer::Renderer::onDrainAudioQueue()
函数中完成的。
bool NuPlayer::Renderer::onDrainAudioQueue() {
// ...
uint32_t prevFramesWritten = mNumFramesWritten;
while (!mAudioQueue.empty()) { // 如果音频的缓冲队列中还有数据,循环就不停止
QueueEntry *entry = &*mAudioQueue.begin(); // 取出队首队列实体
// ...
mLastAudioBufferDrained = entry->mBufferOrdinal;
// ignore 0-sized buffer which could be EOS marker with no data
if (entry->mOffset == 0 && entry->mBuffer->size() > 0) {
int64_t mediaTimeUs; // 获取数据块的时间
CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
ALOGV("onDrainAudioQueue: rendering audio at media time %.2f secs",
mediaTimeUs / 1E6);
onNewAudioMediaTime(mediaTimeUs); // 将新的媒体时间更新到MediaClock中
}
size_t copy = entry->mBuffer->size() - entry->mOffset;
// 写入AudioSink,此时应该能可以听到声音了。
ssize_t written = mAudioSink->write(entry->mBuffer->data() + entry->mOffset,
copy, false /* blocking */);
// ...
entry->mNotifyConsumed->post(); // 通知解码器数据已经消耗
mAudioQueue.erase(mAudioQueue.begin()); // 从队列中删掉已经播放的数据实体
// ...
}
// 计算我们是否需要重新安排另一次写入。
bool reschedule = !mAudioQueue.empty() && (!mPaused
|| prevFramesWritten != mNumFramesWritten); // permit pause to fill
return reschedule;
}
该函数中,关于播放的大部分内容已经在音频输出模块讲过了,现在重点关注一下音视频同步相关的函数:
void NuPlayer::Renderer::onNewAudioMediaTime(int64_t mediaTimeUs) {
if (mediaTimeUs == mAnchorTimeMediaUs) {
return;
}
setAudioFirstAnchorTimeIfNeeded_l(mediaTimeUs); // 通过第一次的媒体时间更新第一帧锚点媒体时间
// 如果我们正在等待音频接收器启动,则mNextAudioClockUpdateTimeUs为-1
if (mNextAudioClockUpdateTimeUs == -1) {
AudioTimestamp ts;
if (mAudioSink->getTimestamp(ts) == OK && ts.mPosition > 0) {
mNextAudioClockUpdateTimeUs = 0; // 开始我们的时钟更新
}
}
int64_t nowUs = ALooper::GetNowUs();
if (mNextAudioClockUpdateTimeUs >= 0) { // 此时mNextAudioClockUpdateTimeUs = 0
if (nowUs >= mNextAudioClockUpdateTimeUs) {
// 将当前播放音频流时间戳、系统时间、音频流当前媒体时间戳更新到MediaClock
int64_t nowMediaUs = mediaTimeUs - getPendingAudioPlayoutDurationUs(nowUs);
mMediaClock->updateAnchor(nowMediaUs, nowUs, mediaTimeUs);
mUseVirtualAudioSink = false;
mNextAudioClockUpdateTimeUs = nowUs + kMinimumAudioClockUpdatePeriodUs;
}
}
mAnchorNumFramesWritten = mNumFramesWritten;
mAnchorTimeMediaUs = mediaTimeUs;
}
这部分的内容还是比较简单的。
音视同步-视频
同样,涉及到同步的代码,和视频数据播放是放在一起的,在 渲染模块—视频数据播放 中已经提到过。重新拿出来分析音视同步部分的代码。
void NuPlayer::Renderer::postDrainVideoQueue() {
QueueEntry &entry = *mVideoQueue.begin();
sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this);
bool needRepostDrainVideoQueue = false;
int64_t delayUs;
int64_t nowUs = ALooper::GetNowUs();
int64_t realTimeUs;
if (mFlags & FLAG_REAL_TIME) {
// ...
} else {
int64_t mediaTimeUs;
CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs)); // 获取媒体时间
{
Mutex::Autolock autoLock(mLock);
// mAnchorTimeMediaUs 该值会在onNewAudioMediaTime函数中,随着音频播放而更新
// 它的值如果小于零的话,意味着没有音频数据
if (mAnchorTimeMediaUs < 0) { // 没有音频数据,则使用视频将以系统时间为准播放
// 只有视频的情况,使用媒体时间和系统时间更新MediaClock
mMediaClock->updateAnchor(mediaTimeUs, nowUs, mediaTimeUs);
mAnchorTimeMediaUs = mediaTimeUs;
realTimeUs = nowUs;
} else if (!mVideoSampleReceived) { // 没有收到视频帧
// 显示时间为当前系统时间,意味着一直显示第一帧
realTimeUs = nowUs;
} else if (mAudioFirstAnchorTimeMediaUs < 0
|| mMediaClock->getRealTimeFor(mediaTimeUs, &realTimeUs) == OK) {
// 一个正常的音视频数据,通常都走这里
realTimeUs = getRealTimeUs(mediaTimeUs, nowUs); // 获取视频数据的显示事件
} else if (mediaTimeUs - mAudioFirstAnchorTimeMediaUs >= 0) {
// 其它情况,视频的显示时间就是系统时间
needRepostDrainVideoQueue = true;
realTimeUs = nowUs;
} else {
realTimeUs = nowUs; // 其它情况,视频的显示时间就是系统时间
}
}
if (!mHasAudio) { // 没有音频流的情况下,
// 平滑的输出视频需要 >= 10fps, 所以,以当前视频流的媒体时间戳+100ms作为maxTimeMedia
mMediaClock->updateMaxTimeMedia(mediaTimeUs + 100000);
}
delayUs = realTimeUs - nowUs; // 计算视频播放的延迟
int64_t postDelayUs = -1;
if (delayUs > 500000) { // 如果延迟超过500ms
postDelayUs = 500000; // 将延迟时间设置为500ms
if (mHasAudio && (mLastAudioBufferDrained - entry.mBufferOrdinal) <= 0) {、
// 如果有音频,并且音频队列的还有未消耗的数据又有新数据增加,则将延迟时间设为10ms
postDelayUs = 10000;
}
} else if (needRepostDrainVideoQueue) {
postDelayUs = mediaTimeUs - mAudioFirstAnchorTimeMediaUs;
postDelayUs /= mPlaybackRate;
}
if (postDelayUs >= 0) { // 以音频为基准,延迟时间通常都大于零
msg->setWhat(kWhatPostDrainVideoQueue);
msg->post(postDelayUs); // 延迟发送,播放视频数据
mVideoScheduler->restart();
mDrainVideoQueuePending = true;
return;
}
}
// 依据Vsync机制调整计算出两个Vsync信号之间的时间
realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;
int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);
delayUs = realTimeUs - nowUs;
// 将Vsync信号的延迟时间考虑到视频播放指定的延迟时间中去
msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);
mDrainVideoQueuePending = true;
}
代码已经挺详细的了,其中提到了Vsync机制的概念。
在Android中,这是一种 垂直同步机制 ,用于处理两个处理速度不同的模块存在。
为了使显示的数据正确且稳定,在视频播放过程中,有两种buffer的概念,一种是处理数据的buffer,一种是专门用于显示的buffer,前者由我们的程序提供,后者往往需要驱动程序支持。因为两者的处理速度不同,所以就使用了Vsync机制。详细的,请大家Google吧。
当执行msg->post之后,消息会在指定的延迟时间后,触发解码器给显示器提供视频数据。音视频也就完了。