渲染模塊&音視頻同步
渲染模塊的作用是,將音頻、視頻數據安裝一定的同步策略通過對應的設備輸出。這是所有的播放器都不可或缺的模塊。
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之後,消息會在指定的延遲時間後,觸發解碼器給顯示器提供視頻數據。音視頻也就完了。