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