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