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 | |
5 | package org.chromium.media; |
6 | |
7 | import android.media.AudioFormat; |
8 | import android.media.AudioManager; |
9 | import android.media.AudioTrack; |
10 | import android.media.MediaCodec; |
11 | import android.media.MediaCrypto; |
12 | import android.media.MediaFormat; |
13 | import android.view.Surface; |
14 | import android.util.Log; |
15 | |
16 | import java.io.IOException; |
17 | import java.nio.ByteBuffer; |
18 | |
19 | import org.chromium.base.CalledByNative; |
20 | import org.chromium.base.JNINamespace; |
21 | |
22 | /** |
23 | * A wrapper of the MediaCodec class to facilitate exception capturing and |
24 | * audio rendering. |
25 | */ |
26 | @JNINamespace("media") |
27 | class MediaCodecBridge { |
28 | |
29 | private static final String TAG = "MediaCodecBridge"; |
30 | |
31 | // Error code for MediaCodecBridge. Keep this value in sync with |
32 | // INFO_MEDIA_CODEC_ERROR in media_codec_bridge.h. |
33 | private static final int MEDIA_CODEC_ERROR = -1000; |
34 | |
35 | // After a flush(), dequeueOutputBuffer() can often produce empty presentation timestamps |
36 | // for several frames. As a result, the player may find that the time does not increase |
37 | // after decoding a frame. To detect this, we check whether the presentation timestamp from |
38 | // dequeueOutputBuffer() is larger than input_timestamp - MAX_PRESENTATION_TIMESTAMP_SHIFT_US |
39 | // after a flush. And we set the presentation timestamp from dequeueOutputBuffer() to be |
40 | // non-decreasing for the remaining frames. |
41 | private static final long MAX_PRESENTATION_TIMESTAMP_SHIFT_US = 100000; |
42 | |
43 | private ByteBuffer[] mInputBuffers; |
44 | private ByteBuffer[] mOutputBuffers; |
45 | |
46 | private MediaCodec mMediaCodec; |
47 | private AudioTrack mAudioTrack; |
48 | private boolean mFlushed; |
49 | private long mLastPresentationTimeUs; |
50 | |
51 | private static class DequeueOutputResult { |
52 | private final int mIndex; |
53 | private final int mFlags; |
54 | private final int mOffset; |
55 | private final long mPresentationTimeMicroseconds; |
56 | private final int mNumBytes; |
57 | |
58 | private DequeueOutputResult(int index, int flags, int offset, |
59 | long presentationTimeMicroseconds, int numBytes) { |
60 | mIndex = index; |
61 | mFlags = flags; |
62 | mOffset = offset; |
63 | mPresentationTimeMicroseconds = presentationTimeMicroseconds; |
64 | mNumBytes = numBytes; |
65 | } |
66 | |
67 | @CalledByNative("DequeueOutputResult") |
68 | private int index() { return mIndex; } |
69 | |
70 | @CalledByNative("DequeueOutputResult") |
71 | private int flags() { return mFlags; } |
72 | |
73 | @CalledByNative("DequeueOutputResult") |
74 | private int offset() { return mOffset; } |
75 | |
76 | @CalledByNative("DequeueOutputResult") |
77 | private long presentationTimeMicroseconds() { return mPresentationTimeMicroseconds; } |
78 | |
79 | @CalledByNative("DequeueOutputResult") |
80 | private int numBytes() { return mNumBytes; } |
81 | } |
82 | |
83 | private MediaCodecBridge(String mime) throws IOException { |
84 | mMediaCodec = MediaCodec.createDecoderByType(mime); |
85 | mLastPresentationTimeUs = 0; |
86 | mFlushed = true; |
87 | } |
88 | |
89 | @CalledByNative |
90 | private static MediaCodecBridge create(String mime) { |
91 | MediaCodecBridge mediaCodecBridge = null; |
92 | try { |
93 | mediaCodecBridge = new MediaCodecBridge(mime); |
94 | } catch (IOException e) { |
95 | Log.e(TAG, "Failed to create MediaCodecBridge " + e.toString()); |
96 | } |
97 | |
98 | return mediaCodecBridge; |
99 | } |
100 | |
101 | @CalledByNative |
102 | private void release() { |
103 | mMediaCodec.release(); |
104 | if (mAudioTrack != null) { |
105 | mAudioTrack.release(); |
106 | } |
107 | } |
108 | |
109 | @CalledByNative |
110 | private void start() { |
111 | mMediaCodec.start(); |
112 | mInputBuffers = mMediaCodec.getInputBuffers(); |
113 | } |
114 | |
115 | @CalledByNative |
116 | private int dequeueInputBuffer(long timeoutUs) { |
117 | try { |
118 | return mMediaCodec.dequeueInputBuffer(timeoutUs); |
119 | } catch(Exception e) { |
120 | Log.e(TAG, "Cannot dequeue Input buffer " + e.toString()); |
121 | } |
122 | return MEDIA_CODEC_ERROR; |
123 | } |
124 | |
125 | @CalledByNative |
126 | private void flush() { |
127 | mMediaCodec.flush(); |
128 | mFlushed = true; |
129 | if (mAudioTrack != null) { |
130 | mAudioTrack.flush(); |
131 | } |
132 | } |
133 | |
134 | @CalledByNative |
135 | private void stop() { |
136 | mMediaCodec.stop(); |
137 | if (mAudioTrack != null) { |
138 | mAudioTrack.pause(); |
139 | } |
140 | } |
141 | |
142 | @CalledByNative |
143 | private int getOutputHeight() { |
144 | return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_HEIGHT); |
145 | } |
146 | |
147 | @CalledByNative |
148 | private int getOutputWidth() { |
149 | return mMediaCodec.getOutputFormat().getInteger(MediaFormat.KEY_WIDTH); |
150 | } |
151 | |
152 | @CalledByNative |
153 | private ByteBuffer getInputBuffer(int index) { |
154 | return mInputBuffers[index]; |
155 | } |
156 | |
157 | @CalledByNative |
158 | private ByteBuffer getOutputBuffer(int index) { |
159 | return mOutputBuffers[index]; |
160 | } |
161 | |
162 | @CalledByNative |
163 | private void queueInputBuffer( |
164 | int index, int offset, int size, long presentationTimeUs, int flags) { |
165 | resetLastPresentationTimeIfNeeded(presentationTimeUs); |
166 | try { |
167 | mMediaCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags); |
168 | } catch(IllegalStateException e) { |
169 | Log.e(TAG, "Failed to queue input buffer " + e.toString()); |
170 | } |
171 | } |
172 | |
173 | @CalledByNative |
174 | private void queueSecureInputBuffer( |
175 | int index, int offset, byte[] iv, byte[] keyId, int[] numBytesOfClearData, |
176 | int[] numBytesOfEncryptedData, int numSubSamples, long presentationTimeUs) { |
177 | resetLastPresentationTimeIfNeeded(presentationTimeUs); |
178 | try { |
179 | MediaCodec.CryptoInfo cryptoInfo = new MediaCodec.CryptoInfo(); |
180 | cryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, |
181 | keyId, iv, MediaCodec.CRYPTO_MODE_AES_CTR); |
182 | mMediaCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, 0); |
183 | } catch(IllegalStateException e) { |
184 | Log.e(TAG, "Failed to queue secure input buffer " + e.toString()); |
185 | } |
186 | } |
187 | |
188 | @CalledByNative |
189 | private void releaseOutputBuffer(int index, boolean render) { |
190 | mMediaCodec.releaseOutputBuffer(index, render); |
191 | } |
192 | |
193 | @CalledByNative |
194 | private void getOutputBuffers() { |
195 | mOutputBuffers = mMediaCodec.getOutputBuffers(); |
196 | } |
197 | |
198 | @CalledByNative |
199 | private DequeueOutputResult dequeueOutputBuffer(long timeoutUs) { |
200 | MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); |
201 | int index = MEDIA_CODEC_ERROR; |
202 | try { |
203 | index = mMediaCodec.dequeueOutputBuffer(info, timeoutUs); |
204 | if (info.presentationTimeUs < mLastPresentationTimeUs) { |
205 | // TODO(qinmin): return a special code through DequeueOutputResult |
206 | // to notify the native code the the frame has a wrong presentation |
207 | // timestamp and should be skipped. |
208 | info.presentationTimeUs = mLastPresentationTimeUs; |
209 | } |
210 | mLastPresentationTimeUs = info.presentationTimeUs; |
211 | } catch (IllegalStateException e) { |
212 | Log.e(TAG, "Cannot dequeue output buffer " + e.toString()); |
213 | } |
214 | return new DequeueOutputResult( |
215 | index, info.flags, info.offset, info.presentationTimeUs, info.size); |
216 | } |
217 | |
218 | @CalledByNative |
219 | private boolean configureVideo(MediaFormat format, Surface surface, MediaCrypto crypto, |
220 | int flags) { |
221 | try { |
222 | mMediaCodec.configure(format, surface, crypto, flags); |
223 | return true; |
224 | } catch (IllegalStateException e) { |
225 | Log.e(TAG, "Cannot configure the video codec " + e.toString()); |
226 | } |
227 | return false; |
228 | } |
229 | |
230 | @CalledByNative |
231 | private static MediaFormat createAudioFormat(String mime, int SampleRate, int ChannelCount) { |
232 | return MediaFormat.createAudioFormat(mime, SampleRate, ChannelCount); |
233 | } |
234 | |
235 | @CalledByNative |
236 | private static MediaFormat createVideoFormat(String mime, int width, int height) { |
237 | return MediaFormat.createVideoFormat(mime, width, height); |
238 | } |
239 | |
240 | @CalledByNative |
241 | private static void setCodecSpecificData(MediaFormat format, int index, ByteBuffer bytes) { |
242 | String name = null; |
243 | if (index == 0) { |
244 | name = "csd-0"; |
245 | } else if (index == 1) { |
246 | name = "csd-1"; |
247 | } |
248 | if (name != null) { |
249 | format.setByteBuffer(name, bytes); |
250 | } |
251 | } |
252 | |
253 | @CalledByNative |
254 | private static void setFrameHasADTSHeader(MediaFormat format) { |
255 | format.setInteger(MediaFormat.KEY_IS_ADTS, 1); |
256 | } |
257 | |
258 | @CalledByNative |
259 | private boolean configureAudio(MediaFormat format, MediaCrypto crypto, int flags, |
260 | boolean playAudio) { |
261 | try { |
262 | mMediaCodec.configure(format, null, crypto, flags); |
263 | if (playAudio) { |
264 | int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); |
265 | int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); |
266 | int channelConfig = (channelCount == 1) ? AudioFormat.CHANNEL_OUT_MONO : |
267 | AudioFormat.CHANNEL_OUT_STEREO; |
268 | // Using 16bit PCM for output. Keep this value in sync with |
269 | // kBytesPerAudioOutputSample in media_codec_bridge.cc. |
270 | int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, |
271 | AudioFormat.ENCODING_PCM_16BIT); |
272 | mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, |
273 | AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM); |
274 | } |
275 | return true; |
276 | } catch (IllegalStateException e) { |
277 | Log.e(TAG, "Cannot configure the audio codec " + e.toString()); |
278 | } |
279 | return false; |
280 | } |
281 | |
282 | @CalledByNative |
283 | private void playOutputBuffer(byte[] buf) { |
284 | if (mAudioTrack != null) { |
285 | if (AudioTrack.PLAYSTATE_PLAYING != mAudioTrack.getPlayState()) { |
286 | mAudioTrack.play(); |
287 | } |
288 | int size = mAudioTrack.write(buf, 0, buf.length); |
289 | if (buf.length != size) { |
290 | Log.i(TAG, "Failed to send all data to audio output, expected size: " + |
291 | buf.length + ", actual size: " + size); |
292 | } |
293 | } |
294 | } |
295 | |
296 | @CalledByNative |
297 | private void setVolume(double volume) { |
298 | if (mAudioTrack != null) { |
299 | mAudioTrack.setStereoVolume((float) volume, (float) volume); |
300 | } |
301 | } |
302 | |
303 | private void resetLastPresentationTimeIfNeeded(long presentationTimeUs) { |
304 | if (mFlushed) { |
305 | mLastPresentationTimeUs = |
306 | Math.max(presentationTimeUs - MAX_PRESENTATION_TIMESTAMP_SHIFT_US, 0); |
307 | mFlushed = false; |
308 | } |
309 | } |
310 | } |