MediaCodecBridge.java revision f2477e01787aa58f445919b809d89e252beef54f
1// Copyright (c) 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.media; 6 7import android.media.AudioFormat; 8import android.media.AudioManager; 9import android.media.AudioTrack; 10import android.media.MediaCodec; 11import android.media.MediaCodecInfo; 12import android.media.MediaCodecList; 13import android.media.MediaCrypto; 14import android.media.MediaFormat; 15import android.os.Build; 16import android.util.Log; 17import android.view.Surface; 18 19import java.io.IOException; 20import java.nio.ByteBuffer; 21import java.util.ArrayList; 22import java.util.HashMap; 23import java.util.Map; 24 25import org.chromium.base.CalledByNative; 26import org.chromium.base.JNINamespace; 27 28/** 29 * A wrapper of the MediaCodec class to facilitate exception capturing and 30 * audio rendering. 31 */ 32@JNINamespace("media") 33class MediaCodecBridge { 34 private static final String TAG = "MediaCodecBridge"; 35 36 // Error code for MediaCodecBridge. Keep this value in sync with 37 // MediaCodecStatus in media_codec_bridge.h. 38 private static final int MEDIA_CODEC_OK = 0; 39 private static final int MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER = 1; 40 private static final int MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER = 2; 41 private static final int MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED = 3; 42 private static final int MEDIA_CODEC_OUTPUT_FORMAT_CHANGED = 4; 43 private static final int MEDIA_CODEC_INPUT_END_OF_STREAM = 5; 44 private static final int MEDIA_CODEC_OUTPUT_END_OF_STREAM = 6; 45 private static final int MEDIA_CODEC_NO_KEY = 7; 46 private static final int MEDIA_CODEC_STOPPED = 8; 47 private static final int MEDIA_CODEC_ERROR = 9; 48 49 // After a flush(), dequeueOutputBuffer() can often produce empty presentation timestamps 50 // for several frames. As a result, the player may find that the time does not increase 51 // after decoding a frame. To detect this, we check whether the presentation timestamp from 52 // dequeueOutputBuffer() is larger than input_timestamp - MAX_PRESENTATION_TIMESTAMP_SHIFT_US 53 // after a flush. And we set the presentation timestamp from dequeueOutputBuffer() to be 54 // non-decreasing for the remaining frames. 55 private static final long MAX_PRESENTATION_TIMESTAMP_SHIFT_US = 100000; 56 57 private ByteBuffer[] mInputBuffers; 58 private ByteBuffer[] mOutputBuffers; 59 60 private MediaCodec mMediaCodec; 61 private AudioTrack mAudioTrack; 62 private boolean mFlushed; 63 private long mLastPresentationTimeUs; 64 65 private static class DequeueInputResult { 66 private final int mStatus; 67 private final int mIndex; 68 69 private DequeueInputResult(int status, int index) { 70 mStatus = status; 71 mIndex = index; 72 } 73 74 @CalledByNative("DequeueInputResult") 75 private int status() { return mStatus; } 76 77 @CalledByNative("DequeueInputResult") 78 private int index() { return mIndex; } 79 } 80 81 /** 82 * This class represents supported android codec information. 83 */ 84 private static class CodecInfo { 85 private final String mCodecType; // e.g. "video/x-vnd.on2.vp8". 86 private final String mCodecName; // e.g. "OMX.google.vp8.decoder". 87 88 private CodecInfo(String codecType, String codecName) { 89 mCodecType = codecType; 90 mCodecName = codecName; 91 } 92 93 @CalledByNative("CodecInfo") 94 private String codecType() { return mCodecType; } 95 96 @CalledByNative("CodecInfo") 97 private String codecName() { return mCodecName; } 98 } 99 100 private static class DequeueOutputResult { 101 private final int mStatus; 102 private final int mIndex; 103 private final int mFlags; 104 private final int mOffset; 105 private final long mPresentationTimeMicroseconds; 106 private final int mNumBytes; 107 108 private DequeueOutputResult(int status, int index, int flags, int offset, 109 long presentationTimeMicroseconds, int numBytes) { 110 mStatus = status; 111 mIndex = index; 112 mFlags = flags; 113 mOffset = offset; 114 mPresentationTimeMicroseconds = presentationTimeMicroseconds; 115 mNumBytes = numBytes; 116 } 117 118 @CalledByNative("DequeueOutputResult") 119 private int status() { return mStatus; } 120 121 @CalledByNative("DequeueOutputResult") 122 private int index() { return mIndex; } 123 124 @CalledByNative("DequeueOutputResult") 125 private int flags() { return mFlags; } 126 127 @CalledByNative("DequeueOutputResult") 128 private int offset() { return mOffset; } 129 130 @CalledByNative("DequeueOutputResult") 131 private long presentationTimeMicroseconds() { return mPresentationTimeMicroseconds; } 132 133 @CalledByNative("DequeueOutputResult") 134 private int numBytes() { return mNumBytes; } 135 } 136 137 /** 138 * Get a list of supported android codec mimes. 139 */ 140 @CalledByNative 141 private static CodecInfo[] getCodecsInfo() { 142 Map<String, CodecInfo> CodecInfoMap = new HashMap<String, CodecInfo>(); 143 int count = MediaCodecList.getCodecCount(); 144 for (int i = 0; i < count; ++i) { 145 MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); 146 if (info.isEncoder()) { 147 continue; 148 } 149 150 String codecString = info.getName(); 151 String[] supportedTypes = info.getSupportedTypes(); 152 for (int j = 0; j < supportedTypes.length; ++j) { 153 if (!CodecInfoMap.containsKey(supportedTypes[j])) { 154 CodecInfoMap.put(supportedTypes[j], new CodecInfo( 155 supportedTypes[j], codecString)); 156 } 157 } 158 } 159 return CodecInfoMap.values().toArray( 160 new CodecInfo[CodecInfoMap.size()]); 161 } 162 163 private static String getSecureDecoderNameForMime(String mime) { 164 int count = MediaCodecList.getCodecCount(); 165 for (int i = 0; i < count; ++i) { 166 MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); 167 if (info.isEncoder()) { 168 continue; 169 } 170 171 String[] supportedTypes = info.getSupportedTypes(); 172 for (int j = 0; j < supportedTypes.length; ++j) { 173 if (supportedTypes[j].equalsIgnoreCase(mime)) { 174 return info.getName() + ".secure"; 175 } 176 } 177 } 178 179 return null; 180 } 181 182 private MediaCodecBridge(MediaCodec mediaCodec) { 183 assert(mediaCodec != null); 184 mMediaCodec = mediaCodec; 185 mLastPresentationTimeUs = 0; 186 mFlushed = true; 187 } 188 189 @CalledByNative 190 private static MediaCodecBridge create(String mime, boolean isSecure) { 191 // Creation of ".secure" codecs sometimes crash instead of throwing exceptions 192 // on pre-JBMR2 devices. 193 if (isSecure && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { 194 return null; 195 } 196 MediaCodec mediaCodec = null; 197 try { 198 // |isSecure| only applies to video decoders. 199 if (mime.startsWith("video") && isSecure) { 200 mediaCodec = MediaCodec.createByCodecName(getSecureDecoderNameForMime(mime)); 201 } else { 202 mediaCodec = MediaCodec.createDecoderByType(mime); 203 } 204 } catch (Exception e) { 205 Log.e(TAG, "Failed to create MediaCodec: " + mime + ", isSecure: " 206 + isSecure + ", " + e.toString()); 207 } 208 209 if (mediaCodec == null) { 210 return null; 211 } 212 213 return new MediaCodecBridge(mediaCodec); 214 } 215 216 @CalledByNative 217 private void release() { 218 mMediaCodec.release(); 219 if (mAudioTrack != null) { 220 mAudioTrack.release(); 221 } 222 } 223 224 @CalledByNative 225 private boolean start() { 226 try { 227 mMediaCodec.start(); 228 mInputBuffers = mMediaCodec.getInputBuffers(); 229 } catch (IllegalStateException e) { 230 Log.e(TAG, "Cannot start the media codec " + e.toString()); 231 return false; 232 } 233 return true; 234 } 235 236 @CalledByNative 237 private DequeueInputResult dequeueInputBuffer(long timeoutUs) { 238 int status = MEDIA_CODEC_ERROR; 239 int index = -1; 240 try { 241 int index_or_status = mMediaCodec.dequeueInputBuffer(timeoutUs); 242 if (index_or_status >= 0) { // index! 243 status = MEDIA_CODEC_OK; 244 index = index_or_status; 245 } else if (index_or_status == MediaCodec.INFO_TRY_AGAIN_LATER) { 246 Log.e(TAG, "dequeueInputBuffer: MediaCodec.INFO_TRY_AGAIN_LATER"); 247 status = MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER; 248 } else { 249 assert(false); 250 } 251 } catch(Exception e) { 252 Log.e(TAG, "Failed to dequeue input buffer: " + e.toString()); 253 } 254 return new DequeueInputResult(status, index); 255 } 256 257 @CalledByNative 258 private int flush() { 259 try { 260 mFlushed = true; 261 if (mAudioTrack != null) { 262 mAudioTrack.flush(); 263 } 264 mMediaCodec.flush(); 265 } catch(IllegalStateException e) { 266 Log.e(TAG, "Failed to flush MediaCodec " + e.toString()); 267 return MEDIA_CODEC_ERROR; 268 } 269 return MEDIA_CODEC_OK; 270 } 271 272 @CalledByNative 273 private void stop() { 274 mMediaCodec.stop(); 275 if (mAudioTrack != null) { 276 mAudioTrack.pause(); 277 } 278 } 279 280 @CalledByNative 281 private int getOutputHeight() { 282 return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_HEIGHT); 283 } 284 285 @CalledByNative 286 private int getOutputWidth() { 287 return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_WIDTH); 288 } 289 290 @CalledByNative 291 private ByteBuffer getInputBuffer(int index) { 292 return mInputBuffers[index]; 293 } 294 295 @CalledByNative 296 private ByteBuffer getOutputBuffer(int index) { 297 return mOutputBuffers[index]; 298 } 299 300 @CalledByNative 301 private int queueInputBuffer( 302 int index, int offset, int size, long presentationTimeUs, int flags) { 303 resetLastPresentationTimeIfNeeded(presentationTimeUs); 304 try { 305 mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); 306 } catch(Exception e) { 307 Log.e(TAG, "Failed to queue input buffer: " + e.toString()); 308 return MEDIA_CODEC_ERROR; 309 } 310 return MEDIA_CODEC_OK; 311 } 312 313 @CalledByNative 314 private int queueSecureInputBuffer( 315 int index, int offset, byte[] iv, byte[] keyId, int[] numBytesOfClearData, 316 int[] numBytesOfEncryptedData, int numSubSamples, long presentationTimeUs) { 317 resetLastPresentationTimeIfNeeded(presentationTimeUs); 318 try { 319 MediaCodec.CryptoInfo cryptoInfo = new MediaCodec.CryptoInfo(); 320 cryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, 321 keyId, iv, MediaCodec.CRYPTO_MODE_AES_CTR); 322 mMediaCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, 0); 323 } catch (MediaCodec.CryptoException e) { 324 Log.e(TAG, "Failed to queue secure input buffer: " + e.toString()); 325 if (e.getErrorCode() == MediaCodec.CryptoException.ERROR_NO_KEY) { 326 Log.e(TAG, "MediaCodec.CryptoException.ERROR_NO_KEY"); 327 return MEDIA_CODEC_NO_KEY; 328 } 329 Log.e(TAG, "MediaCodec.CryptoException with error code " + e.getErrorCode()); 330 return MEDIA_CODEC_ERROR; 331 } catch(IllegalStateException e) { 332 Log.e(TAG, "Failed to queue secure input buffer: " + e.toString()); 333 return MEDIA_CODEC_ERROR; 334 } 335 return MEDIA_CODEC_OK; 336 } 337 338 @CalledByNative 339 private void releaseOutputBuffer(int index, boolean render) { 340 mMediaCodec.releaseOutputBuffer(index, render); 341 } 342 343 @CalledByNative 344 private boolean getOutputBuffers() { 345 try { 346 mOutputBuffers = mMediaCodec.getOutputBuffers(); 347 } catch (IllegalStateException e) { 348 Log.e(TAG, "Cannot get output buffers " + e.toString()); 349 return false; 350 } 351 return true; 352 } 353 354 @CalledByNative 355 private DequeueOutputResult dequeueOutputBuffer(long timeoutUs) { 356 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); 357 int status = MEDIA_CODEC_ERROR; 358 int index = -1; 359 try { 360 int index_or_status = mMediaCodec.dequeueOutputBuffer(info, timeoutUs); 361 if (info.presentationTimeUs < mLastPresentationTimeUs) { 362 // TODO(qinmin): return a special code through DequeueOutputResult 363 // to notify the native code the the frame has a wrong presentation 364 // timestamp and should be skipped. 365 info.presentationTimeUs = mLastPresentationTimeUs; 366 } 367 mLastPresentationTimeUs = info.presentationTimeUs; 368 369 if (index_or_status >= 0) { // index! 370 status = MEDIA_CODEC_OK; 371 index = index_or_status; 372 } else if (index_or_status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 373 status = MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED; 374 } else if (index_or_status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 375 status = MEDIA_CODEC_OUTPUT_FORMAT_CHANGED; 376 } else if (index_or_status == MediaCodec.INFO_TRY_AGAIN_LATER) { 377 status = MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER; 378 } else { 379 assert(false); 380 } 381 } catch (IllegalStateException e) { 382 Log.e(TAG, "Failed to dequeue output buffer: " + e.toString()); 383 } 384 385 return new DequeueOutputResult( 386 status, index, info.flags, info.offset, info.presentationTimeUs, info.size); 387 } 388 389 @CalledByNative 390 private boolean configureVideo(MediaFormat format, Surface surface, MediaCrypto crypto, 391 int flags) { 392 try { 393 mMediaCodec.configure(format, surface, crypto, flags); 394 return true; 395 } catch (IllegalStateException e) { 396 Log.e(TAG, "Cannot configure the video codec " + e.toString()); 397 } 398 return false; 399 } 400 401 @CalledByNative 402 private static MediaFormat createAudioFormat(String mime, int SampleRate, int ChannelCount) { 403 return MediaFormat.createAudioFormat(mime, SampleRate, ChannelCount); 404 } 405 406 @CalledByNative 407 private static MediaFormat createVideoFormat(String mime, int width, int height) { 408 return MediaFormat.createVideoFormat(mime, width, height); 409 } 410 411 @CalledByNative 412 private static void setCodecSpecificData(MediaFormat format, int index, byte[] bytes) { 413 String name = null; 414 if (index == 0) { 415 name = "csd-0"; 416 } else if (index == 1) { 417 name = "csd-1"; 418 } 419 if (name != null) { 420 format.setByteBuffer(name, ByteBuffer.wrap(bytes)); 421 } 422 } 423 424 @CalledByNative 425 private static void setFrameHasADTSHeader(MediaFormat format) { 426 format.setInteger(MediaFormat.KEY_IS_ADTS, 1); 427 } 428 429 @CalledByNative 430 private boolean configureAudio(MediaFormat format, MediaCrypto crypto, int flags, 431 boolean playAudio) { 432 try { 433 mMediaCodec.configure(format, null, crypto, flags); 434 if (playAudio) { 435 int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); 436 int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); 437 int channelConfig = (channelCount == 1) ? AudioFormat.CHANNEL_OUT_MONO : 438 AudioFormat.CHANNEL_OUT_STEREO; 439 // Using 16bit PCM for output. Keep this value in sync with 440 // kBytesPerAudioOutputSample in media_codec_bridge.cc. 441 int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, 442 AudioFormat.ENCODING_PCM_16BIT); 443 mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, 444 AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM); 445 } 446 return true; 447 } catch (IllegalStateException e) { 448 Log.e(TAG, "Cannot configure the audio codec " + e.toString()); 449 } 450 return false; 451 } 452 453 @CalledByNative 454 private void playOutputBuffer(byte[] buf) { 455 if (mAudioTrack != null) { 456 if (AudioTrack.PLAYSTATE_PLAYING != mAudioTrack.getPlayState()) { 457 mAudioTrack.play(); 458 } 459 int size = mAudioTrack.write(buf, 0, buf.length); 460 if (buf.length != size) { 461 Log.i(TAG, "Failed to send all data to audio output, expected size: " + 462 buf.length + ", actual size: " + size); 463 } 464 } 465 } 466 467 @CalledByNative 468 private void setVolume(double volume) { 469 if (mAudioTrack != null) { 470 mAudioTrack.setStereoVolume((float) volume, (float) volume); 471 } 472 } 473 474 private void resetLastPresentationTimeIfNeeded(long presentationTimeUs) { 475 if (mFlushed) { 476 mLastPresentationTimeUs = 477 Math.max(presentationTimeUs - MAX_PRESENTATION_TIMESTAMP_SHIFT_US, 0); 478 mFlushed = false; 479 } 480 } 481} 482