1. はじめに:
前回のブログでは、オーディオ部分のバッファ処理を分析しました。Nuplayer は、オーディオ バッファとビデオ バッファの処理で多くのコードを共有しています. このブログでは、その違いを直接分析します. 全体として, nuplayer の同期メカニズムは exoplayer のメカニズムに似ています. コード ストリームとシステムのポイントに基づいています.推定し、垂直同期信号の時点と組み合わせて、最終的な表示時間を決定します。違いは、表示時間の nuplayer のキャリブレーションが複雑すぎて、わかりにくい部分が多いことですが、キャリブレーションの内容に焦点を当てなければ、他の部分はまだ簡単に理解できます。
2. ビデオ フレームの表示時間を決定します。多くのオーディオおよびビデオ バッファ処理関数が共有されており、関数
を直接見つけます。NuPlayerRenderer.cpp
postDrainVideoQueue_l
void NuPlayer::Renderer::postDrainVideoQueue_l() {
if (mDrainVideoQueuePending
|| mSyncQueues
|| (mPaused && mVideoSampleReceived)) {
return;
}
if (mVideoQueue.empty()) {
return;
}
QueueEntry &entry = *mVideoQueue.begin();
/* 1.创建kWhatDrainVideoQueue消息用于后续投递处理 */
sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, id());
msg->setInt32("generation", mVideoQueueGeneration);
if (entry.mBuffer == NULL) {
// EOS doesn't carry a timestamp.
msg->post();
mDrainVideoQueuePending = true;
return;
}
int64_t delayUs;
int64_t nowUs = ALooper::GetNowUs();
int64_t realTimeUs;
if (mFlags & FLAG_REAL_TIME) {
int64_t mediaTimeUs;
CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
realTimeUs = mediaTimeUs;
} else {
int64_t mediaTimeUs;
CHECK(entry.mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
if (mAnchorTimeMediaUs < 0) {
setAnchorTime(mediaTimeUs, nowUs);
mPausePositionMediaTimeUs = mediaTimeUs;
mAnchorMaxMediaUs = mediaTimeUs;
realTimeUs = nowUs;
} else {
/* 2.获取当前帧送显时间 */
realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
}
if (!mHasAudio) {
mAnchorMaxMediaUs = mediaTimeUs + 100000; // smooth out videos >= 10fps
}
// Heuristics to handle situation when media time changed without a
// discontinuity. If we have not drained an audio buffer that was
// received after this buffer, repost in 10 msec. Otherwise repost
// in 500 msec.
/* 当前帧渲染时间差 = 当前帧时间戳 - 当前帧送显时间 */
/* 当前帧渲染时间差 = 当前帧送显时间 - 系统时间 */
delayUs = realTimeUs - nowUs;
if (delayUs > 500000) {
int64_t postDelayUs = 500000;
if (mHasAudio && (mLastAudioBufferDrained - entry.mBufferOrdinal) <= 0) {
postDelayUs = 10000;
}
msg->setWhat(kWhatPostDrainVideoQueue);
msg->post(postDelayUs);
mVideoScheduler->restart();
ALOGI("possible video time jump of %dms, retrying in %dms",
(int)(delayUs / 1000), (int)(postDelayUs / 1000));
mDrainVideoQueuePending = true;
return;
}
}
/* 3.校准送显时间 */
realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;
/* 4.计算出两个垂直同步信号用时时长 */
int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);
/* 再次计算下一帧视频渲染时间差 = 校准后的送显时间 - 系统时间 */
delayUs = realTimeUs - nowUs;
/* 5.送显:两个垂直同步信号点 */
ALOGW_IF(delayUs > 500000, "unusually high delayUs: %" PRId64, delayUs);
// post 2 display refreshes before rendering is due
msg->post(delayUs > twoVsyncsUs ? delayUs - twoVsyncsUs : 0);
mDrainVideoQueuePending = true;
}
注 1:
送信時刻を確認した後、メッセージを送信し、メッセージ処理中にレンダリング操作を実行します。
注 2:
これは仮の表示時間を取得するためのものであり、mediaTimeUs
代表nowUs
的な手段はコード ストリーム内のポイントと現在のシステム時間です。getRealTimeUs をフォローアップして、初期表示時間がどのように計算されるかを確認します。
int64_t NuPlayer::Renderer::getRealTimeUs(int64_t mediaTimeUs, int64_t nowUs) {
int64_t currentPositionUs;
/* 获取当前播放位置 */
if (mPaused || getCurrentPositionOnLooper(
¤tPositionUs, nowUs, true /* allowPastQueuedVideo */) != 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 (mediaTimeUs - currentPositionUs) + nowUs;
}
戻り値の計算を見ると、getCurrentPositionOnLooper
関数によって取得される現在の再生位置を取得する必要があります。
status_t NuPlayer::Renderer::getCurrentPositionOnLooper(
int64_t *mediaUs, int64_t nowUs, bool allowPastQueuedVideo) {
int64_t currentPositionUs;
/* if判断条件返回false,除非pause状态才会进入这里 */
if (getCurrentPositionIfPaused_l(¤tPositionUs)) {
*mediaUs = currentPositionUs;
return OK;
}
return getCurrentPositionFromAnchor(mediaUs, nowUs, allowPastQueuedVideo);
}
までフォローアップgetCurrentPositionFromAnchor
:
// Called on any threads.
status_t NuPlayer::Renderer::getCurrentPositionFromAnchor(
int64_t *mediaUs, int64_t nowUs, bool allowPastQueuedVideo) {
Mutex::Autolock autoLock(mTimeLock);
if (!mHasAudio && !mHasVideo) {
return NO_INIT;
}
if (mAnchorTimeMediaUs < 0) {
return NO_INIT;
}
/* 计算当前播放时间 = (系统时间 - 已播放时间) + 上一帧音频时间戳 */
int64_t positionUs = (nowUs - mAnchorTimeRealUs) + mAnchorTimeMediaUs;
if (mPauseStartedTimeRealUs != -1) {
positionUs -= (nowUs - mPauseStartedTimeRealUs);
}
// limit position to the last queued media time (for video only stream
// position will be discrete as we don't know how long each frame lasts)
if (mAnchorMaxMediaUs >= 0 && !allowPastQueuedVideo) {
if (positionUs > mAnchorMaxMediaUs) {
positionUs = mAnchorMaxMediaUs;
}
}
if (positionUs < mAudioFirstAnchorTimeMediaUs) {
positionUs = mAudioFirstAnchorTimeMediaUs;
}
*mediaUs = (positionUs <= 0) ? 0 : positionUs;
return OK;
}
nuplayer の同期は、オーディオのタイムスタンプに基づいてビデオ フレームを調整する戦略を引き続き使用することに注意する必要があるため、ここでは、再生されたオーディオ フレームの位置を取得する必要があります. コアはコード内のコメントです. a look at mAnchorTimeRealUs
this where is the variable updated? mAnchorTimeRealUs はオーディオの再生位置を記録し、setAnchorTime
関数によって更新されます。
void NuPlayer::Renderer::setAnchorTime(
int64_t mediaUs, int64_t realUs, int64_t numFramesWritten, bool resume) {
Mutex::Autolock autoLock(mTimeLock);
/* 更新码流中获得的音频时间戳 */
mAnchorTimeMediaUs = mediaUs;
/* 更新AudioTrack实际的播放时间 */
mAnchorTimeRealUs = realUs;
/* 更新实际已写入AudioTrack的帧数 */
mAnchorNumFramesWritten = numFramesWritten;
if (resume) {
mPauseStartedTimeRealUs = -1;
}
}
setAnchorTime は、上位層プレーヤーを介して AudioSink の fillAudioBuffer 関数をコールバックすることで実装されます。
size_t NuPlayer::Renderer::fillAudioBuffer(void *buffer, size_t size) {
...
if (mAudioFirstAnchorTimeMediaUs >= 0) {
int64_t nowUs = ALooper::GetNowUs();
setAnchorTime(mAudioFirstAnchorTimeMediaUs, nowUs - getPlayedOutAudioDurationUs(nowUs));
}
...
}
キーポイントは、getPlayedOutAudioDurationUs
関数で実際の再生を取得する方法です。
int64_t NuPlayer::Renderer::getPlayedOutAudioDurationUs(int64_t nowUs) {
uint32_t numFramesPlayed;
int64_t numFramesPlayedAt;
AudioTimestamp ts;
static const int64_t kStaleTimestamp100ms = 100000;
/* 调用getTimestamp来获取精确播放时间 */
status_t res = mAudioSink->getTimestamp(ts);
if (res == OK) {
// case 1: mixing audio tracks and offloaded tracks.
/* 获取音频已播放帧数 */
numFramesPlayed = ts.mPosition;
/* 获取底层更新该值时的系统时间 */
numFramesPlayedAt =
ts.mTime.tv_sec * 1000000LL + ts.mTime.tv_nsec / 1000;
/* 计算底层更新时与当前系统时间的时差 */
const int64_t timestampAge = nowUs - numFramesPlayedAt;
/* 如果差值超过100ms,则系统系统时间变更为当前时间 - 100ms */
if (timestampAge > kStaleTimestamp100ms) {
// This is an audio FIXME.
// getTimestamp returns a timestamp which may come from audio mixing threads.
// After pausing, the MixerThread may go idle, thus the mTime estimate may
// become stale. Assuming that the MixerThread runs 20ms, with FastMixer at 5ms,
// the max latency should be about 25ms with an average around 12ms (to be verified).
// For safety we use 100ms.
ALOGV("getTimestamp: returned stale timestamp nowUs(%lld) numFramesPlayedAt(%lld)",
(long long)nowUs, (long long)numFramesPlayedAt);
numFramesPlayedAt = nowUs - kStaleTimestamp100ms;
}
//ALOGD("getTimestamp: OK %d %lld", numFramesPlayed, (long long)numFramesPlayedAt);
} else if (res == WOULD_BLOCK) {
// case 2: transitory state on start of a new track
numFramesPlayed = 0;
numFramesPlayedAt = nowUs;
//ALOGD("getTimestamp: WOULD_BLOCK %d %lld",
// numFramesPlayed, (long long)numFramesPlayedAt);
} else {
// case 3: transitory at new track or audio fast tracks.
/* 调用getPlaybackHeadPosition获取当前播放帧数 */
res = mAudioSink->getPosition(&numFramesPlayed);
CHECK_EQ(res, (status_t)OK);
numFramesPlayedAt = nowUs;
numFramesPlayedAt += 1000LL * mAudioSink->latency() / 2; /* XXX */
//ALOGD("getPosition: %d %lld", numFramesPlayed, numFramesPlayedAt);
}
// TODO: remove the (int32_t) casting below as it may overflow at 12.4 hours.
//CHECK_EQ(numFramesPlayed & (1 << 31), 0); // can't be negative until 12.4 hrs, test
/* 实际播放时间(us) = audiotrack已播放帧数 * 1000 * 每帧大小(2ch,16bit即为4)+ 当前时间 - 底层最新值对应的系统时间 */
int64_t durationUs = (int64_t)((int32_t)numFramesPlayed * 1000LL * mAudioSink->msecsPerFrame())
+ nowUs - numFramesPlayedAt;
if (durationUs < 0) {
// Occurs when numFramesPlayed position is very small and the following:
// (1) In case 1, the time nowUs is computed before getTimestamp() is called and
// numFramesPlayedAt is greater than nowUs by time more than numFramesPlayed.
// (2) In case 3, using getPosition and adding mAudioSink->latency() to
// numFramesPlayedAt, by a time amount greater than numFramesPlayed.
//
// Both of these are transitory conditions.
ALOGV("getPlayedOutAudioDurationUs: negative duration %lld set to zero", (long long)durationUs);
durationUs = 0;
}
ALOGV("getPlayedOutAudioDurationUs(%lld) nowUs(%lld) frames(%u) framesAt(%lld)",
(long long)durationUs, (long long)nowUs, numFramesPlayed, (long long)numFramesPlayedAt);
return durationUs;
}
この関数の理解は、AudioTrackgetTimestamp
とgetPlaybackHeadPosition
2 つの関数に基づいている必要があります. 以下は、Java レイヤーの 2 つのインターフェイスです:
/* 1 */
public boolean getTimestamp(AudioTimestamp timestamp);
/* 2 */
public int getPlaybackHeadPosition();
前者はデバイスの下部に実装する必要があり、正確にptsを取得するための機能であることが理解できますが、この機能は頻繁に値を更新しないという特徴があるため、頻繁に呼び出すのには適していません。 . 公式ドキュメントに記載されているリファレンスから、10 代を使用することをお勧めします。60 代に 1 回呼び出す、エントリ クラスを見てください。
public long framePosition; /* 写入帧的位置 */
public long nanoTime; /* 更新帧位置时的系统时间 */
後者は頻繁に呼び出すことができるインターフェースです. その値は, audiotrack が再生の最初から hal レイヤーに書き込み続けるデータを返します. この 2 つの機能を理解すると, audiotrack Audio からそれらを取得するには 2 つの方法があることがわかります.ポイント。ここで、audiosink が AudioTrack をカプセル化していることを説明する必要があります。Java レイヤーとネイティブ レイヤーでは違いがあるため、AudioTrack の関数を呼び出して nuplayer コードで再生位置を取得する場合、関数名とリターン値が異なります。
実際に現在の再生時間のキャリブレーションを行っているコードを見てみましょう. キャリブレーションの原則は、ボトム フレームの値が更新されたときのシステム時間を現在の時間と比較し、差を計算し、この差を使用して行うことです。実際の経過時間を計算するためのキャリブレーション。
上記の関数に戻りますgetCurrentPositionFromAnchor
:
/* 计算当前帧播放时间 = (系统时间 - 已播放时间) + 上一帧音频时间戳 */
int64_t positionUs = (nowUs - mAnchorTimeRealUs) + mAnchorTimeMediaUs;
計算された現在の再生時間を取得すると、注 2 を理解できます。目的は、基になるオーディオ データのキャリブレーションと分析を通じて、同期状態でのビデオ フレームの表示時間を推測することです。
注 3:
ここでは、表示時間のキャリブレーションを行いますが、nuplayer のキャリブレーションは複雑すぎてわかりません。
注 4:
公式の MediaCodec ドキュメントでは、ビデオ フレームをディスプレイに送信するための提案は、事前に 2 つの同期信号ポイントであるため、nuplayer はここで 2 つの同期信号ポイントの期間を計算します。
注 5:
メッセージが 2 つの同期信号ポイント内でレンダリングのために配信されることを確認してください。このメッセージはkWhatDrainVideoQueue
注 1 のメッセージです。
3. ビデオのレンダリング:メッセージがレンダリングをどのように処理するかを
確認します。kWhatDrainVideoQueue
case kWhatDrainVideoQueue:
{
int32_t generation;
CHECK(msg->findInt32("generation", &generation));
if (generation != mVideoQueueGeneration) {
break;
}
mDrainVideoQueuePending = false;
onDrainVideoQueue();
Mutex::Autolock autoLock(mLock);
postDrainVideoQueue_l();
break;
}
onDrainVideoQueue() 関数のフォローアップ:
void NuPlayer::Renderer::onDrainVideoQueue() {
if (mVideoQueue.empty()) {
return;
}
QueueEntry *entry = &*mVideoQueue.begin();
if (entry->mBuffer == NULL) {
// EOS
notifyEOS(false /* audio */, entry->mFinalResult);
mVideoQueue.erase(mVideoQueue.begin());
entry = NULL;
setVideoLateByUs(0);
return;
}
int64_t nowUs = -1;
int64_t realTimeUs;
if (mFlags & FLAG_REAL_TIME) {
CHECK(entry->mBuffer->meta()->findInt64("timeUs", &realTimeUs));
} else {
int64_t mediaTimeUs;
CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));
nowUs = ALooper::GetNowUs();
/* 重新计算实际送显时间 */
realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
}
bool tooLate = false;
if (!mPaused) {
if (nowUs == -1) {
nowUs = ALooper::GetNowUs();
}
/* 计算出当前帧播放时差是否大于40ms */
setVideoLateByUs(nowUs - realTimeUs);
/* 大于40ms,即视频帧来的太迟了,将丢帧 */
tooLate = (mVideoLateByUs > 40000);
if (tooLate) {
ALOGV("video late by %lld us (%.2f secs)",
mVideoLateByUs, mVideoLateByUs / 1E6);
} else {
ALOGV("rendering video at media time %.2f secs",
(mFlags & FLAG_REAL_TIME ? realTimeUs :
(realTimeUs + mAnchorTimeMediaUs - mAnchorTimeRealUs)) / 1E6);
}
} else {
setVideoLateByUs(0);
if (!mVideoSampleReceived && !mHasAudio) {
// This will ensure that the first frame after a flush won't be used as anchor
// when renderer is in paused state, because resume can happen any time after seek.
setAnchorTime(-1, -1);
}
}
/* 注意这两个值决定是送显还是丢帧 */
entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000ll);
entry->mNotifyConsumed->setInt32("render", !tooLate);
entry->mNotifyConsumed->post();
mVideoQueue.erase(mVideoQueue.begin());
entry = NULL;
mVideoSampleReceived = true;
if (!mPaused) {
if (!mVideoRenderingStarted) {
mVideoRenderingStarted = true;
notifyVideoRenderingStart();
}
notifyIfMediaRenderingStarted();
}
}
この関数は複雑ではありません, メッセージの送受信処理の遅延をなくすために, 送信と表示の時間を再度調整します. ビデオ フレームの到着が遅すぎて 40 ミリ秒を超える場合, 変数は false に設定され、フレームは直接落とされるtooLate
。ここが最初に注意するところです. コード内で表示時間を再計算しているのを見ましたが,schedule
再度キャリブレーションするために呼び出されていません. バグなのかどうかわかりません.schedule
関数の原理が理解されていない.ただし、この関数は、理論上の送信時間を特定の同期時点に調整する必要があります。mNotifyConsumed->post()
配信されたメッセージは、kWhatRenderBuffer
これがオーディオ部分で分析されたということです。メッセージの処理をもう一度見てみましょう。繰り返しません。
void NuPlayer::Decoder::onRenderBuffer(const sp<AMessage> &msg) {
status_t err;
int32_t render;
size_t bufferIx;
CHECK(msg->findSize("buffer-ix", &bufferIx));
if (!mIsAudio) {
int64_t timeUs;
sp<ABuffer> buffer = mOutputBuffers[bufferIx];
buffer->meta()->findInt64("timeUs", &timeUs);
if (mCCDecoder != NULL && mCCDecoder->isSelected()) {
mCCDecoder->display(timeUs);
}
}
/* 如果render为false,就丢帧不去渲染 */
if (msg->findInt32("render", &render) && render) {
int64_t timestampNs;
CHECK(msg->findInt64("timestampNs", ×tampNs));
err = mCodec->renderOutputBufferAndRelease(bufferIx, timestampNs);
} else {
err = mCodec->releaseOutputBuffer(bufferIx);
}
if (err != OK) {
ALOGE("failed to release output buffer for %s (err=%d)",
mComponentName.c_str(), err);
handleError(err);
}
}
4. まとめ:
ビデオ フレームと同期処理メカニズムのロジック ダイアグラムは次のとおりです。