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