1 /* 2 * libjingle 3 * Copyright 2013, Google Inc. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, 9 * this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * 3. The name of the author may not be used to endorse or promote products 14 * derived from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 28 29 package org.webrtc; 30 31 import android.media.MediaCodec; 32 import android.media.MediaCodecInfo.CodecCapabilities; 33 import android.media.MediaCodecInfo; 34 import android.media.MediaCodecList; 35 import android.media.MediaFormat; 36 import android.os.Build; 37 import android.os.Bundle; 38 import android.util.Log; 39 40 import java.nio.ByteBuffer; 41 42 // Java-side of peerconnection_jni.cc:MediaCodecVideoEncoder. 43 // This class is an implementation detail of the Java PeerConnection API. 44 // MediaCodec is thread-hostile so this class must be operated on a single 45 // thread. 46 class MediaCodecVideoEncoder { 47 // This class is constructed, operated, and destroyed by its C++ incarnation, 48 // so the class and its methods have non-public visibility. The API this 49 // class exposes aims to mimic the webrtc::VideoEncoder API as closely as 50 // possibly to minimize the amount of translation work necessary. 51 52 private static final String TAG = "MediaCodecVideoEncoder"; 53 54 private static final int DEQUEUE_TIMEOUT = 0; // Non-blocking, no wait. 55 private Thread mediaCodecThread; 56 private MediaCodec mediaCodec; 57 private ByteBuffer[] outputBuffers; 58 private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8"; 59 // List of supported HW VP8 codecs. 60 private static final String[] supportedHwCodecPrefixes = 61 {"OMX.qcom.", "OMX.Nvidia." }; 62 // Bitrate mode 63 private static final int VIDEO_ControlRateConstant = 2; 64 // NV12 color format supported by QCOM codec, but not declared in MediaCodec - 65 // see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h 66 private static final int 67 COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04; 68 // Allowable color formats supported by codec - in order of preference. 69 private static final int[] supportedColorList = { 70 CodecCapabilities.COLOR_FormatYUV420Planar, 71 CodecCapabilities.COLOR_FormatYUV420SemiPlanar, 72 CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar, 73 COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m 74 }; 75 private int colorFormat; 76 MediaCodecVideoEncoder()77 private MediaCodecVideoEncoder() {} 78 79 // Helper struct for findVp8HwEncoder() below. 80 private static class EncoderProperties { EncoderProperties(String codecName, int colorFormat)81 public EncoderProperties(String codecName, int colorFormat) { 82 this.codecName = codecName; 83 this.colorFormat = colorFormat; 84 } 85 public final String codecName; // OpenMax component name for VP8 codec. 86 public final int colorFormat; // Color format supported by codec. 87 } 88 findVp8HwEncoder()89 private static EncoderProperties findVp8HwEncoder() { 90 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) 91 return null; // MediaCodec.setParameters is missing. 92 93 for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) { 94 MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); 95 if (!info.isEncoder()) { 96 continue; 97 } 98 String name = null; 99 for (String mimeType : info.getSupportedTypes()) { 100 if (mimeType.equals(VP8_MIME_TYPE)) { 101 name = info.getName(); 102 break; 103 } 104 } 105 if (name == null) { 106 continue; // No VP8 support in this codec; try the next one. 107 } 108 Log.d(TAG, "Found candidate encoder " + name); 109 110 // Check if this is supported HW encoder. 111 boolean supportedCodec = false; 112 for (String hwCodecPrefix : supportedHwCodecPrefixes) { 113 if (name.startsWith(hwCodecPrefix)) { 114 supportedCodec = true; 115 break; 116 } 117 } 118 if (!supportedCodec) { 119 continue; 120 } 121 122 CodecCapabilities capabilities = 123 info.getCapabilitiesForType(VP8_MIME_TYPE); 124 for (int colorFormat : capabilities.colorFormats) { 125 Log.d(TAG, " Color: 0x" + Integer.toHexString(colorFormat)); 126 } 127 128 // Check if codec supports either yuv420 or nv12. 129 for (int supportedColorFormat : supportedColorList) { 130 for (int codecColorFormat : capabilities.colorFormats) { 131 if (codecColorFormat == supportedColorFormat) { 132 // Found supported HW VP8 encoder. 133 Log.d(TAG, "Found target encoder " + name + 134 ". Color: 0x" + Integer.toHexString(codecColorFormat)); 135 return new EncoderProperties(name, codecColorFormat); 136 } 137 } 138 } 139 } 140 return null; // No HW VP8 encoder. 141 } 142 isPlatformSupported()143 private static boolean isPlatformSupported() { 144 return findVp8HwEncoder() != null; 145 } 146 checkOnMediaCodecThread()147 private void checkOnMediaCodecThread() { 148 if (mediaCodecThread.getId() != Thread.currentThread().getId()) { 149 throw new RuntimeException( 150 "MediaCodecVideoEncoder previously operated on " + mediaCodecThread + 151 " but is now called on " + Thread.currentThread()); 152 } 153 } 154 155 // Return the array of input buffers, or null on failure. initEncode(int width, int height, int kbps, int fps)156 private ByteBuffer[] initEncode(int width, int height, int kbps, int fps) { 157 Log.d(TAG, "Java initEncode: " + width + " x " + height + 158 ". @ " + kbps + " kbps. Fps: " + fps + 159 ". Color: 0x" + Integer.toHexString(colorFormat)); 160 if (mediaCodecThread != null) { 161 throw new RuntimeException("Forgot to release()?"); 162 } 163 EncoderProperties properties = findVp8HwEncoder(); 164 if (properties == null) { 165 throw new RuntimeException("Can not find HW VP8 encoder"); 166 } 167 mediaCodecThread = Thread.currentThread(); 168 try { 169 MediaFormat format = 170 MediaFormat.createVideoFormat(VP8_MIME_TYPE, width, height); 171 format.setInteger(MediaFormat.KEY_BIT_RATE, 1000 * kbps); 172 format.setInteger("bitrate-mode", VIDEO_ControlRateConstant); 173 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, properties.colorFormat); 174 // Default WebRTC settings 175 format.setInteger(MediaFormat.KEY_FRAME_RATE, fps); 176 format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 100); 177 Log.d(TAG, " Format: " + format); 178 mediaCodec = MediaCodec.createByCodecName(properties.codecName); 179 if (mediaCodec == null) { 180 return null; 181 } 182 mediaCodec.configure( 183 format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 184 mediaCodec.start(); 185 colorFormat = properties.colorFormat; 186 outputBuffers = mediaCodec.getOutputBuffers(); 187 ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); 188 Log.d(TAG, "Input buffers: " + inputBuffers.length + 189 ". Output buffers: " + outputBuffers.length); 190 return inputBuffers; 191 } catch (IllegalStateException e) { 192 Log.e(TAG, "initEncode failed", e); 193 return null; 194 } 195 } 196 encode( boolean isKeyframe, int inputBuffer, int size, long presentationTimestampUs)197 private boolean encode( 198 boolean isKeyframe, int inputBuffer, int size, 199 long presentationTimestampUs) { 200 checkOnMediaCodecThread(); 201 try { 202 if (isKeyframe) { 203 // Ideally MediaCodec would honor BUFFER_FLAG_SYNC_FRAME so we could 204 // indicate this in queueInputBuffer() below and guarantee _this_ frame 205 // be encoded as a key frame, but sadly that flag is ignored. Instead, 206 // we request a key frame "soon". 207 Log.d(TAG, "Sync frame request"); 208 Bundle b = new Bundle(); 209 b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); 210 mediaCodec.setParameters(b); 211 } 212 mediaCodec.queueInputBuffer( 213 inputBuffer, 0, size, presentationTimestampUs, 0); 214 return true; 215 } 216 catch (IllegalStateException e) { 217 Log.e(TAG, "encode failed", e); 218 return false; 219 } 220 } 221 release()222 private void release() { 223 Log.d(TAG, "Java releaseEncoder"); 224 checkOnMediaCodecThread(); 225 try { 226 mediaCodec.stop(); 227 mediaCodec.release(); 228 } catch (IllegalStateException e) { 229 Log.e(TAG, "release failed", e); 230 } 231 mediaCodec = null; 232 mediaCodecThread = null; 233 } 234 setRates(int kbps, int frameRateIgnored)235 private boolean setRates(int kbps, int frameRateIgnored) { 236 // frameRate argument is ignored - HW encoder is supposed to use 237 // video frame timestamps for bit allocation. 238 checkOnMediaCodecThread(); 239 Log.v(TAG, "setRates: " + kbps + " kbps. Fps: " + frameRateIgnored); 240 try { 241 Bundle params = new Bundle(); 242 params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, 1000 * kbps); 243 mediaCodec.setParameters(params); 244 return true; 245 } catch (IllegalStateException e) { 246 Log.e(TAG, "setRates failed", e); 247 return false; 248 } 249 } 250 251 // Dequeue an input buffer and return its index, -1 if no input buffer is 252 // available, or -2 if the codec is no longer operative. dequeueInputBuffer()253 private int dequeueInputBuffer() { 254 checkOnMediaCodecThread(); 255 try { 256 return mediaCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT); 257 } catch (IllegalStateException e) { 258 Log.e(TAG, "dequeueIntputBuffer failed", e); 259 return -2; 260 } 261 } 262 263 // Helper struct for dequeueOutputBuffer() below. 264 private static class OutputBufferInfo { OutputBufferInfo( int index, ByteBuffer buffer, boolean isKeyFrame, long presentationTimestampUs)265 public OutputBufferInfo( 266 int index, ByteBuffer buffer, boolean isKeyFrame, 267 long presentationTimestampUs) { 268 this.index = index; 269 this.buffer = buffer; 270 this.isKeyFrame = isKeyFrame; 271 this.presentationTimestampUs = presentationTimestampUs; 272 } 273 274 private final int index; 275 private final ByteBuffer buffer; 276 private final boolean isKeyFrame; 277 private final long presentationTimestampUs; 278 } 279 280 // Dequeue and return an output buffer, or null if no output is ready. Return 281 // a fake OutputBufferInfo with index -1 if the codec is no longer operable. dequeueOutputBuffer()282 private OutputBufferInfo dequeueOutputBuffer() { 283 checkOnMediaCodecThread(); 284 try { 285 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 286 int result = mediaCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT); 287 if (result >= 0) { 288 // MediaCodec doesn't care about Buffer position/remaining/etc so we can 289 // mess with them to get a slice and avoid having to pass extra 290 // (BufferInfo-related) parameters back to C++. 291 ByteBuffer outputBuffer = outputBuffers[result].duplicate(); 292 outputBuffer.position(info.offset); 293 outputBuffer.limit(info.offset + info.size); 294 boolean isKeyFrame = 295 (info.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; 296 if (isKeyFrame) { 297 Log.d(TAG, "Sync frame generated"); 298 } 299 return new OutputBufferInfo( 300 result, outputBuffer.slice(), isKeyFrame, info.presentationTimeUs); 301 } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 302 outputBuffers = mediaCodec.getOutputBuffers(); 303 return dequeueOutputBuffer(); 304 } else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 305 return dequeueOutputBuffer(); 306 } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) { 307 return null; 308 } 309 throw new RuntimeException("dequeueOutputBuffer: " + result); 310 } catch (IllegalStateException e) { 311 Log.e(TAG, "dequeueOutputBuffer failed", e); 312 return new OutputBufferInfo(-1, null, false, -1); 313 } 314 } 315 316 // Release a dequeued output buffer back to the codec for re-use. Return 317 // false if the codec is no longer operable. releaseOutputBuffer(int index)318 private boolean releaseOutputBuffer(int index) { 319 checkOnMediaCodecThread(); 320 try { 321 mediaCodec.releaseOutputBuffer(index, false); 322 return true; 323 } catch (IllegalStateException e) { 324 Log.e(TAG, "releaseOutputBuffer failed", e); 325 return false; 326 } 327 } 328 } 329