1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 package org.mozilla.gecko.media;
6 
7 import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
8 
9 import android.media.MediaCodec;
10 import android.media.MediaCodecInfo.CodecCapabilities;
11 import android.media.MediaCrypto;
12 import android.media.MediaFormat;
13 import android.os.Build;
14 import android.os.Handler;
15 import android.os.HandlerThread;
16 import android.os.Looper;
17 import android.os.Message;
18 import android.os.Bundle;
19 import android.util.Log;
20 import android.view.Surface;
21 
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 
25 // Implement async API using MediaCodec sync mode (API v16).
26 // This class uses internal worker thread/handler (mBufferPoller) to poll
27 // input and output buffer and notifies the client through callbacks.
28 final class JellyBeanAsyncCodec implements AsyncCodec {
29     private static final String LOGTAG = "GeckoAsyncCodecAPIv16";
30     private static final boolean DEBUG = false;
31 
32     private static final int ERROR_CODEC = -10000;
33 
34     private abstract class CancelableHandler extends Handler {
35         private static final int MSG_CANCELLATION = 0x434E434C; // 'CNCL'
36 
CancelableHandler(final Looper looper)37         protected CancelableHandler(final Looper looper) {
38             super(looper);
39         }
40 
cancel()41         protected void cancel() {
42             removeCallbacksAndMessages(null);
43             sendEmptyMessage(MSG_CANCELLATION);
44             // Wait until handleMessageLocked() is done.
45             synchronized (this) { }
46         }
47 
isCanceled()48         protected boolean isCanceled() {
49             return hasMessages(MSG_CANCELLATION);
50         }
51 
52         // Subclass should implement this and return true if it handles msg.
53         // Warning: Never, ever call super.handleMessage() in this method!
handleMessageLocked(Message msg)54         protected abstract boolean handleMessageLocked(Message msg);
55 
handleMessage(final Message msg)56         public final void handleMessage(final Message msg) {
57             // Block cancel() during handleMessageLocked().
58             synchronized (this) {
59                 if (isCanceled() || handleMessageLocked(msg)) {
60                     return;
61                 }
62             }
63 
64             switch (msg.what) {
65                 case MSG_CANCELLATION:
66                     // Just a marker. Nothing to do here.
67                     if (DEBUG) {
68                         Log.d(LOGTAG, "handler " + this + " done cancellation, codec=" + JellyBeanAsyncCodec.this);
69                     }
70                     break;
71                 default:
72                     super.handleMessage(msg);
73                     break;
74             }
75         }
76     }
77 
78     // A handler to invoke AsyncCodec.Callbacks methods.
79     private final class CallbackSender extends CancelableHandler {
80         private static final int MSG_INPUT_BUFFER_AVAILABLE = 1;
81         private static final int MSG_OUTPUT_BUFFER_AVAILABLE = 2;
82         private static final int MSG_OUTPUT_FORMAT_CHANGE = 3;
83         private static final int MSG_ERROR = 4;
84         private Callbacks mCallbacks;
85 
CallbackSender(final Looper looper, final Callbacks callbacks)86         private CallbackSender(final Looper looper, final Callbacks callbacks) {
87             super(looper);
88             mCallbacks = callbacks;
89         }
90 
notifyInputBuffer(final int index)91         public void notifyInputBuffer(final int index) {
92             if (isCanceled()) {
93                 return;
94             }
95 
96             final Message msg = obtainMessage(MSG_INPUT_BUFFER_AVAILABLE);
97             msg.arg1 = index;
98             processMessage(msg);
99         }
100 
processMessage(final Message msg)101         private void processMessage(final Message msg) {
102             if (Looper.myLooper() == getLooper()) {
103                 handleMessage(msg);
104             } else {
105                 sendMessage(msg);
106             }
107         }
108 
notifyOutputBuffer(final int index, final MediaCodec.BufferInfo info)109         public void notifyOutputBuffer(final int index, final MediaCodec.BufferInfo info) {
110             if (isCanceled()) {
111                 return;
112             }
113 
114             final Message msg = obtainMessage(MSG_OUTPUT_BUFFER_AVAILABLE, info);
115             msg.arg1 = index;
116             processMessage(msg);
117         }
118 
notifyOutputFormat(final MediaFormat format)119         public void notifyOutputFormat(final MediaFormat format) {
120             if (isCanceled()) {
121                 return;
122             }
123             processMessage(obtainMessage(MSG_OUTPUT_FORMAT_CHANGE, format));
124         }
125 
notifyError(final int result)126         public void notifyError(final int result) {
127             Log.e(LOGTAG, "codec error:" + result);
128             processMessage(obtainMessage(MSG_ERROR, result, 0));
129         }
130 
handleMessageLocked(final Message msg)131         protected boolean handleMessageLocked(final Message msg) {
132             switch (msg.what) {
133                 case MSG_INPUT_BUFFER_AVAILABLE: // arg1: buffer index.
134                     mCallbacks.onInputBufferAvailable(JellyBeanAsyncCodec.this,
135                                                       msg.arg1);
136                     break;
137                 case MSG_OUTPUT_BUFFER_AVAILABLE: // arg1: buffer index, obj: info.
138                     mCallbacks.onOutputBufferAvailable(JellyBeanAsyncCodec.this,
139                                                        msg.arg1,
140                                                        (MediaCodec.BufferInfo)msg.obj);
141                     break;
142                 case MSG_OUTPUT_FORMAT_CHANGE: // obj: output format.
143                     mCallbacks.onOutputFormatChanged(JellyBeanAsyncCodec.this,
144                                                      (MediaFormat)msg.obj);
145                     break;
146                 case MSG_ERROR: // arg1: error code.
147                     mCallbacks.onError(JellyBeanAsyncCodec.this, msg.arg1);
148                     break;
149                 default:
150                     return false;
151             }
152 
153             return true;
154         }
155     }
156 
157     // Handler to poll input and output buffers using dequeue(Input|Output)Buffer(),
158     // with 10ms time-out. Once triggered and successfully gets a buffer, it
159     // will schedule next polling until EOS or failure. To prevent it from
160     // automatically polling more buffer, use cancel() it inherits from
161     // CancelableHandler.
162     private final class BufferPoller extends CancelableHandler {
163         private static final int MSG_POLL_INPUT_BUFFERS = 1;
164         private static final int MSG_POLL_OUTPUT_BUFFERS = 2;
165 
166         private static final long DEQUEUE_TIMEOUT_US = 10000;
167 
BufferPoller(final Looper looper)168         public BufferPoller(final Looper looper) {
169             super(looper);
170         }
171 
schedulePollingIfNotCanceled(final int what)172         private void schedulePollingIfNotCanceled(final int what) {
173             if (isCanceled()) {
174                 return;
175             }
176 
177             schedulePolling(what);
178         }
179 
schedulePolling(final int what)180         private void schedulePolling(final int what) {
181             if (needsBuffer(what)) {
182                 sendEmptyMessage(what);
183             }
184         }
185 
needsBuffer(final int what)186         private boolean needsBuffer(final int what) {
187             if (mOutputEnded && (what == MSG_POLL_OUTPUT_BUFFERS)) {
188                 return false;
189             }
190 
191             if (mInputEnded && (what == MSG_POLL_INPUT_BUFFERS)) {
192                 return false;
193             }
194 
195             return true;
196         }
197 
handleMessageLocked(final Message msg)198         protected boolean handleMessageLocked(final Message msg) {
199             try {
200                 switch (msg.what) {
201                     case MSG_POLL_INPUT_BUFFERS:
202                         pollInputBuffer();
203                         break;
204                     case MSG_POLL_OUTPUT_BUFFERS:
205                         pollOutputBuffer();
206                         break;
207                     default:
208                         return false;
209                 }
210             } catch (final IllegalStateException e) {
211                 e.printStackTrace();
212                 mCallbackSender.notifyError(ERROR_CODEC);
213             }
214 
215             return true;
216         }
217 
pollInputBuffer()218         private void pollInputBuffer() {
219             final int result = mCodec.dequeueInputBuffer(DEQUEUE_TIMEOUT_US);
220             if (result >= 0) {
221                 mCallbackSender.notifyInputBuffer(result);
222             } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) {
223                 mBufferPoller.schedulePollingIfNotCanceled(BufferPoller.MSG_POLL_INPUT_BUFFERS);
224             } else {
225                 mCallbackSender.notifyError(result);
226             }
227         }
228 
pollOutputBuffer()229         private void pollOutputBuffer() {
230             boolean dequeueMoreBuffer = true;
231             final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
232             final int result = mCodec.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT_US);
233             if (result >= 0) {
234                 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
235                     mOutputEnded = true;
236                 }
237                 mCallbackSender.notifyOutputBuffer(result, info);
238             } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
239                 mOutputBuffers = mCodec.getOutputBuffers();
240             } else if (result == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
241                 mOutputBuffers = mCodec.getOutputBuffers();
242                 mCallbackSender.notifyOutputFormat(mCodec.getOutputFormat());
243             } else if (result == MediaCodec.INFO_TRY_AGAIN_LATER) {
244                 // When input ended, keep polling remaining output buffer until EOS.
245                 dequeueMoreBuffer = mInputEnded;
246             } else {
247                 mCallbackSender.notifyError(result);
248                 dequeueMoreBuffer = false;
249             }
250 
251             if (dequeueMoreBuffer) {
252                 schedulePollingIfNotCanceled(MSG_POLL_OUTPUT_BUFFERS);
253             }
254         }
255     }
256 
257     private MediaCodec mCodec;
258     private ByteBuffer[] mInputBuffers;
259     private ByteBuffer[] mOutputBuffers;
260     private AsyncCodec.Callbacks mCallbacks;
261     private CallbackSender mCallbackSender;
262 
263     private BufferPoller mBufferPoller;
264     private volatile boolean mInputEnded;
265     private volatile boolean mOutputEnded;
266 
267     // Must be called on a thread with looper.
JellyBeanAsyncCodec(final String name)268     /* package */ JellyBeanAsyncCodec(final String name) throws IOException {
269         mCodec = MediaCodec.createByCodecName(name);
270         initBufferPoller(name + " buffer poller");
271     }
272 
initBufferPoller(final String name)273     private void initBufferPoller(final String name) {
274         if (mBufferPoller != null) {
275             Log.e(LOGTAG, "poller already initialized");
276             return;
277         }
278         final HandlerThread thread = new HandlerThread(name);
279         thread.start();
280         mBufferPoller = new BufferPoller(thread.getLooper());
281         if (DEBUG) {
282             Log.d(LOGTAG, "start poller for codec:" + this + ", thread=" + thread.getThreadId());
283         }
284     }
285 
286     @Override
setCallbacks(final AsyncCodec.Callbacks callbacks, final Handler handler)287     public void setCallbacks(final AsyncCodec.Callbacks callbacks, final Handler handler) {
288         if (callbacks == null) {
289             return;
290         }
291 
292         Looper looper = (handler == null) ? null : handler.getLooper();
293         if (looper == null) {
294             // Use this thread if no handler supplied.
295             looper = Looper.myLooper();
296         }
297         if (looper == null) {
298             // This thread has no looper. Use poller thread.
299             looper = mBufferPoller.getLooper();
300         }
301         mCallbackSender = new CallbackSender(looper, callbacks);
302         if (DEBUG) {
303             Log.d(LOGTAG, "setCallbacks(): sender=" + mCallbackSender);
304         }
305     }
306 
307     @Override
configure(final MediaFormat format, final Surface surface, final MediaCrypto crypto, final int flags)308     public void configure(final MediaFormat format, final Surface surface,
309                           final MediaCrypto crypto, final int flags) {
310         assertCallbacks();
311 
312         mCodec.configure(format, surface, crypto, flags);
313     }
314 
315     @Override
isAdaptivePlaybackSupported(final String mimeType)316     public boolean isAdaptivePlaybackSupported(final String mimeType) {
317         return HardwareCodecCapabilityUtils.checkSupportsAdaptivePlayback(mCodec, mimeType);
318     }
319 
320     @Override
isTunneledPlaybackSupported(final String mimeType)321     public boolean isTunneledPlaybackSupported(final String mimeType) {
322         try {
323             return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP
324                    && mCodec.getCodecInfo()
325                             .getCapabilitiesForType(mimeType)
326                             .isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
327         } catch (final Exception e) {
328             return false;
329         }
330     }
331 
assertCallbacks()332     private void assertCallbacks() {
333         if (mCallbackSender == null) {
334             throw new IllegalStateException(LOGTAG + ": callback must be supplied with setCallbacks().");
335         }
336     }
337 
338     @Override
start()339     public void start() {
340         assertCallbacks();
341 
342         mCodec.start();
343         mInputEnded = false;
344         mOutputEnded = false;
345         mInputBuffers = mCodec.getInputBuffers();
346         resumeReceivingInputs();
347         mOutputBuffers = mCodec.getOutputBuffers();
348     }
349 
350     @Override
resumeReceivingInputs()351     public void resumeReceivingInputs() {
352         for (int i = 0; i < mInputBuffers.length; i++) {
353             mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
354         }
355     }
356 
357     @Override
setBitrate(final int bps)358     public final void setBitrate(final int bps) {
359         if (android.os.Build.VERSION.SDK_INT >= 19) {
360             final Bundle params = new Bundle();
361             params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bps);
362             mCodec.setParameters(params);
363         }
364     }
365 
366     @Override
queueInputBuffer(final int index, final int offset, final int size, final long presentationTimeUs, final int flags)367     public final void queueInputBuffer(final int index, final int offset, final int size,
368                                        final long presentationTimeUs, final int flags) {
369         assertCallbacks();
370 
371         mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
372 
373         if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
374                 && ((flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0)) {
375             final Bundle params = new Bundle();
376             params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
377             mCodec.setParameters(params);
378         }
379 
380         try {
381             mCodec.queueInputBuffer(index, offset, size, presentationTimeUs, flags);
382         } catch (final IllegalStateException e) {
383             e.printStackTrace();
384             mCallbackSender.notifyError(ERROR_CODEC);
385             return;
386         }
387 
388         mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
389         mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
390     }
391 
392     @Override
queueSecureInputBuffer(final int index, final int offset, final MediaCodec.CryptoInfo cryptoInfo, final long presentationTimeUs, final int flags)393     public final void queueSecureInputBuffer(final int index,
394                                              final int offset,
395                                              final MediaCodec.CryptoInfo cryptoInfo,
396                                              final long presentationTimeUs,
397                                              final int flags) {
398         assertCallbacks();
399 
400         mInputEnded = (flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
401 
402         try {
403             mCodec.queueSecureInputBuffer(index, offset, cryptoInfo, presentationTimeUs, flags);
404         } catch (final IllegalStateException e) {
405             e.printStackTrace();
406             mCallbackSender.notifyError(ERROR_CODEC);
407             return;
408         }
409 
410         mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_INPUT_BUFFERS);
411         mBufferPoller.schedulePolling(BufferPoller.MSG_POLL_OUTPUT_BUFFERS);
412     }
413 
414     @Override
releaseOutputBuffer(final int index, final boolean render)415     public final void releaseOutputBuffer(final int index, final boolean render) {
416         assertCallbacks();
417 
418         mCodec.releaseOutputBuffer(index, render);
419     }
420 
421     @Override
getInputBuffer(final int index)422     public final ByteBuffer getInputBuffer(final int index) {
423         assertCallbacks();
424 
425         return mInputBuffers[index];
426     }
427 
428     @Override
getOutputBuffer(final int index)429     public final ByteBuffer getOutputBuffer(final int index) {
430         assertCallbacks();
431 
432         return mOutputBuffers[index];
433     }
434 
435     @Override
flush()436     public void flush() {
437         assertCallbacks();
438 
439         mInputEnded = false;
440         mOutputEnded = false;
441         cancelPendingTasks();
442         mCodec.flush();
443     }
444 
cancelPendingTasks()445     private void cancelPendingTasks() {
446         mBufferPoller.cancel();
447         mCallbackSender.cancel();
448     }
449 
450     @Override
stop()451     public void stop() {
452         assertCallbacks();
453 
454         cancelPendingTasks();
455         mCodec.stop();
456     }
457 
458     @Override
release()459     public void release() {
460         assertCallbacks();
461 
462         cancelPendingTasks();
463         mCallbackSender = null;
464         mCodec.release();
465         stopBufferPoller();
466     }
467 
stopBufferPoller()468     private void stopBufferPoller() {
469         if (mBufferPoller == null) {
470             Log.e(LOGTAG, "no initialized poller.");
471             return;
472         }
473 
474         mBufferPoller.getLooper().quit();
475         mBufferPoller = null;
476 
477         if (DEBUG) {
478             Log.d(LOGTAG, "stop poller " + this);
479         }
480     }
481 }
482