Fix potential null pointer access in HardwareVideoEncoder
There was no check for null in the code that prepends config buffer to key frame buffer. It is not a requirement for codec to deliver config buffer. Some codecs probably do not do that.
Bug: none
Change-Id: Id9c57efc5d1de5f569fa773313da4db3cd8b074c
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/299900
Reviewed-by: Erik Språng <sprang@webrtc.org>
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#39807}
diff --git a/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java b/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java
index bd01b7d..0a6e344 100644
--- a/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java
+++ b/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java
@@ -579,75 +579,81 @@
return;
}
- ByteBuffer codecOutputBuffer = codec.getOutputBuffer(index);
- codecOutputBuffer.position(info.offset);
- codecOutputBuffer.limit(info.offset + info.size);
+ ByteBuffer outputBuffer = codec.getOutputBuffer(index);
+ outputBuffer.position(info.offset);
+ outputBuffer.limit(info.offset + info.size);
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
Logging.d(TAG, "Config frame generated. Offset: " + info.offset + ". Size: " + info.size);
- configBuffer = ByteBuffer.allocateDirect(info.size);
- configBuffer.put(codecOutputBuffer);
- } else {
- bitrateAdjuster.reportEncodedFrame(info.size);
- if (adjustedBitrate != bitrateAdjuster.getAdjustedBitrateBps()) {
- updateBitrate();
+ if (info.size > 0
+ && (codecType == VideoCodecMimeType.H264 || codecType == VideoCodecMimeType.H265)) {
+ // In case of H264 and H265 config buffer contains SPS and PPS headers. Presence of these
+ // headers makes IDR frame a truly keyframe. Some encoders issue IDR frames without SPS
+ // and PPS. We save config buffer here to prepend it to all IDR frames encoder delivers.
+ configBuffer = ByteBuffer.allocateDirect(info.size);
+ configBuffer.put(outputBuffer);
}
-
- final boolean isKeyFrame = (info.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
- if (isKeyFrame) {
- Logging.d(TAG, "Sync frame generated");
- }
-
- final ByteBuffer frameBuffer;
- if (isKeyFrame && codecType == VideoCodecMimeType.H264) {
- Logging.d(TAG,
- "Prepending config frame of size " + configBuffer.capacity()
- + " to output buffer with offset " + info.offset + ", size " + info.size);
- // For H.264 key frame prepend SPS and PPS NALs at the start.
- frameBuffer = ByteBuffer.allocateDirect(info.size + configBuffer.capacity());
- configBuffer.rewind();
- frameBuffer.put(configBuffer);
- frameBuffer.put(codecOutputBuffer);
- frameBuffer.rewind();
- } else {
- frameBuffer = codecOutputBuffer.slice();
- }
-
- final EncodedImage.FrameType frameType = isKeyFrame
- ? EncodedImage.FrameType.VideoFrameKey
- : EncodedImage.FrameType.VideoFrameDelta;
-
- outputBuffersBusyCount.increment();
- EncodedImage.Builder builder = outputBuilders.poll();
- 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.
- encodedImage.release();
+ return;
}
+
+ bitrateAdjuster.reportEncodedFrame(info.size);
+ if (adjustedBitrate != bitrateAdjuster.getAdjustedBitrateBps()) {
+ updateBitrate();
+ }
+
+ final boolean isKeyFrame = (info.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
+ if (isKeyFrame) {
+ Logging.d(TAG, "Sync frame generated");
+ }
+
+ final ByteBuffer frameBuffer;
+ final Runnable releaseCallback;
+ if (isKeyFrame && configBuffer != null) {
+ Logging.d(TAG,
+ "Prepending config buffer of size " + configBuffer.capacity()
+ + " to output buffer with offset " + info.offset + ", size " + info.size);
+ frameBuffer = ByteBuffer.allocateDirect(info.size + configBuffer.capacity());
+ configBuffer.rewind();
+ frameBuffer.put(configBuffer);
+ frameBuffer.put(outputBuffer);
+ frameBuffer.rewind();
+ codec.releaseOutputBuffer(index, /* render= */ false);
+ releaseCallback = null;
+ } else {
+ frameBuffer = outputBuffer.slice();
+ outputBuffersBusyCount.increment();
+ releaseCallback = () -> {
+ // 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, /* render= */ false);
+ } catch (Exception e) {
+ Logging.e(TAG, "releaseOutputBuffer failed", e);
+ }
+ outputBuffersBusyCount.decrement();
+ };
+ }
+
+ final EncodedImage.FrameType frameType = isKeyFrame ? EncodedImage.FrameType.VideoFrameKey
+ : EncodedImage.FrameType.VideoFrameDelta;
+
+ EncodedImage.Builder builder = outputBuilders.poll();
+ builder.setBuffer(frameBuffer, releaseCallback).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.
+ encodedImage.release();
} catch (IllegalStateException e) {
Logging.e(TAG, "deliverOutput failed", e);
}
diff --git a/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java b/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java
index 36bfb20..919975d 100644
--- a/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java
+++ b/sdk/android/tests/src/org/webrtc/HardwareVideoEncoderTest.java
@@ -10,15 +10,24 @@
package org.webrtc;
+import static android.media.MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
+import static android.media.MediaCodec.BUFFER_FLAG_SYNC_FRAME;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.webrtc.VideoCodecMimeType.AV1;
+import static org.webrtc.VideoCodecMimeType.H264;
+import static org.webrtc.VideoCodecMimeType.H265;
+import static org.webrtc.VideoCodecMimeType.VP8;
+import static org.webrtc.VideoCodecMimeType.VP9;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
@@ -27,6 +36,7 @@
import android.os.Bundle;
import androidx.test.runner.AndroidJUnit4;
import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
@@ -131,7 +141,7 @@
}
private class TestEncoderBuilder {
- private VideoCodecMimeType codecType = VideoCodecMimeType.VP8;
+ private VideoCodecMimeType codecType = VP8;
private BitrateAdjuster bitrateAdjuster = new BaseBitrateAdjuster();
private boolean isEncodingStatisticsSupported;
@@ -187,8 +197,7 @@
@Test
public void testInit() {
// Set-up.
- HardwareVideoEncoder encoder =
- new TestEncoderBuilder().setCodecType(VideoCodecMimeType.VP8).build();
+ HardwareVideoEncoder encoder = new TestEncoderBuilder().setCodecType(VP8).build();
// Test.
assertThat(encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback))
@@ -203,8 +212,7 @@
.isEqualTo(TEST_ENCODER_SETTINGS.width);
assertThat(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT))
.isEqualTo(TEST_ENCODER_SETTINGS.height);
- assertThat(mediaFormat.getString(MediaFormat.KEY_MIME))
- .isEqualTo(VideoCodecMimeType.VP8.mimeType());
+ assertThat(mediaFormat.getString(MediaFormat.KEY_MIME)).isEqualTo(VP8.mimeType());
assertThat(fakeMediaCodecWrapper.getConfiguredFlags())
.isEqualTo(MediaCodec.CONFIGURE_FLAG_ENCODE);
@@ -231,7 +239,7 @@
fakeMediaCodecWrapper.addOutputData(CodecTestHelper.generateRandomData(100),
/* presentationTimestampUs= */ 0,
- /* flags= */ MediaCodec.BUFFER_FLAG_SYNC_FRAME);
+ /* flags= */ BUFFER_FLAG_SYNC_FRAME);
encoder.waitDeliverEncodedImage();
@@ -267,7 +275,7 @@
fakeMediaCodecWrapper.addOutputData(CodecTestHelper.generateRandomData(100),
/* presentationTimestampUs= */ 0,
- /* flags= */ MediaCodec.BUFFER_FLAG_SYNC_FRAME);
+ /* flags= */ BUFFER_FLAG_SYNC_FRAME);
encoder.waitDeliverEncodedImage();
@@ -454,4 +462,83 @@
assertThat(timestampCaptor.getAllValues())
.containsExactly(0L, frameDurationMs, 2 * frameDurationMs);
}
+
+ private void encodeWithConfigBuffer(VideoCodecMimeType codecType, boolean keyFrame,
+ boolean emptyConfig, String expected) throws InterruptedException {
+ String configData = emptyConfig ? "" : "config";
+ byte[] configBytes = configData.getBytes(Charset.defaultCharset());
+ byte[] frameBytes = "frame".getBytes(Charset.defaultCharset());
+ byte[] expectedBytes = expected.getBytes(Charset.defaultCharset());
+
+ TestEncoder encoder = new TestEncoderBuilder().setCodecType(codecType).build();
+ encoder.initEncode(TEST_ENCODER_SETTINGS, mockEncoderCallback);
+
+ encoder.encode(createTestVideoFrame(/* timestampNs= */ 0), ENCODE_INFO_DELTA_FRAME);
+
+ fakeMediaCodecWrapper.addOutputData(
+ configBytes, /* presentationTimestampUs= */ 0, /* flags= */ BUFFER_FLAG_CODEC_CONFIG);
+ encoder.waitDeliverEncodedImage();
+
+ fakeMediaCodecWrapper.addOutputData(frameBytes, /* presentationTimestampUs= */ 0,
+ /* flags= */ keyFrame ? BUFFER_FLAG_SYNC_FRAME : 0);
+ encoder.waitDeliverEncodedImage();
+
+ verify(mockEncoderCallback)
+ .onEncodedFrame(
+ argThat(
+ (EncodedImage encoded) -> encoded.buffer.equals(ByteBuffer.wrap(expectedBytes))),
+ nullable(CodecSpecificInfo.class));
+
+ assertThat(encoder.release()).isEqualTo(VideoCodecStatus.OK);
+ }
+
+ @Test
+ public void encode_vp8KeyFrame_nonEmptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(VP8, /*keyFrame=*/true, /* emptyConfig= */ false, "frame");
+ }
+
+ @Test
+ public void encode_vp9KeyFrame_nonEmptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(VP9, /*keyFrame=*/true, /* emptyConfig= */ false, "frame");
+ }
+
+ @Test
+ public void encode_av1KeyFrame_nonEmptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(AV1, /*keyFrame=*/true, /* emptyConfig= */ false, "frame");
+ }
+
+ @Test
+ public void encode_h264KeyFrame_nonEmptyConfig_configPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(H264, /*keyFrame=*/true, /* emptyConfig= */ false, "configframe");
+ }
+
+ @Test
+ public void encode_h265KeyFrame_nonEmptyConfig_configPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(H265, /*keyFrame=*/true, /* emptyConfig= */ false, "configframe");
+ }
+
+ @Test
+ public void encode_vp8DeltaFrame_nonEmptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(VP8, /*keyFrame=*/false, /* emptyConfig= */ false, "frame");
+ }
+
+ @Test
+ public void encode_vp9DeltaFrame_nonEmptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(VP9, /*keyFrame=*/false, /* emptyConfig= */ false, "frame");
+ }
+
+ @Test
+ public void encode_av1DeltaFrame_nonEmptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(AV1, /*keyFrame=*/false, /* emptyConfig= */ false, "frame");
+ }
+
+ @Test
+ public void encode_h264KeyFrame_emptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(H264, /*keyFrame=*/true, /* emptyConfig= */ true, "frame");
+ }
+
+ @Test
+ public void encode_h265KeyFrame_emptyConfig_configNotPrepended() throws InterruptedException {
+ encodeWithConfigBuffer(H265, /*keyFrame=*/true, /* emptyConfig= */ true, "frame");
+ }
}