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