MediaCodecBridge.java revision 0529e5d033099cbfc42635f6f6183833b09dff6e
1// Copyright 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.os.Bundle;
17import android.util.Log;
18import android.view.Surface;
19
20import org.chromium.base.CalledByNative;
21import org.chromium.base.JNINamespace;
22
23import java.nio.ByteBuffer;
24import java.util.ArrayList;
25import java.util.HashMap;
26import java.util.Map;
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    // Codec direction.  Keep this in sync with media_codec_bridge.h.
50    private static final int MEDIA_CODEC_DECODER = 0;
51    private static final int MEDIA_CODEC_ENCODER = 1;
52
53    // After a flush(), dequeueOutputBuffer() can often produce empty presentation timestamps
54    // for several frames. As a result, the player may find that the time does not increase
55    // after decoding a frame. To detect this, we check whether the presentation timestamp from
56    // dequeueOutputBuffer() is larger than input_timestamp - MAX_PRESENTATION_TIMESTAMP_SHIFT_US
57    // after a flush. And we set the presentation timestamp from dequeueOutputBuffer() to be
58    // non-decreasing for the remaining frames.
59    private static final long MAX_PRESENTATION_TIMESTAMP_SHIFT_US = 100000;
60
61    private ByteBuffer[] mInputBuffers;
62    private ByteBuffer[] mOutputBuffers;
63
64    private MediaCodec mMediaCodec;
65    private AudioTrack mAudioTrack;
66    private boolean mFlushed;
67    private long mLastPresentationTimeUs;
68
69    private static class DequeueInputResult {
70        private final int mStatus;
71        private final int mIndex;
72
73        private DequeueInputResult(int status, int index) {
74            mStatus = status;
75            mIndex = index;
76        }
77
78        @CalledByNative("DequeueInputResult")
79        private int status() { return mStatus; }
80
81        @CalledByNative("DequeueInputResult")
82        private int index() { return mIndex; }
83    }
84
85    /**
86     * This class represents supported android codec information.
87     */
88    private static class CodecInfo {
89        private final String mCodecType;  // e.g. "video/x-vnd.on2.vp8".
90        private final String mCodecName;  // e.g. "OMX.google.vp8.decoder".
91        private final int mDirection;
92
93        private CodecInfo(String codecType, String codecName,
94                          int direction) {
95            mCodecType = codecType;
96            mCodecName = codecName;
97            mDirection = direction;
98        }
99
100        @CalledByNative("CodecInfo")
101        private String codecType() { return mCodecType; }
102
103        @CalledByNative("CodecInfo")
104        private String codecName() { return mCodecName; }
105
106        @CalledByNative("CodecInfo")
107        private int direction() { return mDirection; }
108    }
109
110    private static class DequeueOutputResult {
111        private final int mStatus;
112        private final int mIndex;
113        private final int mFlags;
114        private final int mOffset;
115        private final long mPresentationTimeMicroseconds;
116        private final int mNumBytes;
117
118        private DequeueOutputResult(int status, int index, int flags, int offset,
119                long presentationTimeMicroseconds, int numBytes) {
120            mStatus = status;
121            mIndex = index;
122            mFlags = flags;
123            mOffset = offset;
124            mPresentationTimeMicroseconds = presentationTimeMicroseconds;
125            mNumBytes = numBytes;
126        }
127
128        @CalledByNative("DequeueOutputResult")
129        private int status() { return mStatus; }
130
131        @CalledByNative("DequeueOutputResult")
132        private int index() { return mIndex; }
133
134        @CalledByNative("DequeueOutputResult")
135        private int flags() { return mFlags; }
136
137        @CalledByNative("DequeueOutputResult")
138        private int offset() { return mOffset; }
139
140        @CalledByNative("DequeueOutputResult")
141        private long presentationTimeMicroseconds() { return mPresentationTimeMicroseconds; }
142
143        @CalledByNative("DequeueOutputResult")
144        private int numBytes() { return mNumBytes; }
145    }
146
147    /**
148     * Get a list of supported android codec mimes.
149     */
150    @CalledByNative
151    private static CodecInfo[] getCodecsInfo() {
152        // Return the first (highest-priority) codec for each MIME type.
153        Map<String, CodecInfo> encoderInfoMap = new HashMap<String, CodecInfo>();
154        Map<String, CodecInfo> decoderInfoMap = new HashMap<String, CodecInfo>();
155        int count = MediaCodecList.getCodecCount();
156        for (int i = 0; i < count; ++i) {
157            MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
158            int direction =
159                info.isEncoder() ? MEDIA_CODEC_ENCODER : MEDIA_CODEC_DECODER;
160            String codecString = info.getName();
161            String[] supportedTypes = info.getSupportedTypes();
162            for (int j = 0; j < supportedTypes.length; ++j) {
163                Map<String, CodecInfo> map = info.isEncoder() ? encoderInfoMap : decoderInfoMap;
164                if (!map.containsKey(supportedTypes[j])) {
165                    map.put(supportedTypes[j], new CodecInfo(
166                        supportedTypes[j], codecString, direction));
167                }
168            }
169        }
170        ArrayList<CodecInfo> codecInfos = new ArrayList<CodecInfo>(
171            decoderInfoMap.size() + encoderInfoMap.size());
172        codecInfos.addAll(encoderInfoMap.values());
173        codecInfos.addAll(decoderInfoMap.values());
174        return codecInfos.toArray(new CodecInfo[codecInfos.size()]);
175    }
176
177    private static String getSecureDecoderNameForMime(String mime) {
178        int count = MediaCodecList.getCodecCount();
179        for (int i = 0; i < count; ++i) {
180            MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
181            if (info.isEncoder()) {
182                continue;
183            }
184
185            String[] supportedTypes = info.getSupportedTypes();
186            for (int j = 0; j < supportedTypes.length; ++j) {
187                if (supportedTypes[j].equalsIgnoreCase(mime)) {
188                    return info.getName() + ".secure";
189                }
190            }
191        }
192
193        return null;
194    }
195
196    private MediaCodecBridge(MediaCodec mediaCodec) {
197        assert mediaCodec != null;
198        mMediaCodec = mediaCodec;
199        mLastPresentationTimeUs = 0;
200        mFlushed = true;
201    }
202
203    @CalledByNative
204    private static MediaCodecBridge create(String mime, boolean isSecure, int direction) {
205        // Creation of ".secure" codecs sometimes crash instead of throwing exceptions
206        // on pre-JBMR2 devices.
207        if (isSecure && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
208            return null;
209        }
210        MediaCodec mediaCodec = null;
211        try {
212            // |isSecure| only applies to video decoders.
213            if (mime.startsWith("video") && isSecure && direction == MEDIA_CODEC_DECODER) {
214                mediaCodec = MediaCodec.createByCodecName(getSecureDecoderNameForMime(mime));
215            } else {
216                if (direction == MEDIA_CODEC_ENCODER) {
217                    mediaCodec = MediaCodec.createEncoderByType(mime);
218                } else {
219                    mediaCodec = MediaCodec.createDecoderByType(mime);
220                }
221            }
222        } catch (Exception e) {
223            Log.e(TAG, "Failed to create MediaCodec: " +  mime + ", isSecure: "
224                    + isSecure + ", direction: " + direction, e);
225        }
226
227        if (mediaCodec == null) {
228            return null;
229        }
230
231        return new MediaCodecBridge(mediaCodec);
232    }
233
234    @CalledByNative
235    private void release() {
236        try {
237            mMediaCodec.release();
238        } catch(IllegalStateException e) {
239            // The MediaCodec is stuck in a wrong state, possibly due to losing
240            // the surface.
241            Log.e(TAG, "Cannot release media codec", e);
242        }
243        mMediaCodec = null;
244        if (mAudioTrack != null) {
245            mAudioTrack.release();
246        }
247    }
248
249    @CalledByNative
250    private boolean start() {
251        try {
252            mMediaCodec.start();
253            mInputBuffers = mMediaCodec.getInputBuffers();
254        } catch (IllegalStateException e) {
255            Log.e(TAG, "Cannot start the media codec", e);
256            return false;
257        }
258        return true;
259    }
260
261    @CalledByNative
262    private DequeueInputResult dequeueInputBuffer(long timeoutUs) {
263        int status = MEDIA_CODEC_ERROR;
264        int index = -1;
265        try {
266            int indexOrStatus = mMediaCodec.dequeueInputBuffer(timeoutUs);
267            if (indexOrStatus >= 0) { // index!
268                status = MEDIA_CODEC_OK;
269                index = indexOrStatus;
270            } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
271                Log.e(TAG, "dequeueInputBuffer: MediaCodec.INFO_TRY_AGAIN_LATER");
272                status = MEDIA_CODEC_DEQUEUE_INPUT_AGAIN_LATER;
273            } else {
274                Log.e(TAG, "Unexpected index_or_status: " + indexOrStatus);
275                assert false;
276            }
277        } catch (Exception e) {
278            Log.e(TAG, "Failed to dequeue input buffer", e);
279        }
280        return new DequeueInputResult(status, index);
281    }
282
283    @CalledByNative
284    private int flush() {
285        try {
286            mFlushed = true;
287            if (mAudioTrack != null) {
288                // Need to call pause() here, or otherwise flush() is a no-op.
289                mAudioTrack.pause();
290                mAudioTrack.flush();
291            }
292            mMediaCodec.flush();
293        } catch (IllegalStateException e) {
294            Log.e(TAG, "Failed to flush MediaCodec", e);
295            return MEDIA_CODEC_ERROR;
296        }
297        return MEDIA_CODEC_OK;
298    }
299
300    @CalledByNative
301    private void stop() {
302        mMediaCodec.stop();
303        if (mAudioTrack != null) {
304            mAudioTrack.pause();
305        }
306    }
307
308    @CalledByNative
309    private int getOutputHeight() {
310        return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_HEIGHT);
311    }
312
313    @CalledByNative
314    private int getOutputWidth() {
315        return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_WIDTH);
316    }
317
318    @CalledByNative
319    private ByteBuffer getInputBuffer(int index) {
320        return mInputBuffers[index];
321    }
322
323    @CalledByNative
324    private ByteBuffer getOutputBuffer(int index) {
325        return mOutputBuffers[index];
326    }
327
328    @CalledByNative
329    private int getInputBuffersCount() {
330        return mInputBuffers.length;
331    }
332
333    @CalledByNative
334    private int getOutputBuffersCount() {
335        return mOutputBuffers != null ? mOutputBuffers.length : -1;
336    }
337
338    @CalledByNative
339    private int getOutputBuffersCapacity() {
340        return mOutputBuffers != null ? mOutputBuffers[0].capacity() : -1;
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);
349            return false;
350        }
351        return true;
352    }
353
354    @CalledByNative
355    private int queueInputBuffer(
356            int index, int offset, int size, long presentationTimeUs, int flags) {
357        resetLastPresentationTimeIfNeeded(presentationTimeUs);
358        try {
359            mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
360        } catch (Exception e) {
361            Log.e(TAG, "Failed to queue input buffer", e);
362            return MEDIA_CODEC_ERROR;
363        }
364        return MEDIA_CODEC_OK;
365    }
366
367    @CalledByNative
368    private void setVideoBitrate(int bps) {
369        Bundle b = new Bundle();
370        b.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bps);
371        mMediaCodec.setParameters(b);
372    }
373
374    @CalledByNative
375    private void requestKeyFrameSoon() {
376        Bundle b = new Bundle();
377        b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
378        mMediaCodec.setParameters(b);
379    }
380
381    @CalledByNative
382    private int queueSecureInputBuffer(
383            int index, int offset, byte[] iv, byte[] keyId, int[] numBytesOfClearData,
384            int[] numBytesOfEncryptedData, int numSubSamples, long presentationTimeUs) {
385        resetLastPresentationTimeIfNeeded(presentationTimeUs);
386        try {
387            MediaCodec.CryptoInfo cryptoInfo = new MediaCodec.CryptoInfo();
388            cryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData,
389                    keyId, iv, MediaCodec.CRYPTO_MODE_AES_CTR);
390            mMediaCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, 0);
391        } catch (MediaCodec.CryptoException e) {
392            Log.e(TAG, "Failed to queue secure input buffer", e);
393            if (e.getErrorCode() == MediaCodec.CryptoException.ERROR_NO_KEY) {
394                Log.e(TAG, "MediaCodec.CryptoException.ERROR_NO_KEY");
395                return MEDIA_CODEC_NO_KEY;
396            }
397            Log.e(TAG, "MediaCodec.CryptoException with error code " + e.getErrorCode());
398            return MEDIA_CODEC_ERROR;
399        } catch (IllegalStateException e) {
400            Log.e(TAG, "Failed to queue secure input buffer", e);
401            return MEDIA_CODEC_ERROR;
402        }
403        return MEDIA_CODEC_OK;
404    }
405
406    @CalledByNative
407    private void releaseOutputBuffer(int index, boolean render) {
408        try {
409            mMediaCodec.releaseOutputBuffer(index, render);
410        } catch(IllegalStateException e) {
411            // TODO(qinmin): May need to report the error to the caller. crbug.com/356498.
412            Log.e(TAG, "Failed to release output buffer", e);
413        }
414    }
415
416    @CalledByNative
417    private DequeueOutputResult dequeueOutputBuffer(long timeoutUs) {
418        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
419        int status = MEDIA_CODEC_ERROR;
420        int index = -1;
421        try {
422            int indexOrStatus = mMediaCodec.dequeueOutputBuffer(info, timeoutUs);
423            if (info.presentationTimeUs < mLastPresentationTimeUs) {
424                // TODO(qinmin): return a special code through DequeueOutputResult
425                // to notify the native code the the frame has a wrong presentation
426                // timestamp and should be skipped.
427                info.presentationTimeUs = mLastPresentationTimeUs;
428            }
429            mLastPresentationTimeUs = info.presentationTimeUs;
430
431            if (indexOrStatus >= 0) { // index!
432                status = MEDIA_CODEC_OK;
433                index = indexOrStatus;
434            } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
435                status = MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED;
436            } else if (indexOrStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
437                status = MEDIA_CODEC_OUTPUT_FORMAT_CHANGED;
438            } else if (indexOrStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
439                status = MEDIA_CODEC_DEQUEUE_OUTPUT_AGAIN_LATER;
440            } else {
441                Log.e(TAG, "Unexpected index_or_status: " + indexOrStatus);
442                assert false;
443            }
444        } catch (IllegalStateException e) {
445            Log.e(TAG, "Failed to dequeue output buffer", e);
446        }
447
448        return new DequeueOutputResult(
449                status, index, info.flags, info.offset, info.presentationTimeUs, info.size);
450    }
451
452    @CalledByNative
453    private boolean configureVideo(MediaFormat format, Surface surface, MediaCrypto crypto,
454            int flags) {
455        try {
456            mMediaCodec.configure(format, surface, crypto, flags);
457            return true;
458        } catch (IllegalStateException e) {
459            Log.e(TAG, "Cannot configure the video codec", e);
460        }
461        return false;
462    }
463
464    @CalledByNative
465    private static MediaFormat createAudioFormat(String mime, int sampleRate, int channelCount) {
466        return MediaFormat.createAudioFormat(mime, sampleRate, channelCount);
467    }
468
469    @CalledByNative
470    private static MediaFormat createVideoDecoderFormat(String mime, int width, int height) {
471        return MediaFormat.createVideoFormat(mime, width, height);
472    }
473
474    @CalledByNative
475    private static MediaFormat createVideoEncoderFormat(String mime, int width, int height,
476            int bitRate, int frameRate, int iFrameInterval, int colorFormat) {
477        MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
478        format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
479        format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
480        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
481        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
482        return format;
483    }
484
485    @CalledByNative
486    private static void setCodecSpecificData(MediaFormat format, int index, byte[] bytes) {
487        String name = null;
488        if (index == 0) {
489            name = "csd-0";
490        } else if (index == 1) {
491            name = "csd-1";
492        }
493        if (name != null) {
494            format.setByteBuffer(name, ByteBuffer.wrap(bytes));
495        }
496    }
497
498    @CalledByNative
499    private static void setFrameHasADTSHeader(MediaFormat format) {
500        format.setInteger(MediaFormat.KEY_IS_ADTS, 1);
501    }
502
503    @CalledByNative
504    private boolean configureAudio(MediaFormat format, MediaCrypto crypto, int flags,
505            boolean playAudio) {
506        try {
507            mMediaCodec.configure(format, null, crypto, flags);
508            if (playAudio) {
509                int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
510                int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
511                int channelConfig = getAudioFormat(channelCount);
512                // Using 16bit PCM for output. Keep this value in sync with
513                // kBytesPerAudioOutputSample in media_codec_bridge.cc.
514                int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig,
515                        AudioFormat.ENCODING_PCM_16BIT);
516                mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig,
517                        AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM);
518                if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) {
519                    mAudioTrack = null;
520                    return false;
521                }
522            }
523            return true;
524        } catch (IllegalStateException e) {
525            Log.e(TAG, "Cannot configure the audio codec", e);
526        }
527        return false;
528    }
529
530    /**
531     *  Play the audio buffer that is passed in.
532     *
533     *  @param buf Audio buffer to be rendered.
534     *  @return The number of frames that have already been consumed by the
535     *  hardware. This number resets to 0 after each flush call.
536     */
537    @CalledByNative
538    private long playOutputBuffer(byte[] buf) {
539        if (mAudioTrack == null) {
540            return 0;
541        }
542
543        if (AudioTrack.PLAYSTATE_PLAYING != mAudioTrack.getPlayState()) {
544            mAudioTrack.play();
545        }
546        int size = mAudioTrack.write(buf, 0, buf.length);
547        if (buf.length != size) {
548            Log.i(TAG, "Failed to send all data to audio output, expected size: " +
549                    buf.length + ", actual size: " + size);
550        }
551        // TODO(qinmin): Returning the head position allows us to estimate
552        // the current presentation time in native code. However, it is
553        // better to use AudioTrack.getCurrentTimestamp() to get the last
554        // known time when a frame is played. However, we will need to
555        // convert the java nano time to C++ timestamp.
556        // If the stream runs too long, getPlaybackHeadPosition() could
557        // overflow. AudioTimestampHelper in MediaSourcePlayer has the same
558        // issue. See http://crbug.com/358801.
559        return mAudioTrack.getPlaybackHeadPosition();
560    }
561
562    @CalledByNative
563    private void setVolume(double volume) {
564        if (mAudioTrack != null) {
565            mAudioTrack.setStereoVolume((float) volume, (float) volume);
566        }
567    }
568
569    private void resetLastPresentationTimeIfNeeded(long presentationTimeUs) {
570        if (mFlushed) {
571            mLastPresentationTimeUs =
572                    Math.max(presentationTimeUs - MAX_PRESENTATION_TIMESTAMP_SHIFT_US, 0);
573            mFlushed = false;
574        }
575    }
576
577    private int getAudioFormat(int channelCount) {
578        switch (channelCount) {
579            case 1:
580                return AudioFormat.CHANNEL_OUT_MONO;
581            case 2:
582                return AudioFormat.CHANNEL_OUT_STEREO;
583            case 4:
584                return AudioFormat.CHANNEL_OUT_QUAD;
585            case 6:
586                return AudioFormat.CHANNEL_OUT_5POINT1;
587            case 8:
588                return AudioFormat.CHANNEL_OUT_7POINT1;
589            default:
590                return AudioFormat.CHANNEL_OUT_DEFAULT;
591        }
592    }
593}
594