/* * Copyright 2017 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ package org.webrtc; import static android.media.MediaCodecInfo.CodecProfileLevel.AVCLevel3; import static android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR; 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; import android.os.Bundle; import android.view.Surface; import androidx.annotation.Nullable; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Map; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import org.webrtc.ThreadUtils.ThreadChecker; /** * Android hardware video encoder. */ class HardwareVideoEncoder implements VideoEncoder { private static final String TAG = "HardwareVideoEncoder"; private static final int MAX_VIDEO_FRAMERATE = 30; // See MAX_ENCODER_Q_SIZE in androidmediaencoder.cc. private static final int MAX_ENCODER_Q_SIZE = 2; private static final int MEDIA_CODEC_RELEASE_TIMEOUT_MS = 5000; private static final int DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US = 100000; // Size of the input frames should be multiple of 16 for the H/W encoder. private static final int REQUIRED_RESOLUTION_ALIGNMENT = 16; /** * Keeps track of the number of output buffers that have been passed down the pipeline and not yet * released. We need to wait for this to go down to zero before operations invalidating the output * buffers, i.e., stop() and getOutputBuffer(). */ private static class BusyCount { private final Object countLock = new Object(); private int count; public void increment() { synchronized (countLock) { count++; } } // This method may be called on an arbitrary thread. public void decrement() { synchronized (countLock) { count--; if (count == 0) { countLock.notifyAll(); } } } // The increment and waitForZero methods are called on the same thread (deliverEncodedImage, // running on the output thread). Hence, after waitForZero returns, the count will stay zero // until the same thread calls increment. public void waitForZero() { boolean wasInterrupted = false; synchronized (countLock) { while (count > 0) { try { countLock.wait(); } catch (InterruptedException e) { Logging.e(TAG, "Interrupted while waiting on busy count", e); wasInterrupted = true; } } } if (wasInterrupted) { Thread.currentThread().interrupt(); } } } // --- Initialized on construction. private final MediaCodecWrapperFactory mediaCodecWrapperFactory; private final String codecName; private final VideoCodecMimeType codecType; private final Integer surfaceColorFormat; private final Integer yuvColorFormat; private final Map params; private final int keyFrameIntervalSec; // Base interval for generating key frames. // Interval at which to force a key frame. Used to reduce color distortions caused by some // Qualcomm video encoders. private final long forcedKeyFrameNs; private final BitrateAdjuster bitrateAdjuster; // EGL context shared with the application. Used to access texture inputs. private final EglBase14.Context sharedContext; // Drawer used to draw input textures onto the codec's input surface. private final GlRectDrawer textureDrawer = new GlRectDrawer(); private final VideoFrameDrawer videoFrameDrawer = new VideoFrameDrawer(); // A queue of EncodedImage.Builders that correspond to frames in the codec. These builders are // pre-populated with all the information that can't be sent through MediaCodec. private final BlockingDeque outputBuilders = new LinkedBlockingDeque<>(); private final ThreadChecker encodeThreadChecker = new ThreadChecker(); private final ThreadChecker outputThreadChecker = new ThreadChecker(); private final BusyCount outputBuffersBusyCount = new BusyCount(); // --- Set on initialize and immutable until release. private Callback callback; private boolean automaticResizeOn; // --- Valid and immutable while an encoding session is running. @Nullable private MediaCodecWrapper codec; // Thread that delivers encoded frames to the user callback. @Nullable private Thread outputThread; // EGL base wrapping the shared texture context. Holds hooks to both the shared context and the // input surface. Making this base current allows textures from the context to be drawn onto the // surface. @Nullable private EglBase14 textureEglBase; // Input surface for the codec. The encoder will draw input textures onto this surface. @Nullable private Surface textureInputSurface; private int width; private int height; // Y-plane strides in the encoder's input private int stride; // Y-plane slice-height in the encoder's input private int sliceHeight; // True if encoder input color format is semi-planar (NV12). private boolean isSemiPlanar; // Size of frame for current color format and stride, in bytes. private int frameSizeBytes; private boolean useSurfaceMode; // --- Only accessed from the encoding thread. // Presentation timestamp of next frame to encode. private long nextPresentationTimestampUs; // Presentation timestamp of the last requested (or forced) key frame. private long lastKeyFrameNs; // --- Only accessed on the output thread. // Contents of the last observed config frame output by the MediaCodec. Used by H.264. @Nullable private ByteBuffer configBuffer; private int adjustedBitrate; // Whether the encoder is running. Volatile so that the output thread can watch this value and // exit when the encoder stops. private volatile boolean running; // Any exception thrown during shutdown. The output thread releases the MediaCodec and uses this // 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. * * @param codecName the hardware codec implementation to use * @param codecType the type of the given video codec (eg. VP8, VP9, H264, H265, AV1) * @param surfaceColorFormat color format for surface mode or null if not available * @param yuvColorFormat color format for bytebuffer mode * @param keyFrameIntervalSec interval in seconds between key frames; used to initialize the codec * @param forceKeyFrameIntervalMs interval at which to force a key frame if one is not requested; * used to reduce distortion caused by some codec implementations * @param bitrateAdjuster algorithm used to correct codec implementations that do not produce the * desired bitrates * @throws IllegalArgumentException if colorFormat is unsupported */ public HardwareVideoEncoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName, VideoCodecMimeType codecType, Integer surfaceColorFormat, Integer yuvColorFormat, Map params, int keyFrameIntervalSec, int forceKeyFrameIntervalMs, BitrateAdjuster bitrateAdjuster, EglBase14.Context sharedContext) { this.mediaCodecWrapperFactory = mediaCodecWrapperFactory; this.codecName = codecName; this.codecType = codecType; this.surfaceColorFormat = surfaceColorFormat; this.yuvColorFormat = yuvColorFormat; this.params = params; this.keyFrameIntervalSec = keyFrameIntervalSec; this.forcedKeyFrameNs = TimeUnit.MILLISECONDS.toNanos(forceKeyFrameIntervalMs); this.bitrateAdjuster = bitrateAdjuster; this.sharedContext = sharedContext; // Allow construction on a different thread. encodeThreadChecker.detachThread(); } @Override public VideoCodecStatus initEncode(Settings settings, Callback callback) { encodeThreadChecker.checkIsOnValidThread(); this.callback = callback; automaticResizeOn = settings.automaticResizeOn; this.width = settings.width; this.height = settings.height; useSurfaceMode = canUseSurface(); if (settings.startBitrate != 0 && settings.maxFramerate != 0) { bitrateAdjuster.setTargets(settings.startBitrate * 1000, settings.maxFramerate); } adjustedBitrate = bitrateAdjuster.getAdjustedBitrateBps(); Logging.d(TAG, "initEncode name: " + codecName + " type: " + codecType + " width: " + width + " height: " + height + " framerate_fps: " + settings.maxFramerate + " bitrate_kbps: " + settings.startBitrate + " surface mode: " + useSurfaceMode); return initEncodeInternal(); } private VideoCodecStatus initEncodeInternal() { encodeThreadChecker.checkIsOnValidThread(); nextPresentationTimestampUs = 0; lastKeyFrameNs = -1; isEncodingStatisticsEnabled = false; try { codec = mediaCodecWrapperFactory.createByCodecName(codecName); } catch (IOException | IllegalArgumentException e) { Logging.e(TAG, "Cannot create media encoder " + codecName); return VideoCodecStatus.FALLBACK_SOFTWARE; } final int colorFormat = useSurfaceMode ? surfaceColorFormat : yuvColorFormat; try { MediaFormat format = MediaFormat.createVideoFormat(codecType.mimeType(), width, height); format.setInteger(MediaFormat.KEY_BIT_RATE, adjustedBitrate); format.setInteger(MediaFormat.KEY_BITRATE_MODE, BITRATE_MODE_CBR); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); format.setFloat( MediaFormat.KEY_FRAME_RATE, (float) bitrateAdjuster.getAdjustedFramerateFps()); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, keyFrameIntervalSec); if (codecType == VideoCodecMimeType.H264) { String profileLevelId = params.get(VideoCodecInfo.H264_FMTP_PROFILE_LEVEL_ID); if (profileLevelId == null) { profileLevelId = VideoCodecInfo.H264_CONSTRAINED_BASELINE_3_1; } switch (profileLevelId) { case VideoCodecInfo.H264_CONSTRAINED_HIGH_3_1: format.setInteger("profile", AVCProfileHigh); format.setInteger("level", AVCLevel3); break; case VideoCodecInfo.H264_CONSTRAINED_BASELINE_3_1: break; default: Logging.w(TAG, "Unknown profile level id: " + profileLevelId); } } if (codecName.equals("c2.google.av1.encoder")) { // Enable RTC mode in AV1 HW encoder. format.setInteger("vendor.google-av1enc.encoding-preset.int32.value", 1); } 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); if (useSurfaceMode) { textureEglBase = EglBase.createEgl14(sharedContext, EglBase.CONFIG_RECORDABLE); textureInputSurface = codec.createInputSurface(); textureEglBase.createSurface(textureInputSurface); textureEglBase.makeCurrent(); } updateInputFormat(codec.getInputFormat()); codec.start(); } catch (IllegalStateException e) { Logging.e(TAG, "initEncodeInternal failed", e); release(); return VideoCodecStatus.FALLBACK_SOFTWARE; } running = true; outputThreadChecker.detachThread(); outputThread = createOutputThread(); outputThread.start(); return VideoCodecStatus.OK; } @Override public VideoCodecStatus release() { encodeThreadChecker.checkIsOnValidThread(); final VideoCodecStatus returnValue; if (outputThread == null) { returnValue = VideoCodecStatus.OK; } else { // The outputThread actually stops and releases the codec once running is false. running = false; if (!ThreadUtils.joinUninterruptibly(outputThread, MEDIA_CODEC_RELEASE_TIMEOUT_MS)) { Logging.e(TAG, "Media encoder release timeout"); returnValue = VideoCodecStatus.TIMEOUT; } else if (shutdownException != null) { // Log the exception and turn it into an error. Logging.e(TAG, "Media encoder release exception", shutdownException); returnValue = VideoCodecStatus.ERROR; } else { returnValue = VideoCodecStatus.OK; } } textureDrawer.release(); videoFrameDrawer.release(); if (textureEglBase != null) { textureEglBase.release(); textureEglBase = null; } if (textureInputSurface != null) { textureInputSurface.release(); textureInputSurface = null; } outputBuilders.clear(); codec = null; outputThread = null; // Allow changing thread after release. encodeThreadChecker.detachThread(); return returnValue; } @Override public VideoCodecStatus encode(VideoFrame videoFrame, EncodeInfo encodeInfo) { encodeThreadChecker.checkIsOnValidThread(); if (codec == null) { return VideoCodecStatus.UNINITIALIZED; } final boolean isTextureBuffer = videoFrame.getBuffer() instanceof VideoFrame.TextureBuffer; // If input resolution changed, restart the codec with the new resolution. final int frameWidth = videoFrame.getBuffer().getWidth(); final int frameHeight = videoFrame.getBuffer().getHeight(); final boolean shouldUseSurfaceMode = canUseSurface() && isTextureBuffer; if (frameWidth != width || frameHeight != height || shouldUseSurfaceMode != useSurfaceMode) { VideoCodecStatus status = resetCodec(frameWidth, frameHeight, shouldUseSurfaceMode); if (status != VideoCodecStatus.OK) { return status; } } if (outputBuilders.size() > MAX_ENCODER_Q_SIZE) { // Too many frames in the encoder. Drop this frame. Logging.e(TAG, "Dropped frame, encoder queue full"); return VideoCodecStatus.NO_OUTPUT; // See webrtc bug 2887. } boolean requestedKeyFrame = false; for (EncodedImage.FrameType frameType : encodeInfo.frameTypes) { if (frameType == EncodedImage.FrameType.VideoFrameKey) { requestedKeyFrame = true; } } if (requestedKeyFrame || shouldForceKeyFrame(videoFrame.getTimestampNs())) { requestKeyFrame(videoFrame.getTimestampNs()); } EncodedImage.Builder builder = EncodedImage.builder() .setCaptureTimeNs(videoFrame.getTimestampNs()) .setEncodedWidth(videoFrame.getBuffer().getWidth()) .setEncodedHeight(videoFrame.getBuffer().getHeight()) .setRotation(videoFrame.getRotation()); outputBuilders.offer(builder); long presentationTimestampUs = nextPresentationTimestampUs; // Round frame duration down to avoid bitrate overshoot. long frameDurationUs = (long) (TimeUnit.SECONDS.toMicros(1) / bitrateAdjuster.getAdjustedFramerateFps()); nextPresentationTimestampUs += frameDurationUs; final VideoCodecStatus returnValue; if (useSurfaceMode) { returnValue = encodeTextureBuffer(videoFrame, presentationTimestampUs); } else { returnValue = encodeByteBuffer(videoFrame, presentationTimestampUs); } // Check if the queue was successful. if (returnValue != VideoCodecStatus.OK) { // Keep the output builders in sync with buffers in the codec. outputBuilders.pollLast(); } return returnValue; } private VideoCodecStatus encodeTextureBuffer( VideoFrame videoFrame, long presentationTimestampUs) { encodeThreadChecker.checkIsOnValidThread(); try { // TODO(perkj): glClear() shouldn't be necessary since every pixel is covered anyway, // but it's a workaround for bug webrtc:5147. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // It is not necessary to release this frame because it doesn't own the buffer. VideoFrame derotatedFrame = new VideoFrame(videoFrame.getBuffer(), 0 /* rotation */, videoFrame.getTimestampNs()); videoFrameDrawer.drawFrame(derotatedFrame, textureDrawer, null /* additionalRenderMatrix */); textureEglBase.swapBuffers(TimeUnit.MICROSECONDS.toNanos(presentationTimestampUs)); } catch (RuntimeException e) { Logging.e(TAG, "encodeTexture failed", e); return VideoCodecStatus.ERROR; } return VideoCodecStatus.OK; } private VideoCodecStatus encodeByteBuffer(VideoFrame videoFrame, long presentationTimestampUs) { encodeThreadChecker.checkIsOnValidThread(); // No timeout. Don't block for an input buffer, drop frames if the encoder falls behind. int index; try { index = codec.dequeueInputBuffer(0 /* timeout */); } catch (IllegalStateException e) { Logging.e(TAG, "dequeueInputBuffer failed", e); return VideoCodecStatus.ERROR; } if (index == -1) { // Encoder is falling behind. No input buffers available. Drop the frame. Logging.d(TAG, "Dropped frame, no input buffers available"); return VideoCodecStatus.NO_OUTPUT; // See webrtc bug 2887. } ByteBuffer buffer; try { buffer = codec.getInputBuffer(index); } catch (IllegalStateException e) { Logging.e(TAG, "getInputBuffer with index=" + index + " failed", e); return VideoCodecStatus.ERROR; } if (buffer.capacity() < frameSizeBytes) { Logging.e(TAG, "Input buffer size: " + buffer.capacity() + " is smaller than frame size: " + frameSizeBytes); return VideoCodecStatus.ERROR; } fillInputBuffer(buffer, videoFrame.getBuffer()); try { codec.queueInputBuffer( index, 0 /* offset */, frameSizeBytes, presentationTimestampUs, 0 /* flags */); } catch (IllegalStateException e) { Logging.e(TAG, "queueInputBuffer failed", e); // IllegalStateException thrown when the codec is in the wrong state. return VideoCodecStatus.ERROR; } return VideoCodecStatus.OK; } @Override public VideoCodecStatus setRateAllocation(BitrateAllocation bitrateAllocation, int framerate) { encodeThreadChecker.checkIsOnValidThread(); if (framerate > MAX_VIDEO_FRAMERATE) { framerate = MAX_VIDEO_FRAMERATE; } bitrateAdjuster.setTargets(bitrateAllocation.getSum(), framerate); return VideoCodecStatus.OK; } @Override public VideoCodecStatus setRates(RateControlParameters rcParameters) { encodeThreadChecker.checkIsOnValidThread(); bitrateAdjuster.setTargets(rcParameters.bitrate.getSum(), rcParameters.framerateFps); return VideoCodecStatus.OK; } @Override public ScalingSettings getScalingSettings() { if (automaticResizeOn) { if (codecType == VideoCodecMimeType.VP8) { final int kLowVp8QpThreshold = 29; final int kHighVp8QpThreshold = 95; return new ScalingSettings(kLowVp8QpThreshold, kHighVp8QpThreshold); } else if (codecType == VideoCodecMimeType.H264) { final int kLowH264QpThreshold = 24; final int kHighH264QpThreshold = 37; return new ScalingSettings(kLowH264QpThreshold, kHighH264QpThreshold); } } return ScalingSettings.OFF; } @Override public String getImplementationName() { return codecName; } @Override public EncoderInfo getEncoderInfo() { // Since our MediaCodec is guaranteed to encode 16-pixel-aligned frames only, we set alignment // value to be 16. Additionally, this encoder produces a single stream. So it should not require // alignment for all layers. return new EncoderInfo( /* requestedResolutionAlignment= */ REQUIRED_RESOLUTION_ALIGNMENT, /* applyAlignmentToAllSimulcastLayers= */ false); } private VideoCodecStatus resetCodec(int newWidth, int newHeight, boolean newUseSurfaceMode) { encodeThreadChecker.checkIsOnValidThread(); VideoCodecStatus status = release(); if (status != VideoCodecStatus.OK) { return status; } width = newWidth; height = newHeight; useSurfaceMode = newUseSurfaceMode; return initEncodeInternal(); } private boolean shouldForceKeyFrame(long presentationTimestampNs) { encodeThreadChecker.checkIsOnValidThread(); return forcedKeyFrameNs > 0 && presentationTimestampNs > lastKeyFrameNs + forcedKeyFrameNs; } private void requestKeyFrame(long presentationTimestampNs) { encodeThreadChecker.checkIsOnValidThread(); // Ideally MediaCodec would honor BUFFER_FLAG_SYNC_FRAME so we could // indicate this in queueInputBuffer() below and guarantee _this_ frame // be encoded as a key frame, but sadly that flag is ignored. Instead, // we request a key frame "soon". try { Bundle b = new Bundle(); b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); codec.setParameters(b); } catch (IllegalStateException e) { Logging.e(TAG, "requestKeyFrame failed", e); return; } lastKeyFrameNs = presentationTimestampNs; } private Thread createOutputThread() { return new Thread() { @Override public void run() { while (running) { deliverEncodedImage(); } releaseCodecOnOutputThread(); } }; } // Visible for testing. protected void deliverEncodedImage() { outputThreadChecker.checkIsOnValidThread(); try { MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); int index = codec.dequeueOutputBuffer(info, DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US); if (index < 0) { if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { outputBuffersBusyCount.waitForZero(); } return; } 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); 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); } 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"); } // Extract QP before releasing output buffer. Integer qp = null; if (isEncodingStatisticsEnabled) { MediaFormat format = codec.getOutputFormat(index); if (format != null && format.containsKey(MediaFormat.KEY_VIDEO_QP_AVERAGE)) { qp = format.getInteger(MediaFormat.KEY_VIDEO_QP_AVERAGE); } } 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); builder.setFrameType(frameType); 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); } } private void releaseCodecOnOutputThread() { outputThreadChecker.checkIsOnValidThread(); Logging.d(TAG, "Releasing MediaCodec on output thread"); outputBuffersBusyCount.waitForZero(); try { codec.stop(); } catch (Exception e) { Logging.e(TAG, "Media encoder stop failed", e); } try { codec.release(); } catch (Exception e) { Logging.e(TAG, "Media encoder release failed", e); // Propagate exceptions caught during release back to the main thread. shutdownException = e; } configBuffer = null; Logging.d(TAG, "Release on output thread done"); } private VideoCodecStatus updateBitrate() { outputThreadChecker.checkIsOnValidThread(); adjustedBitrate = bitrateAdjuster.getAdjustedBitrateBps(); try { Bundle params = new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, adjustedBitrate); codec.setParameters(params); return VideoCodecStatus.OK; } catch (IllegalStateException e) { Logging.e(TAG, "updateBitrate failed", e); return VideoCodecStatus.ERROR; } } private boolean canUseSurface() { return sharedContext != null && surfaceColorFormat != null; } /** Fetches stride and slice height from input media format */ private void updateInputFormat(MediaFormat format) { stride = width; sliceHeight = height; if (format != null) { if (format.containsKey(MediaFormat.KEY_STRIDE)) { stride = format.getInteger(MediaFormat.KEY_STRIDE); stride = Math.max(stride, width); } if (format.containsKey(MediaFormat.KEY_SLICE_HEIGHT)) { sliceHeight = format.getInteger(MediaFormat.KEY_SLICE_HEIGHT); sliceHeight = Math.max(sliceHeight, height); } } isSemiPlanar = isSemiPlanar(yuvColorFormat); if (isSemiPlanar) { int chromaHeight = (height + 1) / 2; frameSizeBytes = sliceHeight * stride + chromaHeight * stride; } else { int chromaStride = (stride + 1) / 2; int chromaSliceHeight = (sliceHeight + 1) / 2; frameSizeBytes = sliceHeight * stride + chromaSliceHeight * chromaStride * 2; } Logging.d(TAG, "updateInputFormat format: " + format + " stride: " + stride + " sliceHeight: " + sliceHeight + " isSemiPlanar: " + isSemiPlanar + " frameSizeBytes: " + frameSizeBytes); } 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 frame) { VideoFrame.I420Buffer i420 = frame.toI420(); if (isSemiPlanar) { YuvHelper.I420ToNV12(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(), i420.getDataV(), i420.getStrideV(), buffer, i420.getWidth(), i420.getHeight(), stride, sliceHeight); } else { YuvHelper.I420Copy(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(), i420.getDataV(), i420.getStrideV(), buffer, i420.getWidth(), i420.getHeight(), stride, sliceHeight); } i420.release(); } protected boolean isSemiPlanar(int colorFormat) { switch (colorFormat) { case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: return false; case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: case MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar: case MediaCodecUtils.COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m: return true; default: throw new IllegalArgumentException("Unsupported colorFormat: " + colorFormat); } } }