Add histogram metrics for video playback judder
Bug: 234833109
Test: atest VideoRenderQualityTracker_test
Change-Id: I3a7f36384ab5e79f5e0e706a8f62a1e4854587a0
diff --git a/media/libstagefright/MediaCodec.cpp b/media/libstagefright/MediaCodec.cpp
index 939f6d6..bd56b18 100644
--- a/media/libstagefright/MediaCodec.cpp
+++ b/media/libstagefright/MediaCodec.cpp
@@ -215,6 +215,11 @@
static const char *kCodecFreezeDistanceHistogram =
"android.media.mediacodec.freeze.distance.histogram";
+static const char *kCodecJudderCount = "android.media.mediacodec.judder.count";
+static const char *kCodecJudderScoreAverage = "android.media.mediacodec.judder.average";
+static const char *kCodecJudderScoreMax = "android.media.mediacodec.judder.max";
+static const char *kCodecJudderScoreHistogram = "android.media.mediacodec.judder.histogram";
+
/* -1: shaper disabled
>=0: number of fields changed */
static const char *kCodecShapingEnhanced = "android.media.mediacodec.shaped";
@@ -1135,6 +1140,13 @@
mediametrics_setInt64(mMetricsHandle, kCodecFreezeDistanceAverage, histogram.getAvg());
mediametrics_setString(mMetricsHandle, kCodecFreezeDistanceHistogram, histogram.emit());
}
+ if (m.judderScoreHistogram.getCount() >= 1) {
+ const MediaHistogram &histogram = m.judderScoreHistogram;
+ mediametrics_setInt64(mMetricsHandle, kCodecJudderCount, histogram.getCount());
+ mediametrics_setInt64(mMetricsHandle, kCodecJudderScoreAverage, histogram.getAvg());
+ mediametrics_setInt64(mMetricsHandle, kCodecJudderScoreMax, histogram.getMax());
+ mediametrics_setString(mMetricsHandle, kCodecJudderScoreHistogram, histogram.emit());
+ }
}
if (mLatencyHist.getCount() != 0 ) {
diff --git a/media/libstagefright/VideoRenderQualityTracker.cpp b/media/libstagefright/VideoRenderQualityTracker.cpp
index 5f0e969..f995e25 100644
--- a/media/libstagefright/VideoRenderQualityTracker.cpp
+++ b/media/libstagefright/VideoRenderQualityTracker.cpp
@@ -55,6 +55,10 @@
freezeDurationMsHistogramBuckets = {1, 20, 40, 60, 80, 100, 120, 150, 175, 225, 300, 400, 500};
freezeDistanceMsHistogramBuckets = {0, 20, 100, 400, 1000, 2000, 3000, 4000, 8000, 15000, 30000,
60000};
+
+ // Judder configuration
+ judderErrorToleranceUs = 2000;
+ judderScoreHistogramBuckets = {1, 4, 5, 9, 11, 20, 30, 40, 50, 60, 70, 80};
}
VideoRenderQualityTracker::VideoRenderQualityTracker() : mConfiguration(Configuration()) {
@@ -239,6 +243,14 @@
processFreeze(actualRenderTimeUs, mLastRenderTimeUs, mLastFreezeEndTimeUs, mMetrics);
mLastFreezeEndTimeUs = actualRenderTimeUs;
}
+
+ // Judder is computed on the prior video frame, not the current video frame
+ int64_t judderScore = computePreviousJudderScore(mActualFrameDurationUs,
+ mContentFrameDurationUs,
+ mConfiguration);
+ if (judderScore != 0) {
+ mMetrics.judderScoreHistogram.insert(judderScore);
+ }
}
void VideoRenderQualityTracker::processFreeze(int64_t actualRenderTimeUs, int64_t lastRenderTimeUs,
@@ -252,10 +264,53 @@
}
}
+int64_t VideoRenderQualityTracker::computePreviousJudderScore(
+ const FrameDurationUs &actualFrameDurationUs,
+ const FrameDurationUs &contentFrameDurationUs,
+ const Configuration &c) {
+ // If the frame before or after was dropped, then don't generate a judder score, since any
+ // problems with frame drops are scored as a freeze instead.
+ if (actualFrameDurationUs[0] == -1 || actualFrameDurationUs[1] == -1 ||
+ actualFrameDurationUs[2] == -1) {
+ return 0;
+ }
+
+ // Don't score judder for when playback is paused or rebuffering (long frame duration), or if
+ // the player is intentionally playing each frame at a slow rate (e.g. half-rate). If the long
+ // frame duration was unintentional, it is assumed that this will be coupled with a later frame
+ // drop, and be scored as a freeze instead of judder.
+ if (actualFrameDurationUs[1] >= 2 * contentFrameDurationUs[1]) {
+ return 0;
+ }
+
+ // The judder score is based on the error of this frame
+ int64_t errorUs = actualFrameDurationUs[1] - contentFrameDurationUs[1];
+ // Don't score judder if the previous frame has high error, but this frame has low error
+ if (abs(errorUs) < c.judderErrorToleranceUs) {
+ return 0;
+ }
+
+ // Add a penalty if this frame has judder that amplifies the problem introduced by previous
+ // judder, instead of catching up for the previous judder (50, 16, 16, 50) vs (50, 16, 50, 16)
+ int64_t previousErrorUs = actualFrameDurationUs[2] - contentFrameDurationUs[2];
+ // Don't add the pentalty for errors from the previous frame if the previous frame has low error
+ if (abs(previousErrorUs) >= c.judderErrorToleranceUs) {
+ errorUs = abs(errorUs) + abs(errorUs + previousErrorUs);
+ }
+
+ // Avoid scoring judder for 3:2 pulldown or other minimally-small frame duration errors
+ if (abs(errorUs) < contentFrameDurationUs[1] / 4) {
+ return 0;
+ }
+
+ return abs(errorUs) / 1000; // error in millis to keep numbers small
+}
+
void VideoRenderQualityTracker::configureHistograms(VideoRenderQualityMetrics &m,
const Configuration &c) {
m.freezeDurationMsHistogram.setup(c.freezeDurationMsHistogramBuckets);
m.freezeDistanceMsHistogram.setup(c.freezeDistanceMsHistogramBuckets);
+ m.judderScoreHistogram.setup(c.judderScoreHistogramBuckets);
}
int64_t VideoRenderQualityTracker::nowUs() {
diff --git a/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h b/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
index 45c822a..25ccfdb 100644
--- a/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
+++ b/media/libstagefright/include/media/stagefright/VideoRenderQualityTracker.h
@@ -63,6 +63,9 @@
// A histogram of the durations between each freeze.
MediaHistogram freezeDistanceMsHistogram;
+
+ // A histogram of the judder scores.
+ MediaHistogram judderScoreHistogram;
};
///////////////////////////////////////////////////////
@@ -113,6 +116,9 @@
// Freeze configuration
std::vector<int64_t> freezeDurationMsHistogramBuckets;
std::vector<int64_t> freezeDistanceMsHistogramBuckets;
+
+ int32_t judderErrorToleranceUs;
+ std::vector<int64_t> judderScoreHistogramBuckets;
};
VideoRenderQualityTracker();
@@ -197,6 +203,11 @@
static void processFreeze(int64_t actualRenderTimeUs, int64_t lastRenderTimeUs,
int64_t lastFreezeEndTimeUs, VideoRenderQualityMetrics &m);
+ // Compute a judder score for the previously-rendered frame.
+ static int64_t computePreviousJudderScore(const FrameDurationUs &actualRenderDurationUs,
+ const FrameDurationUs &contentRenderDurationUs,
+ const Configuration &c);
+
// Check to see if a discontinuity has occurred by examining the content time and the
// app-desired render time. If so, reset some internal state.
bool resetIfDiscontinuity(int64_t contentTimeUs, int64_t desiredRenderTimeUs);
diff --git a/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp b/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
index 2249a8b..efe1990 100644
--- a/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
+++ b/media/libstagefright/tests/VideoRenderQualityTracker_test.cpp
@@ -50,12 +50,13 @@
}
}
- void render(int numFrames) {
+ void render(int numFrames, float durationMs = -1) {
+ int64_t durationUs = durationMs < 0 ? mContentFrameDurationUs : durationMs * 1000;
for (int i = 0; i < numFrames; ++i) {
mVideoRenderQualityTracker.onFrameReleased(mMediaTimeUs);
mVideoRenderQualityTracker.onFrameRendered(mMediaTimeUs, mClockTimeNs);
mMediaTimeUs += mContentFrameDurationUs;
- mClockTimeNs += mContentFrameDurationUs * 1000;
+ mClockTimeNs += durationUs * 1000;
}
}
@@ -291,4 +292,207 @@
6 * 17) / 6);
}
+TEST_F(VideoRenderQualityTrackerTest, when60hz_hasNoJudder) {
+ Configuration c;
+ Helper h(16.66, c); // ~24Hz
+ h.render({16.66, 16.66, 16.66, 16.66, 16.66, 16.66, 16.66});
+ EXPECT_LE(h.getMetrics().judderScoreHistogram.getMax(), 0);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, whenSmallVariance60hz_hasNoJudder) {
+ Configuration c;
+ Helper h(16.66, c); // ~24Hz
+ h.render({14, 18, 14, 18, 14, 18, 14, 18});
+ EXPECT_LE(h.getMetrics().judderScoreHistogram.getMax(), 0);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, whenBadSmallVariance60Hz_hasJudder) {
+ Configuration c;
+ Helper h(16.66, c); // ~24Hz
+ h.render({14, 18, 14, /* no 18 between 14s */ 14, 18, 14, 18});
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 1);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, when30Hz_hasNoJudder) {
+ Configuration c;
+ Helper h(33.33, c);
+ h.render({33.33, 33.33, 33.33, 33.33, 33.33, 33.33});
+ EXPECT_LE(h.getMetrics().judderScoreHistogram.getMax(), 0);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, whenSmallVariance30Hz_hasNoJudder) {
+ Configuration c;
+ Helper h(33.33, c);
+ h.render({29.0, 35.0, 29.0, 35.0, 29.0, 35.0});
+ EXPECT_LE(h.getMetrics().judderScoreHistogram.getMax(), 0);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, whenBadSmallVariance30Hz_hasJudder) {
+ Configuration c;
+ Helper h(33.33, c);
+ h.render({29.0, 35.0, 29.0, /* no 35 between 29s */ 29.0, 35.0, 29.0, 35.0});
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 1);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, whenBad30HzTo60Hz_hasJudder) {
+ Configuration c;
+ Helper h(33.33, c);
+ h.render({33.33, 33.33, 50.0, /* frame stayed 1 vsync too long */ 16.66, 33.33, 33.33});
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 2); // note: 2 counts of judder
+}
+
+TEST_F(VideoRenderQualityTrackerTest, when24HzTo60Hz_hasNoJudder) {
+ Configuration c;
+ Helper h(41.66, c);
+ h.render({50.0, 33.33, 50.0, 33.33, 50.0, 33.33});
+ EXPECT_LE(h.getMetrics().judderScoreHistogram.getMax(), 0);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, when25HzTo60Hz_hasJudder) {
+ Configuration c;
+ Helper h(40, c);
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ EXPECT_GT(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, when50HzTo60Hz_hasJudder) {
+ Configuration c;
+ Helper h(20, c);
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ EXPECT_GT(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, when30HzTo50Hz_hasJudder) {
+ Configuration c;
+ Helper h(33.33, c);
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ EXPECT_GT(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, whenSmallVariancePulldown24HzTo60Hz_hasNoJudder) {
+ Configuration c;
+ Helper h(41.66, c);
+ h.render({52.0, 31.33, 52.0, 31.33, 52.0, 31.33});
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 0);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, whenBad24HzTo60Hz_hasJudder) {
+ Configuration c;
+ Helper h(41.66, c);
+ h.render({50.0, 33.33, 50.0, 33.33, /* no 50 between 33s */ 33.33, 50.0, 33.33});
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 1);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, capturesJudderScoreHistogram) {
+ Configuration c;
+ c.judderErrorToleranceUs = 2000;
+ c.judderScoreHistogramBuckets = {1, 5, 8};
+ Helper h(16, c);
+ h.render({16, 16, 23, 16, 16, 10, 16, 4, 16, 20, 16, 16});
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.emit(), "0{1,2}1");
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getCount(), 4);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getMin(), 4);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getMax(), 12);
+ EXPECT_EQ(h.getMetrics().judderScoreHistogram.getAvg(), (7 + 6 + 12 + 4) / 4);
+}
+
+TEST_F(VideoRenderQualityTrackerTest, ranksJudderScoresInOrder) {
+ // Each rendering is ranked from best to worst from a user experience
+ Configuration c;
+ c.judderErrorToleranceUs = 2000;
+ c.judderScoreHistogramBuckets = {0, 1000};
+ int64_t previousScore = 0;
+
+ // 30fps poorly displayed at 60Hz
+ {
+ Helper h(33.33, c);
+ h.render({33.33, 33.33, 16.66, 50.0, 33.33, 33.33});
+ int64_t scoreBad30fpsTo60Hz = h.getMetrics().judderScoreHistogram.getMax();
+ EXPECT_GT(scoreBad30fpsTo60Hz, previousScore);
+ previousScore = scoreBad30fpsTo60Hz;
+ }
+
+ // 25fps displayed at 60hz
+ {
+ Helper h(40, c);
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ h.render({33.33, 33.33, 50.0});
+ int64_t score25fpsTo60hz = h.getMetrics().judderScoreHistogram.getMax();
+ EXPECT_GT(score25fpsTo60hz, previousScore);
+ previousScore = score25fpsTo60hz;
+ }
+
+ // 50fps displayed at 60hz
+ {
+ Helper h(20, c);
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ h.render({16.66, 16.66, 16.66, 33.33});
+ int64_t score50fpsTo60hz = h.getMetrics().judderScoreHistogram.getMax();
+ EXPECT_GT(score50fpsTo60hz, previousScore);
+ previousScore = score50fpsTo60hz;
+ }
+
+ // 24fps poorly displayed at 60Hz
+ {
+ Helper h(41.66, c);
+ h.render({50.0, 33.33, 50.0, 33.33, 33.33, 50.0, 33.33});
+ int64_t scoreBad24HzTo60Hz = h.getMetrics().judderScoreHistogram.getMax();
+ EXPECT_GT(scoreBad24HzTo60Hz, previousScore);
+ previousScore = scoreBad24HzTo60Hz;
+ }
+
+ // 30fps displayed at 50hz
+ {
+ Helper h(33.33, c);
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ h.render({40.0, 40.0, 40.0, 60.0});
+ int64_t score30fpsTo50hz = h.getMetrics().judderScoreHistogram.getMax();
+ EXPECT_GT(score30fpsTo50hz, previousScore);
+ previousScore = score30fpsTo50hz;
+ }
+
+ // 24fps displayed at 50Hz
+ {
+ Helper h(41.66, c);
+ h.render(40.0, 11);
+ h.render(60.0, 1);
+ h.render(40.0, 11);
+ h.render(60.0, 1);
+ h.render(40.0, 11);
+ int64_t score24HzTo50Hz = h.getMetrics().judderScoreHistogram.getMax();
+ EXPECT_GT(score24HzTo50Hz, previousScore);
+ previousScore = score24HzTo50Hz;
+ }
+}
+
} // android