Fetch encoded QP from MediaCodec encoders
It is a part of "encoding statistics" feature [1] available in Android SDK 33. Local testing revealed that for HW VP8/9 encoders we get QP in range [0,64] which is not what WebRTC quality scaler expects. Exclude VP8/9 encoders for now.
[1] https://developer.android.com/reference/android/media/MediaFormat#VIDEO_ENCODING_STATISTICS_LEVEL_1
Bug: webrtc:15015
Change-Id: I8af2fd96afb34e18cb3e2cc3562b10149324c16e
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/298306
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#39722}
diff --git a/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java b/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java
index b5f7629..bd01b7d 100644
--- a/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java
+++ b/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java
@@ -16,6 +16,7 @@
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaFormat;
import android.opengl.GLES20;
import android.os.Build;
@@ -162,6 +163,9 @@
// value to send exceptions thrown during release back to the encoder thread.
@Nullable private volatile Exception shutdownException;
+ // True if collection of encoding statistics is enabled.
+ private boolean isEncodingStatisticsEnabled;
+
/**
* Creates a new HardwareVideoEncoder with the given codecName, codecType, colorFormat, key frame
* intervals, and bitrateAdjuster.
@@ -226,6 +230,8 @@
nextPresentationTimestampUs = 0;
lastKeyFrameNs = -1;
+ isEncodingStatisticsEnabled = false;
+
try {
codec = mediaCodecWrapperFactory.createByCodecName(codecName);
} catch (IOException | IllegalArgumentException e) {
@@ -258,6 +264,13 @@
Logging.w(TAG, "Unknown profile level id: " + profileLevelId);
}
}
+
+ if (isEncodingStatisticsSupported()) {
+ format.setInteger(MediaFormat.KEY_VIDEO_ENCODING_STATISTICS_LEVEL,
+ MediaFormat.VIDEO_ENCODING_STATISTICS_LEVEL_1);
+ isEncodingStatisticsEnabled = true;
+ }
+
Logging.d(TAG, "Format: " + format);
codec.configure(
format, null /* surface */, null /* crypto */, MediaCodec.CONFIGURE_FLAG_ENCODE);
@@ -606,21 +619,30 @@
outputBuffersBusyCount.increment();
EncodedImage.Builder builder = outputBuilders.poll();
- EncodedImage encodedImage = builder
- .setBuffer(frameBuffer,
- () -> {
- // This callback should not throw any exceptions since
- // it may be called on an arbitrary thread.
- // Check bug webrtc:11230 for more details.
- try {
- codec.releaseOutputBuffer(index, false);
- } catch (Exception e) {
- Logging.e(TAG, "releaseOutputBuffer failed", e);
- }
- outputBuffersBusyCount.decrement();
- })
- .setFrameType(frameType)
- .createEncodedImage();
+ builder
+ .setBuffer(frameBuffer,
+ () -> {
+ // This callback should not throw any exceptions since
+ // it may be called on an arbitrary thread.
+ // Check bug webrtc:11230 for more details.
+ try {
+ codec.releaseOutputBuffer(index, false);
+ } catch (Exception e) {
+ Logging.e(TAG, "releaseOutputBuffer failed", e);
+ }
+ outputBuffersBusyCount.decrement();
+ })
+ .setFrameType(frameType);
+
+ if (isEncodingStatisticsEnabled) {
+ MediaFormat format = codec.getOutputFormat(index);
+ if (format != null && format.containsKey(MediaFormat.KEY_VIDEO_QP_AVERAGE)) {
+ int qp = format.getInteger(MediaFormat.KEY_VIDEO_QP_AVERAGE);
+ builder.setQp(qp);
+ }
+ }
+
+ EncodedImage encodedImage = builder.createEncodedImage();
// TODO(mellem): Set codec-specific info.
callback.onEncodedFrame(encodedImage, new CodecSpecificInfo());
// Note that the callback may have retained the image.
@@ -685,6 +707,29 @@
return height;
}
+ protected boolean isEncodingStatisticsSupported() {
+ // WebRTC quality scaler, which adjusts resolution and/or frame rate based on encoded QP,
+ // expects QP to be in native bitstream range for given codec. Native QP range for VP8 is
+ // [0, 127] and for VP9 is [0, 255]. MediaCodec VP8 and VP9 encoders (perhaps not all)
+ // return QP in range [0, 64], which is libvpx API specific range. Due to this mismatch we
+ // can't use QP feedback from these codecs.
+ if (codecType == VideoCodecMimeType.VP8 || codecType == VideoCodecMimeType.VP9) {
+ return false;
+ }
+
+ MediaCodecInfo codecInfo = codec.getCodecInfo();
+ if (codecInfo == null) {
+ return false;
+ }
+
+ CodecCapabilities codecCaps = codecInfo.getCapabilitiesForType(codecType.mimeType());
+ if (codecCaps == null) {
+ return false;
+ }
+
+ return codecCaps.isFeatureSupported(CodecCapabilities.FEATURE_EncodingStatistics);
+ }
+
// Visible for testing.
protected void fillInputBuffer(ByteBuffer buffer, VideoFrame.Buffer videoFrameBuffer) {
yuvFormat.fillBuffer(buffer, videoFrameBuffer, stride, sliceHeight);
diff --git a/sdk/android/src/java/org/webrtc/MediaCodecWrapper.java b/sdk/android/src/java/org/webrtc/MediaCodecWrapper.java
index 60c853d..6abdbfe 100644
--- a/sdk/android/src/java/org/webrtc/MediaCodecWrapper.java
+++ b/sdk/android/src/java/org/webrtc/MediaCodecWrapper.java
@@ -11,6 +11,7 @@
package org.webrtc;
import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Bundle;
@@ -52,4 +53,8 @@
Surface createInputSurface();
void setParameters(Bundle params);
+
+ MediaCodecInfo getCodecInfo();
+
+ MediaFormat getOutputFormat(int index);
}
diff --git a/sdk/android/src/java/org/webrtc/MediaCodecWrapperFactoryImpl.java b/sdk/android/src/java/org/webrtc/MediaCodecWrapperFactoryImpl.java
index 2ba62ac..56ab21f 100644
--- a/sdk/android/src/java/org/webrtc/MediaCodecWrapperFactoryImpl.java
+++ b/sdk/android/src/java/org/webrtc/MediaCodecWrapperFactoryImpl.java
@@ -12,6 +12,7 @@
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.os.Bundle;
@@ -106,6 +107,16 @@
public void setParameters(Bundle params) {
mediaCodec.setParameters(params);
}
+
+ @Override
+ public MediaCodecInfo getCodecInfo() {
+ return mediaCodec.getCodecInfo();
+ }
+
+ @Override
+ public MediaFormat getOutputFormat(int index) {
+ return mediaCodec.getOutputFormat(index);
+ }
}
@Override
diff --git a/sdk/android/tests/src/org/webrtc/FakeMediaCodecWrapper.java b/sdk/android/tests/src/org/webrtc/FakeMediaCodecWrapper.java
index fb7aba4..5e2a1f4 100644
--- a/sdk/android/tests/src/org/webrtc/FakeMediaCodecWrapper.java
+++ b/sdk/android/tests/src/org/webrtc/FakeMediaCodecWrapper.java
@@ -12,6 +12,7 @@
import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCrypto;
import android.media.MediaFormat;
@@ -318,4 +319,14 @@
@Override
public void setParameters(Bundle params) {}
+
+ @Override
+ public MediaCodecInfo getCodecInfo() {
+ return null;
+ }
+
+ @Override
+ public MediaFormat getOutputFormat(int index) {
+ return outputFormat;
+ }
}
diff --git a/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java b/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java
index bd4a642..36bfb20 100644
--- a/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java
+++ b/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java
@@ -15,6 +15,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -22,6 +23,7 @@
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
+import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.test.runner.AndroidJUnit4;
import java.nio.ByteBuffer;
@@ -69,13 +71,16 @@
private static class TestEncoder extends HardwareVideoEncoder {
private final Object deliverEncodedImageLock = new Object();
private boolean deliverEncodedImageDone = true;
+ private boolean isEncodingStatisticsSupported;
TestEncoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName,
VideoCodecMimeType codecType, Integer surfaceColorFormat, Integer yuvColorFormat,
Map<String, String> params, int keyFrameIntervalSec, int forceKeyFrameIntervalMs,
- BitrateAdjuster bitrateAdjuster, EglBase14.Context sharedContext) {
+ BitrateAdjuster bitrateAdjuster, EglBase14.Context sharedContext,
+ boolean isEncodingStatisticsSupported) {
super(mediaCodecWrapperFactory, codecName, codecType, surfaceColorFormat, yuvColorFormat,
params, keyFrameIntervalSec, forceKeyFrameIntervalMs, bitrateAdjuster, sharedContext);
+ this.isEncodingStatisticsSupported = isEncodingStatisticsSupported;
}
public void waitDeliverEncodedImage() throws InterruptedException {
@@ -118,11 +123,17 @@
buffer.flip();
i420Buffer.release();
}
+
+ @Override
+ protected boolean isEncodingStatisticsSupported() {
+ return isEncodingStatisticsSupported;
+ }
}
private class TestEncoderBuilder {
private VideoCodecMimeType codecType = VideoCodecMimeType.VP8;
private BitrateAdjuster bitrateAdjuster = new BaseBitrateAdjuster();
+ private boolean isEncodingStatisticsSupported;
public TestEncoderBuilder setCodecType(VideoCodecMimeType codecType) {
this.codecType = codecType;
@@ -134,6 +145,12 @@
return this;
}
+ public TestEncoderBuilder SetIsEncodingStatisticsSupported(
+ boolean isEncodingStatisticsSupported) {
+ this.isEncodingStatisticsSupported = isEncodingStatisticsSupported;
+ return this;
+ }
+
public TestEncoder build() {
return new TestEncoder((String name)
-> fakeMediaCodecWrapper,
@@ -143,7 +160,7 @@
/* params= */ new HashMap<>(),
/* keyFrameIntervalSec= */ 0,
/* forceKeyFrameIntervalMs= */ 0, bitrateAdjuster,
- /* sharedContext= */ null);
+ /* sharedContext= */ null, isEncodingStatisticsSupported);
}
}
@@ -194,6 +211,76 @@
}
@Test
+ public void encodingStatistics_unsupported_disabled() throws InterruptedException {
+ TestEncoder encoder = new TestEncoderBuilder().SetIsEncodingStatisticsSupported(false).build();
+
+ assertThat(encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback))
+ .isEqualTo(VideoCodecStatus.OK);
+
+ MediaFormat configuredFormat = fakeMediaCodecWrapper.getConfiguredFormat();
+ assertThat(configuredFormat).isNotNull();
+ assertThat(configuredFormat.containsKey(MediaFormat.KEY_VIDEO_ENCODING_STATISTICS_LEVEL))
+ .isFalse();
+
+ // Verify that QP is not set in encoded frame even if reported by MediaCodec.
+ MediaFormat outputFormat = new MediaFormat();
+ outputFormat.setInteger(MediaFormat.KEY_VIDEO_QP_AVERAGE, 123);
+ doReturn(outputFormat).when(fakeMediaCodecWrapper).getOutputFormat(anyInt());
+
+ encoder.encode(createTestVideoFrame(/* timestampNs= */ 42), ENCODE_INFO_KEY_FRAME);
+
+ fakeMediaCodecWrapper.addOutputData(CodecTestHelper.generateRandomData(100),
+ /* presentationTimestampUs= */ 0,
+ /* flags= */ MediaCodec.BUFFER_FLAG_SYNC_FRAME);
+
+ encoder.waitDeliverEncodedImage();
+
+ ArgumentCaptor<EncodedImage> videoFrameCaptor = ArgumentCaptor.forClass(EncodedImage.class);
+ verify(mockEncoderCallback)
+ .onEncodedFrame(videoFrameCaptor.capture(), any(CodecSpecificInfo.class));
+
+ EncodedImage videoFrame = videoFrameCaptor.getValue();
+ assertThat(videoFrame).isNotNull();
+ assertThat(videoFrame.qp).isNull();
+ }
+
+ @Test
+ public void encodingStatistics_supported_enabled() throws InterruptedException {
+ TestEncoder encoder = new TestEncoderBuilder().SetIsEncodingStatisticsSupported(true).build();
+
+ assertThat(encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback))
+ .isEqualTo(VideoCodecStatus.OK);
+
+ MediaFormat configuredFormat = fakeMediaCodecWrapper.getConfiguredFormat();
+ assertThat(configuredFormat).isNotNull();
+ assertThat(configuredFormat.containsKey(MediaFormat.KEY_VIDEO_ENCODING_STATISTICS_LEVEL))
+ .isTrue();
+ assertThat(configuredFormat.getInteger(MediaFormat.KEY_VIDEO_ENCODING_STATISTICS_LEVEL))
+ .isEqualTo(MediaFormat.VIDEO_ENCODING_STATISTICS_LEVEL_1);
+
+ // Verify that QP is set in encoded frame.
+ MediaFormat outputFormat = new MediaFormat();
+ outputFormat.setInteger(MediaFormat.KEY_VIDEO_QP_AVERAGE, 123);
+ doReturn(outputFormat).when(fakeMediaCodecWrapper).getOutputFormat(anyInt());
+
+ encoder.encode(createTestVideoFrame(/* timestampNs= */ 42), ENCODE_INFO_KEY_FRAME);
+
+ fakeMediaCodecWrapper.addOutputData(CodecTestHelper.generateRandomData(100),
+ /* presentationTimestampUs= */ 0,
+ /* flags= */ MediaCodec.BUFFER_FLAG_SYNC_FRAME);
+
+ encoder.waitDeliverEncodedImage();
+
+ ArgumentCaptor<EncodedImage> videoFrameCaptor = ArgumentCaptor.forClass(EncodedImage.class);
+ verify(mockEncoderCallback)
+ .onEncodedFrame(videoFrameCaptor.capture(), any(CodecSpecificInfo.class));
+
+ EncodedImage videoFrame = videoFrameCaptor.getValue();
+ assertThat(videoFrame).isNotNull();
+ assertThat(videoFrame.qp).isEqualTo(123);
+ }
+
+ @Test
public void testEncodeByteBuffer() {
// Set-up.
HardwareVideoEncoder encoder = new TestEncoderBuilder().build();