/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.media; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.IBinder; import android.os.RemoteException; import android.os.TransactionTooLargeException; import android.util.Log; import android.view.Surface; import java.io.IOException; import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /* package */ final class Codec extends ICodec.Stub implements IBinder.DeathRecipient { private static final String LOGTAG = "GeckoRemoteCodec"; private static final boolean DEBUG = false; public enum Error { DECODE, FATAL }; private final class Callbacks implements AsyncCodec.Callbacks { private ICodecCallbacks mRemote; private boolean mHasInputCapacitySet; private boolean mHasOutputCapacitySet; public Callbacks(ICodecCallbacks remote) { mRemote = remote; } @Override public void onInputBufferAvailable(AsyncCodec codec, int index) { if (mFlushing) { // Flush invalidates all buffers. return; } if (!mHasInputCapacitySet) { int capacity = codec.getInputBuffer(index).capacity(); if (capacity > 0) { mSamplePool.setInputBufferSize(capacity); mHasInputCapacitySet = true; } } if (!mInputProcessor.onBuffer(index)) { reportError(Error.FATAL, new Exception("FAIL: input buffer queue is full")); } } @Override public void onOutputBufferAvailable(AsyncCodec codec, int index, MediaCodec.BufferInfo info) { if (mFlushing) { // Flush invalidates all buffers. return; } ByteBuffer output = codec.getOutputBuffer(index); if (!mHasOutputCapacitySet) { int capacity = output.capacity(); if (capacity > 0) { mSamplePool.setOutputBufferSize(capacity); mHasOutputCapacitySet = true; } } Sample copy = mSamplePool.obtainOutput(info); try { if (info.size > 0) { copy.buffer.readFromByteBuffer(output, info.offset, info.size); } mSentOutputs.add(copy); mRemote.onOutput(copy); } catch (IOException e) { Log.e(LOGTAG, "Fail to read output buffer:" + e.getMessage()); outputDummy(info); } catch (TransactionTooLargeException ttle) { Log.e(LOGTAG, "Output is too large:" + ttle.getMessage()); outputDummy(info); } catch (RemoteException e) { // Dead recipient. e.printStackTrace(); } mCodec.releaseOutputBuffer(index, true); boolean eos = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; if (DEBUG && eos) { Log.d(LOGTAG, "output EOS"); } } private void outputDummy(MediaCodec.BufferInfo info) { try { if (DEBUG) Log.d(LOGTAG, "return dummy sample"); mRemote.onOutput(Sample.create(null, info, null)); } catch (RemoteException e) { // Dead recipient. e.printStackTrace(); } } @Override public void onError(AsyncCodec codec, int error) { reportError(Error.FATAL, new Exception("codec error:" + error)); } @Override public void onOutputFormatChanged(AsyncCodec codec, MediaFormat format) { try { mRemote.onOutputFormatChanged(new FormatParam(format)); } catch (RemoteException re) { // Dead recipient. re.printStackTrace(); } } } private final class InputProcessor { private Queue mInputSamples = new LinkedList<>(); private Queue mAvailableInputBuffers = new LinkedList<>(); private Queue mDequeuedSamples = new LinkedList<>(); private synchronized Sample onAllocate(int size) { Sample sample = mSamplePool.obtainInput(size); mDequeuedSamples.add(sample); return sample; } private synchronized boolean onSample(Sample sample) { if (sample == null) { return false; } if (!sample.isEOS()) { Sample temp = sample; sample = mDequeuedSamples.remove(); sample.info = temp.info; sample.cryptoInfo = temp.cryptoInfo; temp.dispose(); } if (!mInputSamples.offer(sample)) { return false; } feedSampleToBuffer(); return true; } private synchronized boolean onBuffer(int index) { if (!mAvailableInputBuffers.offer(index)) { return false; } feedSampleToBuffer(); return true; } private void feedSampleToBuffer() { while (!mAvailableInputBuffers.isEmpty() && !mInputSamples.isEmpty()) { int index = mAvailableInputBuffers.poll(); int len = 0; Sample sample = mInputSamples.poll(); long pts = sample.info.presentationTimeUs; int flags = sample.info.flags; if (!sample.isEOS() && sample.buffer != null) { len = sample.info.size; ByteBuffer buf = mCodec.getInputBuffer(index); try { sample.writeToByteBuffer(buf); mCallbacks.onInputExhausted(); } catch (IOException e) { e.printStackTrace(); } catch (RemoteException e) { e.printStackTrace(); } mSamplePool.recycleInput(sample); } mCodec.queueInputBuffer(index, 0, len, pts, flags); } } private synchronized void reset() { mInputSamples.clear(); mAvailableInputBuffers.clear(); } } private volatile ICodecCallbacks mCallbacks; private AsyncCodec mCodec; private InputProcessor mInputProcessor; private volatile boolean mFlushing = false; private SamplePool mSamplePool; private Queue mSentOutputs = new ConcurrentLinkedQueue<>(); public synchronized void setCallbacks(ICodecCallbacks callbacks) throws RemoteException { mCallbacks = callbacks; callbacks.asBinder().linkToDeath(this, 0); } // IBinder.DeathRecipient @Override public synchronized void binderDied() { Log.e(LOGTAG, "Callbacks is dead"); try { release(); } catch (RemoteException e) { // Nowhere to report the error. } } @Override public synchronized boolean configure(FormatParam format, Surface surface, int flags) throws RemoteException { if (mCallbacks == null) { Log.e(LOGTAG, "FAIL: callbacks must be set before calling configure()"); return false; } if (mCodec != null) { if (DEBUG) Log.d(LOGTAG, "release existing codec: " + mCodec); releaseCodec(); } if (DEBUG) Log.d(LOGTAG, "configure " + this); MediaFormat fmt = format.asFormat(); String codecName = getDecoderForFormat(fmt); if (codecName == null) { Log.e(LOGTAG, "FAIL: cannot find codec"); return false; } try { AsyncCodec codec = AsyncCodecFactory.create(codecName); codec.setCallbacks(new Callbacks(mCallbacks), null); codec.configure(fmt, surface, flags); mCodec = codec; mInputProcessor = new InputProcessor(); mSamplePool = new SamplePool(codecName); if (DEBUG) Log.d(LOGTAG, codec.toString() + " created"); return true; } catch (Exception e) { if (DEBUG) Log.d(LOGTAG, "FAIL: cannot create codec -- " + codecName); e.printStackTrace(); return false; } } private void releaseCodec() { mInputProcessor.reset(); try { mCodec.release(); } catch (Exception e) { reportError(Error.FATAL, e); } mCodec = null; } private String getDecoderForFormat(MediaFormat format) { String mime = format.getString(MediaFormat.KEY_MIME); if (mime == null) { return null; } int numCodecs = MediaCodecList.getCodecCount(); for (int i = 0; i < numCodecs; i++) { MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); if (info.isEncoder()) { continue; } String[] types = info.getSupportedTypes(); for (String t : types) { if (t.equalsIgnoreCase(mime)) { return info.getName(); } } } return null; // TODO: API 21+ is simpler. //static MediaCodecList sCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); //return sCodecList.findDecoderForFormat(format); } @Override public synchronized void start() throws RemoteException { if (DEBUG) Log.d(LOGTAG, "start " + this); mFlushing = false; try { mCodec.start(); } catch (Exception e) { reportError(Error.FATAL, e); } } private void reportError(Error error, Exception e) { if (e != null) { e.printStackTrace(); } try { mCallbacks.onError(error == Error.FATAL); } catch (RemoteException re) { re.printStackTrace(); } } @Override public synchronized void stop() throws RemoteException { if (DEBUG) Log.d(LOGTAG, "stop " + this); try { mCodec.stop(); } catch (Exception e) { reportError(Error.FATAL, e); } } @Override public synchronized void flush() throws RemoteException { mFlushing = true; if (DEBUG) Log.d(LOGTAG, "flush " + this); mInputProcessor.reset(); try { mCodec.flush(); } catch (Exception e) { reportError(Error.FATAL, e); } mFlushing = false; if (DEBUG) Log.d(LOGTAG, "flushed " + this); } @Override public synchronized Sample dequeueInput(int size) { return mInputProcessor.onAllocate(size); } @Override public synchronized void queueInput(Sample sample) throws RemoteException { if (!mInputProcessor.onSample(sample)) { reportError(Error.FATAL, new Exception("FAIL: input sample queue is full")); } } @Override public synchronized void releaseOutput(Sample sample) { try { mSamplePool.recycleOutput(mSentOutputs.remove()); } catch (Exception e) { Log.e(LOGTAG, "failed to release output:" + sample); e.printStackTrace(); } sample.dispose(); } @Override public synchronized void release() throws RemoteException { if (DEBUG) Log.d(LOGTAG, "release " + this); releaseCodec(); mSamplePool.reset(); mSamplePool = null; mCallbacks.asBinder().unlinkToDeath(this, 0); mCallbacks = null; } }