blob: 965014c9ad8aec14c7fdeef7c6f02475dd4a376f [file] [log] [blame] [edit]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//#define LOG_NDEBUG 0
#define LOG_TAG "VideoFrameSchedulerBase"
#include <utils/Log.h>
#define ATRACE_TAG ATRACE_TAG_VIDEO
#include <utils/Trace.h>
#include <utils/Vector.h>
#include <media/stagefright/foundation/ADebug.h>
#include <media/stagefright/foundation/AUtils.h>
#include <media/stagefright/VideoFrameSchedulerBase.h>
namespace android {
template<class T>
static int compare(const T *lhs, const T *rhs) {
if (*lhs < *rhs) {
return -1;
} else if (*lhs > *rhs) {
return 1;
} else {
return 0;
}
}
/* ======================================================================= */
/* PLL */
/* ======================================================================= */
static const size_t kMinSamplesToStartPrime = 3;
static const size_t kMinSamplesToStopPrime = VideoFrameSchedulerBase::kHistorySize;
static const size_t kMinSamplesToEstimatePeriod = 3;
static const size_t kMaxSamplesToEstimatePeriod = VideoFrameSchedulerBase::kHistorySize;
static const size_t kPrecision = 12;
static const int64_t kErrorThreshold = (1 << (kPrecision * 2)) / 10;
static const int64_t kMultiplesThresholdDiv = 4; // 25%
static const int64_t kReFitThresholdDiv = 100; // 1%
static const nsecs_t kMaxAllowedFrameSkip = VideoFrameSchedulerBase::kNanosIn1s; // 1 sec
static const nsecs_t kMinPeriod = VideoFrameSchedulerBase::kNanosIn1s / 120; // 120Hz
static const nsecs_t kRefitRefreshPeriod = 10 * VideoFrameSchedulerBase::kNanosIn1s; // 10 sec
VideoFrameSchedulerBase::PLL::PLL()
: mPeriod(-1),
mPhase(0),
mPrimed(false),
mSamplesUsedForPriming(0),
mLastTime(-1),
mNumSamples(0) {
}
void VideoFrameSchedulerBase::PLL::reset(float fps) {
//test();
mSamplesUsedForPriming = 0;
mLastTime = -1;
// set up or reset video PLL
if (fps <= 0.f) {
mPeriod = -1;
mPrimed = false;
} else {
ALOGV("reset at %.1f fps", fps);
mPeriod = (nsecs_t)(1e9 / fps + 0.5);
mPrimed = true;
}
restart();
}
// reset PLL but keep previous period estimate
void VideoFrameSchedulerBase::PLL::restart() {
mNumSamples = 0;
mPhase = -1;
}
#if 0
void VideoFrameSchedulerBase::PLL::test() {
nsecs_t period = VideoFrameSchedulerBase::kNanosIn1s / 60;
mTimes[0] = 0;
mTimes[1] = period;
mTimes[2] = period * 3;
mTimes[3] = period * 4;
mTimes[4] = period * 7;
mTimes[5] = period * 8;
mTimes[6] = period * 10;
mTimes[7] = period * 12;
mNumSamples = 8;
int64_t a, b, err;
fit(0, period * 12 / 7, 8, &a, &b, &err);
// a = 0.8(5)+
// b = -0.14097(2)+
// err = 0.2750578(703)+
ALOGD("a=%lld (%.6f), b=%lld (%.6f), err=%lld (%.6f)",
(long long)a, (a / (float)(1 << kPrecision)),
(long long)b, (b / (float)(1 << kPrecision)),
(long long)err, (err / (float)(1 << (kPrecision * 2))));
}
#endif
// If overflow happens, the value is already incorrect, and no mater what value we get is OK.
// And this part of calculation is not important, so it's OK to simply disable overflow check
// instead of using double which makes code more complicated.
__attribute__((no_sanitize("integer")))
bool VideoFrameSchedulerBase::PLL::fit(
nsecs_t phase, nsecs_t period, size_t numSamplesToUse,
int64_t *a, int64_t *b, int64_t *err) {
if (numSamplesToUse > mNumSamples) {
numSamplesToUse = mNumSamples;
}
if ((period >> kPrecision) == 0 ) {
ALOGW("Period is 0, or after including precision is 0 - would cause div0, returning");
return false;
}
int64_t sumX = 0;
int64_t sumXX = 0;
int64_t sumXY = 0;
int64_t sumYY = 0;
int64_t sumY = 0;
int64_t x = 0; // x usually is in [0..numSamplesToUse)
nsecs_t lastTime;
for (size_t i = 0; i < numSamplesToUse; i++) {
size_t ix = (mNumSamples - numSamplesToUse + i) % kHistorySize;
nsecs_t time = mTimes[ix];
if (i > 0) {
x += divRound(time - lastTime, period);
}
// y is usually in [-numSamplesToUse..numSamplesToUse+kRefitRefreshPeriod/kMinPeriod) << kPrecision
// ideally in [0..numSamplesToUse), but shifted by -numSamplesToUse during
// priming, and possibly shifted by up to kRefitRefreshPeriod/kMinPeriod
// while we are not refitting.
int64_t y = divRound(time - phase, period >> kPrecision);
sumX += x;
sumY += y;
sumXX += x * x;
sumXY += x * y;
sumYY += y * y;
lastTime = time;
}
int64_t div = (int64_t)numSamplesToUse * sumXX - sumX * sumX;
if (div == 0) {
return false;
}
int64_t a_nom = (int64_t)numSamplesToUse * sumXY - sumX * sumY;
int64_t b_nom = sumXX * sumY - sumX * sumXY;
*a = divRound(a_nom, div);
*b = divRound(b_nom, div);
// don't use a and b directly as the rounding error is significant
*err = sumYY - divRound(a_nom * sumXY + b_nom * sumY, div);
ALOGV("fitting[%zu] a=%lld (%.6f), b=%lld (%.6f), err=%lld (%.6f)",
numSamplesToUse,
(long long)*a, (*a / (float)(1 << kPrecision)),
(long long)*b, (*b / (float)(1 << kPrecision)),
(long long)*err, (*err / (float)(1 << (kPrecision * 2))));
return true;
}
void VideoFrameSchedulerBase::PLL::prime(size_t numSamplesToUse) {
if (numSamplesToUse > mNumSamples) {
numSamplesToUse = mNumSamples;
}
CHECK(numSamplesToUse >= 3); // must have at least 3 samples
// estimate video framerate from deltas between timestamps, and
// 2nd order deltas
Vector<nsecs_t> deltas;
nsecs_t lastTime, firstTime;
for (size_t i = 0; i < numSamplesToUse; ++i) {
size_t index = (mNumSamples - numSamplesToUse + i) % kHistorySize;
nsecs_t time = mTimes[index];
if (i > 0) {
if (time - lastTime > kMinPeriod) {
//ALOGV("delta: %lld", (long long)(time - lastTime));
deltas.push(time - lastTime);
}
} else {
firstTime = time;
}
lastTime = time;
}
deltas.sort(compare<nsecs_t>);
size_t numDeltas = deltas.size();
if (numDeltas > 1) {
nsecs_t deltaMinLimit = max(deltas[0] / kMultiplesThresholdDiv, kMinPeriod);
nsecs_t deltaMaxLimit = deltas[numDeltas / 2] * kMultiplesThresholdDiv;
for (size_t i = numDeltas / 2 + 1; i < numDeltas; ++i) {
if (deltas[i] > deltaMaxLimit) {
deltas.resize(i);
numDeltas = i;
break;
}
}
for (size_t i = 1; i < numDeltas; ++i) {
nsecs_t delta2nd = deltas[i] - deltas[i - 1];
if (delta2nd >= deltaMinLimit) {
//ALOGV("delta2: %lld", (long long)(delta2nd));
deltas.push(delta2nd);
}
}
}
// use the one that yields the best match
int64_t bestScore;
for (size_t i = 0; i < deltas.size(); ++i) {
nsecs_t delta = deltas[i];
int64_t score = 0;
#if 1
// simplest score: number of deltas that are near multiples
size_t matches = 0;
for (size_t j = 0; j < deltas.size(); ++j) {
nsecs_t err = periodicError(deltas[j], delta);
if (err < delta / kMultiplesThresholdDiv) {
++matches;
}
}
score = matches;
#if 0
// could be weighed by the (1 - normalized error)
if (numSamplesToUse >= kMinSamplesToEstimatePeriod) {
int64_t a, b, err;
fit(firstTime, delta, numSamplesToUse, &a, &b, &err);
err = (1 << (2 * kPrecision)) - err;
score *= max(0, err);
}
#endif
#else
// or use the error as a negative score
if (numSamplesToUse >= kMinSamplesToEstimatePeriod) {
int64_t a, b, err;
fit(firstTime, delta, numSamplesToUse, &a, &b, &err);
score = -delta * err;
}
#endif
if (i == 0 || score > bestScore) {
bestScore = score;
mPeriod = delta;
mPhase = firstTime;
}
}
ALOGV("priming[%zu] phase:%lld period:%lld",
numSamplesToUse, (long long)mPhase, (long long)mPeriod);
}
nsecs_t VideoFrameSchedulerBase::PLL::addSample(nsecs_t time) {
if (mLastTime >= 0
// if time goes backward, or we skipped rendering
&& (time > mLastTime + kMaxAllowedFrameSkip || time < mLastTime)) {
restart();
}
mLastTime = time;
mTimes[mNumSamples % kHistorySize] = time;
++mNumSamples;
bool doFit = time > mRefitAt;
if ((mPeriod <= 0 || !mPrimed) && mNumSamples >= kMinSamplesToStartPrime) {
prime(kMinSamplesToStopPrime);
++mSamplesUsedForPriming;
doFit = true;
}
if (mPeriod > 0 && mNumSamples >= kMinSamplesToEstimatePeriod) {
if (mPhase < 0) {
// initialize phase to the current render time
mPhase = time;
doFit = true;
} else if (!doFit) {
int64_t err = periodicError(time - mPhase, mPeriod);
doFit = err > mPeriod / kReFitThresholdDiv;
}
if (doFit) {
int64_t a, b, err;
if (!fit(mPhase, mPeriod, kMaxSamplesToEstimatePeriod, &a, &b, &err)) {
// samples are not suitable for fitting. this means they are
// also not suitable for priming.
ALOGV("could not fit - keeping old period:%lld", (long long)mPeriod);
return mPeriod;
}
mRefitAt = time + kRefitRefreshPeriod;
mPhase += (mPeriod * b) >> kPrecision;
mPeriod = (mPeriod * a) >> kPrecision;
ALOGV("new phase:%lld period:%lld", (long long)mPhase, (long long)mPeriod);
if (err < kErrorThreshold) {
if (!mPrimed && mSamplesUsedForPriming >= kMinSamplesToStopPrime) {
mPrimed = true;
}
} else {
mPrimed = false;
mSamplesUsedForPriming = 0;
}
}
}
return mPeriod;
}
nsecs_t VideoFrameSchedulerBase::PLL::getPeriod() const {
return mPrimed ? mPeriod : 0;
}
/* ======================================================================= */
/* Frame Scheduler */
/* ======================================================================= */
VideoFrameSchedulerBase::VideoFrameSchedulerBase()
: mVsyncTime(0),
mVsyncPeriod(0),
mVsyncRefreshAt(0),
mLastVsyncTime(-1),
mTimeCorrection(0) {
}
void VideoFrameSchedulerBase::init(float videoFps) {
updateVsync();
mLastVsyncTime = -1;
mTimeCorrection = 0;
mPll.reset(videoFps);
}
void VideoFrameSchedulerBase::restart() {
mLastVsyncTime = -1;
mTimeCorrection = 0;
mPll.restart();
}
nsecs_t VideoFrameSchedulerBase::getVsyncPeriod() {
if (mVsyncPeriod > 0) {
return mVsyncPeriod;
}
return kDefaultVsyncPeriod;
}
float VideoFrameSchedulerBase::getFrameRate() {
nsecs_t videoPeriod = mPll.getPeriod();
if (videoPeriod > 0) {
return 1e9 / videoPeriod;
}
return 0.f;
}
nsecs_t VideoFrameSchedulerBase::schedule(nsecs_t renderTime) {
nsecs_t origRenderTime = renderTime;
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
if (now >= mVsyncRefreshAt) {
updateVsync();
}
// without VSYNC info, there is nothing to do
if (mVsyncPeriod == 0) {
ALOGV("no vsync: render=%lld", (long long)renderTime);
return renderTime;
}
// ensure vsync time is well before (corrected) render time
if (mVsyncTime > renderTime - 4 * mVsyncPeriod) {
mVsyncTime -=
((mVsyncTime - renderTime) / mVsyncPeriod + 5) * mVsyncPeriod;
}
// Video presentation takes place at the VSYNC _after_ renderTime. Adjust renderTime
// so this effectively becomes a rounding operation (to the _closest_ VSYNC.)
renderTime -= mVsyncPeriod / 2;
const nsecs_t videoPeriod = mPll.addSample(origRenderTime);
if (videoPeriod > 0) {
// Smooth out rendering
size_t N = 12;
nsecs_t fiveSixthDev =
abs(((videoPeriod * 5 + mVsyncPeriod) % (mVsyncPeriod * 6)) - mVsyncPeriod)
/ (mVsyncPeriod / 100);
// use 20 samples if we are doing 5:6 ratio +- 1% (e.g. playing 50Hz on 60Hz)
if (fiveSixthDev < 12) { /* 12% / 6 = 2% */
N = 20;
}
nsecs_t offset = 0;
nsecs_t edgeRemainder = 0;
for (size_t i = 1; i <= N; i++) {
offset +=
(renderTime + mTimeCorrection + videoPeriod * i - mVsyncTime) % mVsyncPeriod;
edgeRemainder += (videoPeriod * i) % mVsyncPeriod;
}
mTimeCorrection += mVsyncPeriod / 2 - offset / (nsecs_t)N;
renderTime += mTimeCorrection;
nsecs_t correctionLimit = mVsyncPeriod * 3 / 5;
edgeRemainder = abs(edgeRemainder / (nsecs_t)N - mVsyncPeriod / 2);
if (edgeRemainder <= mVsyncPeriod / 3) {
correctionLimit /= 2;
}
// estimate how many VSYNCs a frame will spend on the display
nsecs_t nextVsyncTime =
renderTime + mVsyncPeriod - ((renderTime - mVsyncTime) % mVsyncPeriod);
if (mLastVsyncTime >= 0) {
size_t minVsyncsPerFrame = videoPeriod / mVsyncPeriod;
size_t vsyncsForLastFrame = divRound(nextVsyncTime - mLastVsyncTime, mVsyncPeriod);
bool vsyncsPerFrameAreNearlyConstant =
periodicError(videoPeriod, mVsyncPeriod) / (mVsyncPeriod / 20) == 0;
if (mTimeCorrection > correctionLimit &&
(vsyncsPerFrameAreNearlyConstant || vsyncsForLastFrame > minVsyncsPerFrame)) {
// remove a VSYNC
mTimeCorrection -= mVsyncPeriod / 2;
renderTime -= mVsyncPeriod / 2;
nextVsyncTime -= mVsyncPeriod;
if (vsyncsForLastFrame > 0)
--vsyncsForLastFrame;
} else if (mTimeCorrection < -correctionLimit &&
(vsyncsPerFrameAreNearlyConstant || vsyncsForLastFrame == minVsyncsPerFrame)) {
// add a VSYNC
mTimeCorrection += mVsyncPeriod / 2;
renderTime += mVsyncPeriod / 2;
nextVsyncTime += mVsyncPeriod;
if (vsyncsForLastFrame < ULONG_MAX)
++vsyncsForLastFrame;
} else if (mTimeCorrection < -correctionLimit * 2
|| mTimeCorrection > correctionLimit * 2) {
ALOGW("correction beyond limit: %lld vs %lld (vsyncs for last frame: %zu, min: %zu)"
" restarting. render=%lld",
(long long)mTimeCorrection, (long long)correctionLimit,
vsyncsForLastFrame, minVsyncsPerFrame, (long long)origRenderTime);
restart();
return origRenderTime;
}
ATRACE_INT64("FRAME_VSYNCS", vsyncsForLastFrame);
}
mLastVsyncTime = nextVsyncTime;
}
// align rendertime to the center between VSYNC edges
renderTime -= (renderTime - mVsyncTime) % mVsyncPeriod;
renderTime += mVsyncPeriod / 2;
ALOGV("adjusting render: %lld => %lld", (long long)origRenderTime, (long long)renderTime);
ATRACE_INT64("FRAME_FLIP_IN(ms)", (renderTime - now) / 1000000);
return renderTime;
}
VideoFrameSchedulerBase::~VideoFrameSchedulerBase() {}
} // namespace android