1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv.tuner.exoplayer.audio;
18
19import android.media.MediaCodec;
20import android.util.Log;
21
22import com.google.android.exoplayer.CodecCounters;
23import com.google.android.exoplayer.DecoderInfo;
24import com.google.android.exoplayer.ExoPlaybackException;
25import com.google.android.exoplayer.MediaCodecSelector;
26import com.google.android.exoplayer.MediaCodecUtil;
27import com.google.android.exoplayer.MediaFormat;
28import com.google.android.exoplayer.SampleHolder;
29
30import java.nio.ByteBuffer;
31import java.util.ArrayList;
32
33/** A decoder to use MediaCodec for decoding audio stream. */
34public class MediaCodecAudioDecoder extends AudioDecoder {
35    private static final String TAG = "MediaCodecAudioDecoder";
36
37    public static final int INDEX_INVALID = -1;
38
39    private final CodecCounters mCodecCounters;
40    private final MediaCodecSelector mSelector;
41
42    private MediaCodec mCodec;
43    private MediaCodec.BufferInfo mOutputBufferInfo;
44    private ByteBuffer mMediaCodecOutputBuffer;
45    private ArrayList<Long> mDecodeOnlyPresentationTimestamps;
46    private boolean mWaitingForFirstSyncFrame;
47    private boolean mIsNewIndex;
48    private int mInputIndex;
49    private int mOutputIndex;
50
51    /** Creates a MediaCodec based audio decoder. */
52    public MediaCodecAudioDecoder(MediaCodecSelector selector) {
53        mSelector = selector;
54        mOutputBufferInfo = new MediaCodec.BufferInfo();
55        mCodecCounters = new CodecCounters();
56        mDecodeOnlyPresentationTimestamps = new ArrayList<>();
57    }
58
59    /** Returns {@code true} if there is decoder for {@code mimeType}. */
60    public static boolean supportMimeType(MediaCodecSelector selector, String mimeType) {
61        if (selector == null) {
62            return false;
63        }
64        return getDecoderInfo(selector, mimeType) != null;
65    }
66
67    private static DecoderInfo getDecoderInfo(MediaCodecSelector selector, String mimeType) {
68        try {
69            return selector.getDecoderInfo(mimeType, false);
70        } catch (MediaCodecUtil.DecoderQueryException e) {
71            Log.e(TAG, "Select decoder error:" + e);
72            return null;
73        }
74    }
75
76    private boolean shouldInitCodec(MediaFormat format) {
77        return format != null && mCodec == null;
78    }
79
80    @Override
81    public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException {
82        if (!shouldInitCodec(format)) {
83            return;
84        }
85
86        String mimeType = format.mimeType;
87        DecoderInfo decoderInfo = getDecoderInfo(mSelector, mimeType);
88        if (decoderInfo == null) {
89            Log.i(TAG, "There is not decoder found for " + mimeType);
90            return;
91        }
92
93        String codecName = decoderInfo.name;
94        try {
95            mCodec = MediaCodec.createByCodecName(codecName);
96            mCodec.configure(format.getFrameworkMediaFormatV16(), null, null, 0);
97            mCodec.start();
98        } catch (Exception e) {
99            Log.e(TAG, "Failed when configure or start codec:" + e);
100            throw new ExoPlaybackException(e);
101        }
102        mInputIndex = INDEX_INVALID;
103        mOutputIndex = INDEX_INVALID;
104        mWaitingForFirstSyncFrame = true;
105        mCodecCounters.codecInitCount++;
106    }
107
108    @Override
109    public void resetDecoderState(String mimeType) {
110        if (mCodec == null) {
111            return;
112        }
113        mInputIndex = INDEX_INVALID;
114        mOutputIndex = INDEX_INVALID;
115        mDecodeOnlyPresentationTimestamps.clear();
116        mCodec.flush();
117        mWaitingForFirstSyncFrame = true;
118    }
119
120    @Override
121    public void release() {
122        if (mCodec != null) {
123            mDecodeOnlyPresentationTimestamps.clear();
124            mInputIndex = INDEX_INVALID;
125            mOutputIndex = INDEX_INVALID;
126            mCodecCounters.codecReleaseCount++;
127            try {
128                mCodec.stop();
129            } finally {
130                try {
131                    mCodec.release();
132                } finally {
133                    mCodec = null;
134                }
135            }
136        }
137    }
138
139    /** Returns the index of input buffer which is ready for using. */
140    public int getInputIndex() {
141        return mInputIndex;
142    }
143
144    @Override
145    public ByteBuffer getInputBuffer() {
146        if (mInputIndex < 0) {
147            mInputIndex = mCodec.dequeueInputBuffer(0);
148            if (mInputIndex < 0) {
149                return null;
150            }
151            return mCodec.getInputBuffer(mInputIndex);
152        }
153        return mCodec.getInputBuffer(mInputIndex);
154    }
155
156    @Override
157    public void decode(SampleHolder sampleHolder) {
158        if (mWaitingForFirstSyncFrame) {
159            if (!sampleHolder.isSyncFrame()) {
160                sampleHolder.clearData();
161                return;
162            }
163            mWaitingForFirstSyncFrame = false;
164        }
165        long presentationTimeUs = sampleHolder.timeUs;
166        if (sampleHolder.isDecodeOnly()) {
167            mDecodeOnlyPresentationTimestamps.add(presentationTimeUs);
168        }
169        mCodec.queueInputBuffer(mInputIndex, 0, sampleHolder.data.limit(), presentationTimeUs, 0);
170        mInputIndex = INDEX_INVALID;
171        mCodecCounters.inputBufferCount++;
172    }
173
174    private int getDecodeOnlyIndex(long presentationTimeUs) {
175        final int size = mDecodeOnlyPresentationTimestamps.size();
176        for (int i = 0; i < size; i++) {
177            if (mDecodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) {
178                return i;
179            }
180        }
181        return INDEX_INVALID;
182    }
183
184    /** Returns the index of output buffer which is ready for using. */
185    public int getOutputIndex() {
186        if (mOutputIndex < 0) {
187            mOutputIndex = mCodec.dequeueOutputBuffer(mOutputBufferInfo, 0);
188            mIsNewIndex = true;
189        } else {
190            mIsNewIndex = false;
191        }
192        return mOutputIndex;
193    }
194
195    @Override
196    public android.media.MediaFormat getOutputFormat() {
197        return mCodec.getOutputFormat();
198    }
199
200    /** Returns {@code true} if the output is only for decoding but not for rendering. */
201    public boolean maybeDecodeOnlyIndex() {
202        int decodeOnlyIndex = getDecodeOnlyIndex(mOutputBufferInfo.presentationTimeUs);
203        if (decodeOnlyIndex != INDEX_INVALID) {
204            mCodec.releaseOutputBuffer(mOutputIndex, false);
205            mCodecCounters.skippedOutputBufferCount++;
206            mDecodeOnlyPresentationTimestamps.remove(decodeOnlyIndex);
207            mOutputIndex = INDEX_INVALID;
208            return true;
209        }
210        return false;
211    }
212
213    @Override
214    public ByteBuffer getDecodedSample() {
215        if (maybeDecodeOnlyIndex() || mOutputIndex < 0) {
216            return null;
217        }
218        if (mIsNewIndex) {
219            mMediaCodecOutputBuffer = mCodec.getOutputBuffer(mOutputIndex);
220        }
221        return mMediaCodecOutputBuffer;
222    }
223
224    @Override
225    public long getDecodedTimeUs() {
226        return mOutputBufferInfo.presentationTimeUs;
227    }
228
229    /** Releases the output buffer after rendering. */
230    public void releaseOutputBuffer() {
231        mCodecCounters.renderedOutputBufferCount++;
232        mCodec.releaseOutputBuffer(mOutputIndex, false);
233        mOutputIndex = INDEX_INVALID;
234    }
235}
236