content: /*
   * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
   * 
   * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
   * 
   * Spydroid is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 3 of the License, or
   * (at your option) any later version.
   * 
   * This source code is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   * 
   * You should have received a copy of the GNU General Public License
   * along with this source code; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   */
  
  package net.majorkernelpanic.streaming.hw;
  
  import android.annotation.SuppressLint;
  import android.content.Context;
  import android.content.SharedPreferences;
  import android.content.SharedPreferences.Editor;
  import android.media.MediaCodec;
  import android.media.MediaCodec.BufferInfo;
  import android.media.MediaCodecInfo;
  import android.media.MediaFormat;
  import android.os.Build;
  import android.preference.PreferenceManager;
  import android.util.Base64;
  import android.util.Log;
  
  import net.majorkernelpanic.streaming.hw.CodecManager.Codec;
  
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.nio.ByteBuffer;
  
  /**
   * 
   * The purpose of this class is to detect and by-pass some bugs (or underspecified configuration) that
   * encoders available through the MediaCodec API may have. <br />
   * Feeding the encoder with a surface is not tested here.
   * Some bugs you may have encountered:<br />
   * <ul>
   * <li>U and V panes reversed</li>
   * <li>Some padding is needed after the Y pane</li>
   * <li>stride!=width or slice-height!=height</li>
   * </ul>
   */
  @SuppressLint("NewApi")
  public class EncoderDebugger {
  
  	public final static String TAG = "EncoderDebugger";
  
  	/** Prefix that will be used for all shared preferences saved by libstreaming. */
  	private static final String PREF_PREFIX = "libstreaming-";
  
  	/** 
  	 * If this is set to false the test will be run only once and the result 
  	 * will be saved in the shared preferences. 
  	 */
  	private static final boolean DEBUG = false;
  	
  	/** Set this to true to see more logs. */
  	private static final boolean VERBOSE = false;
  
  	/** Will be incremented every time this test is modified. */
  	private static final int VERSION = 3;
  
  	/** Bitrate that will be used with the encoder. */
  	private final static int BITRATE = 1000000;
  
  	/** Framerate that will be used to test the encoder. */
  	private final static int FRAMERATE = 20;
  
  	private final static String MIME_TYPE = "video/avc";
  
  	private final static int NB_DECODED = 34;
  	private final static int NB_ENCODED = 50;
  
  	private int mDecoderColorFormat, mEncoderColorFormat;
  	private String mDecoderName, mEncoderName, mErrorLog;
  	private MediaCodec mEncoder, mDecoder;
  	private int mWidth, mHeight, mSize;
  	private byte[] mSPS, mPPS;
  	private byte[] mData, mInitialImage;
  	private MediaFormat mDecOutputFormat;
  	private NV21Convertor mNV21;
  	private SharedPreferences mPreferences;
  	private byte[][] mVideo, mDecodedVideo;
  	private String mB64PPS, mB64SPS;
  
  	public synchronized static void asyncDebug(final Context context, final int width, final int height) {
  		new Thread(new Runnable() {
  			@Override
  			public void run() {
  				try {
  					SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
  					debug(prefs, width, height);
  				} catch (Exception e) {}
  			}
  		}).start();
  	}
  	
  	public synchronized static EncoderDebugger debug(Context context, int width, int height) {
  		SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
  		return debug(prefs, width, height);
  	}
  
  	public synchronized static EncoderDebugger debug(SharedPreferences prefs, int width, int height) {
  		EncoderDebugger debugger = new EncoderDebugger(prefs, width, height);
  		debugger.debug();
  		return debugger;
  	}
  
  	public String getB64PPS() {
  		return mB64PPS;
  	}
  
  	public String getB64SPS() {
  		return mB64SPS;
  	}
  
  	public String getEncoderName() {
  		return mEncoderName;
  	}
  
  	public int getEncoderColorFormat() {
  		return mEncoderColorFormat;
  	}
  
  	/** This {@link NV21Convertor} will do the necessary work to feed properly the encoder. */
  	public NV21Convertor getNV21Convertor() {
  		return mNV21;
  	}
  
  	/** A log of all the errors that occured during the test. */
  	public String getErrorLog() {
  		return mErrorLog;
  	}
  
  	private EncoderDebugger(SharedPreferences prefs, int width, int height) {
  		mPreferences = prefs;
  		mWidth = width;
  		mHeight = height;
  		mSize = width*height;
  		reset();
  	}
  
  	private void reset() {
  		mNV21 = new NV21Convertor();
  		mVideo = new byte[NB_ENCODED][];
  		mDecodedVideo = new byte[NB_DECODED][];
  		mErrorLog = "";
  		mPPS = null;
  		mSPS = null;		
  	}
  
  	private void debug() {
  		
  		// If testing the phone again is not needed, 
  		// we just restore the result from the shared preferences
  		if (!checkTestNeeded()) {
  			String resolution = mWidth+"x"+mHeight+"-";			
  
  			boolean success = mPreferences.getBoolean(PREF_PREFIX+resolution+"success",false);
  			if (!success) {
  				throw new RuntimeException("Phone not supported with this resolution ("+mWidth+"x"+mHeight+")");
  			}
  
  			mNV21.setSize(mWidth, mHeight);
  			mNV21.setSliceHeigth(mPreferences.getInt(PREF_PREFIX+resolution+"sliceHeight", 0));
  			mNV21.setStride(mPreferences.getInt(PREF_PREFIX+resolution+"stride", 0));
  			mNV21.setYPadding(mPreferences.getInt(PREF_PREFIX+resolution+"padding", 0));
  			mNV21.setPlanar(mPreferences.getBoolean(PREF_PREFIX+resolution+"planar", false));
  			mNV21.setColorPanesReversed(mPreferences.getBoolean(PREF_PREFIX+resolution+"reversed", false));
  			mEncoderName = mPreferences.getString(PREF_PREFIX+resolution+"encoderName", "");
  			mEncoderColorFormat = mPreferences.getInt(PREF_PREFIX+resolution+"colorFormat", 0);
  			mB64PPS = mPreferences.getString(PREF_PREFIX+resolution+"pps", "");
  			mB64SPS = mPreferences.getString(PREF_PREFIX+resolution+"sps", "");
  
  			return;
  		}
  
  		if (VERBOSE) Log.d(TAG, ">>>> Testing the phone for resolution "+mWidth+"x"+mHeight);
  		
  		// Builds a list of available encoders and decoders we may be able to use
  		// because they support some nice color formats
  		Codec[] encoders = CodecManager.findEncodersForMimeType(MIME_TYPE);
  		Codec[] decoders = CodecManager.findDecodersForMimeType(MIME_TYPE);
  
  		int count = 0, n = 1;
  		for (int i=0;i<encoders.length;i++) {
  			count += encoders[i].formats.length;
  		}
  		
  		// Tries available encoders
  		for (int i=0;i<encoders.length;i++) {
  			for (int j=0;j<encoders[i].formats.length;j++) {
  				reset();
  				
  				mEncoderName = encoders[i].name;
  				mEncoderColorFormat = encoders[i].formats[j];
  
  				if (VERBOSE) Log.v(TAG, ">> Test "+(n++)+"/"+count+": "+mEncoderName+" with color format "+mEncoderColorFormat+" at "+mWidth+"x"+mHeight);
  				
  				// Converts from NV21 to YUV420 with the specified parameters
  				mNV21.setSize(mWidth, mHeight);
  				mNV21.setSliceHeigth(mHeight);
  				mNV21.setStride(mWidth);
  				mNV21.setYPadding(0);
  				mNV21.setEncoderColorFormat(mEncoderColorFormat);
  
  				// /!\ NV21Convertor can directly modify the input
  				createTestImage();
  				mData = mNV21.convert(mInitialImage);
  
  				try {
  
  					// Starts the encoder
  					configureEncoder();
  					searchSPSandPPS();
  					
  					if (VERBOSE) Log.v(TAG, "SPS and PPS in b64: SPS="+mB64SPS+", PPS="+mB64PPS);
  
  					// Feeds the encoder with an image repeatidly to produce some NAL units
  					encode();
  
  					// We now try to decode the NALs with decoders available on the phone
  					boolean decoded = false;
  					for (int k=0;k<decoders.length && !decoded;k++) {
  						for (int l=0;l<decoders[k].formats.length && !decoded;l++) {
  							mDecoderName = decoders[k].name;
  							mDecoderColorFormat = decoders[k].formats[l];
  							try {
  								configureDecoder();
  							} catch (Exception e) {
  								if (VERBOSE) Log.d(TAG, mDecoderName+" can't be used with "+mDecoderColorFormat+" at "+mWidth+"x"+mHeight);
  								releaseDecoder();
  								break;
  							}
  							try {
  								decode(true);
  								if (VERBOSE) Log.d(TAG, mDecoderName+" successfully decoded the NALs (color format "+mDecoderColorFormat+")");
  								decoded = true;
  							} catch (Exception e) {
  								if (VERBOSE) Log.e(TAG, mDecoderName+" failed to decode the NALs");
  								e.printStackTrace();
  							} finally {
  								releaseDecoder();
  							}
  						}
  					}
  
  					if (!decoded) throw new RuntimeException("Failed to decode NALs from the encoder.");
  
  					// Compares the image before and after
  					if (!compareLumaPanes()) {
  						// TODO: try again with a different stride
  						// TODO: try again with the "stride" param
  						throw new RuntimeException("It is likely that stride!=width");
  					}
  
  					int padding;
  					if ((padding = checkPaddingNeeded())>0) {
  						if (padding<4096) {
  							if (VERBOSE) Log.d(TAG, "Some padding is needed: "+padding);
  							mNV21.setYPadding(padding);
  							createTestImage();
  							mData = mNV21.convert(mInitialImage);
  							encodeDecode();
  						} else {
  							// TODO: try again with a different sliceHeight
  							// TODO: try again with the "slice-height" param
  							throw new RuntimeException("It is likely that sliceHeight!=height");
  						}
  					}
  
  					createTestImage();
  					if (!compareChromaPanes(false)) {
  						if (compareChromaPanes(true)) {
  							mNV21.setColorPanesReversed(true);
  							if (VERBOSE) Log.d(TAG, "U and V pane are reversed");
  						} else {
  							throw new RuntimeException("Incorrect U or V pane...");
  						}
  					}
  
  					saveTestResult(true);
  					Log.v(TAG, "The encoder "+mEncoderName+" is usable with resolution "+mWidth+"x"+mHeight);
  					return;
  
  				} catch (Exception e) {
  					StringWriter sw = new StringWriter();
  					PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw);
  					String stack = sw.toString();
  					String str = "Encoder "+mEncoderName+" cannot be used with color format "+mEncoderColorFormat;
  					if (VERBOSE) Log.e(TAG, str, e);
  					mErrorLog += str + "\n" + stack;
  					e.printStackTrace();
  				} finally {
  					releaseEncoder();
  				}
  
  			}
  		}
  
  		saveTestResult(false);
  		Log.e(TAG,"No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight);
  		throw new RuntimeException("No usable encoder were found on the phone for resolution "+mWidth+"x"+mHeight);
  
  	}
  
  	private boolean checkTestNeeded() {
  		String resolution = mWidth+"x"+mHeight+"-";
  
  		// Forces the test
  		if (DEBUG || mPreferences==null) return true; 
  
  		// If the sdk has changed on the phone, or the version of the test 
  		// it has to be run again
  		if (mPreferences.contains(PREF_PREFIX+resolution+"lastSdk")) {
  			int lastSdk = mPreferences.getInt(PREF_PREFIX+resolution+"lastSdk", 0);
  			int lastVersion = mPreferences.getInt(PREF_PREFIX+resolution+"lastVersion", 0);
  			if (Build.VERSION.SDK_INT>lastSdk || VERSION>lastVersion) {
  				return true;
  			}
  		} else {
  			return true;
  		}
  		return false;
  	}
  
  
  	/**
  	 * Saves the result of the test in the shared preferences,
  	 * we will run it again only if the SDK has changed on the phone,
  	 * or if this test has been modified.
  	 */	
  	private void saveTestResult(boolean success) {
  		String resolution = mWidth+"x"+mHeight+"-";
  		Editor editor = mPreferences.edit();
  
  		editor.putBoolean(PREF_PREFIX+resolution+"success", success);
  
  		if (success) {
  			editor.putInt(PREF_PREFIX+resolution+"lastSdk", Build.VERSION.SDK_INT);
  			editor.putInt(PREF_PREFIX+resolution+"lastVersion", VERSION);
  			editor.putInt(PREF_PREFIX+resolution+"sliceHeight", mNV21.getSliceHeigth());
  			editor.putInt(PREF_PREFIX+resolution+"stride", mNV21.getStride());
  			editor.putInt(PREF_PREFIX+resolution+"padding", mNV21.getYPadding());
  			editor.putBoolean(PREF_PREFIX+resolution+"planar", mNV21.getPlanar());
  			editor.putBoolean(PREF_PREFIX+resolution+"reversed", mNV21.getUVPanesReversed());
  			editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName);
  			editor.putInt(PREF_PREFIX+resolution+"colorFormat", mEncoderColorFormat);
  			editor.putString(PREF_PREFIX+resolution+"encoderName", mEncoderName);
  			editor.putString(PREF_PREFIX+resolution+"pps", mB64PPS);
  			editor.putString(PREF_PREFIX+resolution+"sps", mB64SPS);
  		}
  
  		editor.commit();
  	}
  
  	/**
  	 * Creates the test image that will be used to feed the encoder.
  	 */
  	private void createTestImage() {
  		mInitialImage = new byte[3*mSize/2];
  		for (int i=0;i<mSize;i++) {
  			mInitialImage[i] = (byte) (40+i%199);
  		}
  		for (int i=mSize;i<3*mSize/2;i+=2) {
  			mInitialImage[i] = (byte) (40+i%200);
  			mInitialImage[i+1] = (byte) (40+(i+99)%200);
  		}
  
  	}
  
  	/**
  	 * Compares the Y pane of the initial image, and the Y pane
  	 * after having encoded & decoded the image.
  	 */
  	private boolean compareLumaPanes() {
  		int d, e, f = 0;
  		for (int j=0;j<NB_DECODED;j++) {
  			for (int i=0;i<mSize;i+=10) {
  				d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i]&0xFF);
  				e = (mInitialImage[i+1]&0xFF) - (mDecodedVideo[j][i+1]&0xFF);
  				d = d<0 ? -d : d;
  				e = e<0 ? -e : e;
  				if (d>50 && e>50) {
  					mDecodedVideo[j] = null;
  					f++;
  					break;
  				}
  			}
  		}
  		return f<=NB_DECODED/2;
  	}
  
  	private int checkPaddingNeeded() {
  		int i = 0, j = 3*mSize/2-1, max = 0;
  		int[] r = new int[NB_DECODED];
  		for (int k=0;k<NB_DECODED;k++) {
  			if (mDecodedVideo[k] != null) {
  				i = 0;
  				while (i<j && (mDecodedVideo[k][j-i]&0xFF)<50) i+=2;
  				if (i>0) {
  					r[k] = ((i>>6)<<6);
  					max = r[k]>max ? r[k] : max;
  					if (VERBOSE) Log.e(TAG,"Padding needed: "+r[k]);
  				} else {
  					if (VERBOSE) Log.v(TAG,"No padding needed.");
  				}
  			}
  		}
  
  		return ((max>>6)<<6);
  	}
  
  	/**
  	 * Compares the U or V pane of the initial image, and the U or V pane
  	 * after having encoded & decoded the image.
  	 */	
  	private boolean compareChromaPanes(boolean crossed) {
  		int d, f = 0;
  
  		for (int j=0;j<NB_DECODED;j++) {
  			if (mDecodedVideo[j] != null) {
  				// We compare the U and V pane before and after
  				if (!crossed) {
  					for (int i=mSize;i<3*mSize/2;i+=1) {
  						d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i]&0xFF);
  						d = d<0 ? -d : d;
  						if (d>50) {
  							//if (VERBOSE) Log.e(TAG,"BUG "+(i-mSize)+" d "+d);
  							f++;
  							break;
  						}
  					}
  
  					// We compare the V pane before with the U pane after
  				} else {
  					for (int i=mSize;i<3*mSize/2;i+=2) {
  						d = (mInitialImage[i]&0xFF) - (mDecodedVideo[j][i+1]&0xFF);
  						d = d<0 ? -d : d;
  						if (d>50) {
  							f++;
  						}
  					}
  				}
  			}
  		}
  		return f<=NB_DECODED/2;
  	}	
  
  	/**
  	 * Converts the image obtained from the decoder to NV21.
  	 */
  	private void convertToNV21(int k) {		
  		byte[] buffer = new byte[3*mSize/2];
  
  		int stride = mWidth, sliceHeight = mHeight;
  		int colorFormat = mDecoderColorFormat;
  		boolean planar = false;
  
  		if (mDecOutputFormat != null) {
  			MediaFormat format = mDecOutputFormat;
  			if (format != null) {
  				if (format.containsKey("slice-height")) {
  					sliceHeight = format.getInteger("slice-height");
  					if (sliceHeight<mHeight) sliceHeight = mHeight;
  				}
  				if (format.containsKey("stride")) {
  					stride = format.getInteger("stride");
  					if (stride<mWidth) stride = mWidth;
  				}
  				if (format.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
  					if (format.getInteger(MediaFormat.KEY_COLOR_FORMAT)>0) {
  						colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT);
  					}
  				}
  			}
  		}
  
  		switch (colorFormat) {
  		case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
  		case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
  		case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
  			planar = false;
  			break;	
  		case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
  		case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
  			planar = true;
  			break;
  		}
  
  		for (int i=0;i<mSize;i++) {
  			if (i%mWidth==0) i+=stride-mWidth;
  			buffer[i] = mDecodedVideo[k][i];
  		}
  
  		if (!planar) {
  			for (int i=0,j=0;j<mSize/4;i+=1,j+=1) {
  				if (i%mWidth/2==0) i+=(stride-mWidth)/2;
  				buffer[mSize+2*j+1] = mDecodedVideo[k][stride*sliceHeight+2*i];
  				buffer[mSize+2*j] = mDecodedVideo[k][stride*sliceHeight+2*i+1];
  			}
  		} else {
  			for (int i=0,j=0;j<mSize/4;i+=1,j+=1) {
  				if (i%mWidth/2==0) i+=(stride-mWidth)/2;
  				buffer[mSize+2*j+1] = mDecodedVideo[k][stride*sliceHeight+i];
  				buffer[mSize+2*j] = mDecodedVideo[k][stride*sliceHeight*5/4+i];
  			}
  		}
  
  		mDecodedVideo[k] = buffer;
  
  	}
  
  	/**
  	 * Instantiates and starts the encoder.
  	 */
  	private void configureEncoder() throws IOException {
  		mEncoder = MediaCodec.createByCodecName(mEncoderName);
  		MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
  		mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE);
  		mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAMERATE);	
  		mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mEncoderColorFormat);
  		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
  		mEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  		mEncoder.start();
  	}
  
  	private void releaseEncoder() {
  		if (mEncoder != null) {
  			try {
  				mEncoder.stop();
  			} catch (Exception ignore) {}
  			try {
  				mEncoder.release();
  			} catch (Exception ignore) {}
  		}
  	}
  
  	/**
  	 * Instantiates and starts the decoder.
  	 */	
  	private void configureDecoder() throws IOException {
  		byte[] prefix = new byte[] {0x00,0x00,0x00,0x01};
  
  		ByteBuffer csd0 = ByteBuffer.allocate(4+mSPS.length+4+mPPS.length);
  		csd0.put(new byte[] {0x00,0x00,0x00,0x01});
  		csd0.put(mSPS);
  		csd0.put(new byte[] {0x00,0x00,0x00,0x01});
  		csd0.put(mPPS);
  
  		mDecoder = MediaCodec.createByCodecName(mDecoderName);
  		MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
  		mediaFormat.setByteBuffer("csd-0", csd0);
  		mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mDecoderColorFormat);
  		mDecoder.configure(mediaFormat, null, null, 0);
  		mDecoder.start();
  
  		ByteBuffer[] decInputBuffers = mDecoder.getInputBuffers();
  
  		int decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE);
  		if (decInputIndex>=0) {
  			decInputBuffers[decInputIndex].clear();
  			decInputBuffers[decInputIndex].put(prefix);
  			decInputBuffers[decInputIndex].put(mSPS);
  			mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0);
  		} else {
  			if (VERBOSE) Log.e(TAG,"No buffer available !");
  		}
  
  		decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE);
  		if (decInputIndex>=0) {
  			decInputBuffers[decInputIndex].clear();
  			decInputBuffers[decInputIndex].put(prefix);
  			decInputBuffers[decInputIndex].put(mPPS);
  			mDecoder.queueInputBuffer(decInputIndex, 0, decInputBuffers[decInputIndex].position(), timestamp(), 0);
  		} else {
  			if (VERBOSE) Log.e(TAG,"No buffer available !");
  		}
  
  
  	}
  
  	private void releaseDecoder() {
  		if (mDecoder != null) {
  			try {
  				mDecoder.stop();
  			} catch (Exception ignore) {}
  			try {
  				mDecoder.release();
  			} catch (Exception ignore) {}
  		}
  	}	
  
  	/**
  	 * Tries to obtain the SPS and the PPS for the encoder.
  	 */
  	private long searchSPSandPPS() {
  
  		ByteBuffer[] inputBuffers = mEncoder.getInputBuffers();
  		ByteBuffer[] outputBuffers = mEncoder.getOutputBuffers();
  		BufferInfo info = new BufferInfo();
  		byte[] csd = new byte[128];
  		int len = 0, p = 4, q = 4;
  		long elapsed = 0, now = timestamp();
  
  		while (elapsed<3000000 && (mSPS==null || mPPS==null)) {
  
  			// Some encoders won't give us the SPS and PPS unless they receive something to encode first...
  			int bufferIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE);
  			if (bufferIndex>=0) {
  				check(inputBuffers[bufferIndex].capacity()>=mData.length, "The input buffer is not big enough.");
  				inputBuffers[bufferIndex].clear();
  				inputBuffers[bufferIndex].put(mData, 0, mData.length);
  				mEncoder.queueInputBuffer(bufferIndex, 0, mData.length, timestamp(), 0);
  			} else {
  				if (VERBOSE) Log.e(TAG,"No buffer available !");
  			}
  
  			// We are looking for the SPS and the PPS here. As always, Android is very inconsistent, I have observed that some
  			// encoders will give those parameters through the MediaFormat object (that is the normal behaviour).
  			// But some other will not, in that case we try to find a NAL unit of type 7 or 8 in the byte stream outputed by the encoder...
  
  			int index = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE);
  
  			if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
  
  				// The PPS and PPS shoud be there
  				MediaFormat format = mEncoder.getOutputFormat();
  				ByteBuffer spsb = format.getByteBuffer("csd-0");
  				ByteBuffer ppsb = format.getByteBuffer("csd-1");
  				mSPS = new byte[spsb.capacity()-4];
  				spsb.position(4);
  				spsb.get(mSPS,0,mSPS.length);
  				mPPS = new byte[ppsb.capacity()-4];
  				ppsb.position(4);
  				ppsb.get(mPPS,0,mPPS.length);
  				break;
  
  			} else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
  				outputBuffers = mEncoder.getOutputBuffers();
  			} else if (index>=0) {
  
  				len = info.size;
  				if (len<128) {
  					outputBuffers[index].get(csd,0,len);
  					if (len>0 && csd[0]==0 && csd[1]==0 && csd[2]==0 && csd[3]==1) {
  						// Parses the SPS and PPS, they could be in two different packets and in a different order 
  						//depending on the phone so we don't make any assumption about that
  						while (p<len) {
  							while (!(csd[p+0]==0 && csd[p+1]==0 && csd[p+2]==0 && csd[p+3]==1) && p+3<len) p++;
  							if (p+3>=len) p=len;
  							if ((csd[q]&0x1F)==7) {
  								mSPS = new byte[p-q];
  								System.arraycopy(csd, q, mSPS, 0, p-q);
  							} else {
  								mPPS = new byte[p-q];
  								System.arraycopy(csd, q, mPPS, 0, p-q);
  							}
  							p += 4;
  							q = p;
  						}
  					}					
  				}
  				mEncoder.releaseOutputBuffer(index, false);
  			}
  
  			elapsed = timestamp() - now;
  		}
  
  		check(mPPS != null & mSPS != null, "Could not determine the SPS & PPS.");
  		mB64PPS = Base64.encodeToString(mPPS, 0, mPPS.length, Base64.NO_WRAP);
  		mB64SPS = Base64.encodeToString(mSPS, 0, mSPS.length, Base64.NO_WRAP);
  
  		return elapsed;
  	}
  
  	private long encode() {
  		int n = 0;
  		long elapsed = 0, now = timestamp();
  		int encOutputIndex = 0, encInputIndex = 0;
  		BufferInfo info = new BufferInfo();
  		ByteBuffer[] encInputBuffers = mEncoder.getInputBuffers();
  		ByteBuffer[] encOutputBuffers = mEncoder.getOutputBuffers();
  
  		while (elapsed<5000000) {
  			// Feeds the encoder with an image
  			encInputIndex = mEncoder.dequeueInputBuffer(1000000/FRAMERATE);
  			if (encInputIndex>=0) {
  				check(encInputBuffers[encInputIndex].capacity()>=mData.length, "The input buffer is not big enough.");
  				encInputBuffers[encInputIndex].clear();
  				encInputBuffers[encInputIndex].put(mData, 0, mData.length);
  				mEncoder.queueInputBuffer(encInputIndex, 0, mData.length, timestamp(), 0);
  			} else {
  				if (VERBOSE) Log.d(TAG,"No buffer available !");
  			}
  
  			// Tries to get a NAL unit
  			encOutputIndex = mEncoder.dequeueOutputBuffer(info, 1000000/FRAMERATE);
  			if (encOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
  				encOutputBuffers = mEncoder.getOutputBuffers();
  			} else if (encOutputIndex>=0) {
  				mVideo[n] = new byte[info.size];
  				encOutputBuffers[encOutputIndex].clear();
  				encOutputBuffers[encOutputIndex].get(mVideo[n++], 0, info.size);
  				mEncoder.releaseOutputBuffer(encOutputIndex, false);
  				if (n>=NB_ENCODED) {
  					flushMediaCodec(mEncoder);
  					return elapsed;
  				}
  			}
  
  			elapsed = timestamp() - now;
  		}
  
  		throw new RuntimeException("The encoder is too slow.");
  
  	}
  
  	/**
  	 * @param withPrefix If set to true, the decoder will be fed with NALs preceeded with 0x00000001.
  	 * @return How long it took to decode all the NALs
  	 */
  	private long decode(boolean withPrefix) {
  		int n = 0, i = 0, j = 0;
  		long elapsed = 0, now = timestamp();
  		int decInputIndex = 0, decOutputIndex = 0;
  		ByteBuffer[] decInputBuffers = mDecoder.getInputBuffers();
  		ByteBuffer[] decOutputBuffers = mDecoder.getOutputBuffers();
  		BufferInfo info = new BufferInfo();
  
  		while (elapsed<3000000) {
  
  			// Feeds the decoder with a NAL unit
  			if (i<NB_ENCODED) {
  				decInputIndex = mDecoder.dequeueInputBuffer(1000000/FRAMERATE);
  				if (decInputIndex>=0) {
  					int l1 = decInputBuffers[decInputIndex].capacity();
  					int l2 = mVideo[i].length;
  					decInputBuffers[decInputIndex].clear();
  					
  					if ((withPrefix && hasPrefix(mVideo[i])) || (!withPrefix && !hasPrefix(mVideo[i]))) {
  						check(l1>=l2, "The decoder input buffer is not big enough (nal="+l2+", capacity="+l1+").");
  						decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length);
  					} else if (withPrefix && !hasPrefix(mVideo[i])) {
  						check(l1>=l2+4, "The decoder input buffer is not big enough (nal="+(l2+4)+", capacity="+l1+").");
  						decInputBuffers[decInputIndex].put(new byte[] {0,0,0,1});
  						decInputBuffers[decInputIndex].put(mVideo[i],0,mVideo[i].length);
  					} else if (!withPrefix && hasPrefix(mVideo[i])) {
  						check(l1>=l2-4, "The decoder input buffer is not big enough (nal="+(l2-4)+", capacity="+l1+").");
  						decInputBuffers[decInputIndex].put(mVideo[i],4,mVideo[i].length-4);
  					}
  					
  					mDecoder.queueInputBuffer(decInputIndex, 0, l2, timestamp(), 0);
  					i++;
  				} else {
  					if (VERBOSE) Log.d(TAG,"No buffer available !");
  				}
  			}
  
  			// Tries to get a decoded image
  			decOutputIndex = mDecoder.dequeueOutputBuffer(info, 1000000/FRAMERATE);
  			if (decOutputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
  				decOutputBuffers = mDecoder.getOutputBuffers();
  			} else if (decOutputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
  				mDecOutputFormat = mDecoder.getOutputFormat();
  			} else if (decOutputIndex>=0) {
  				if (n>2) {
  					// We have successfully encoded and decoded an image !
  					int length = info.size;
  					mDecodedVideo[j] = new byte[length];
  					decOutputBuffers[decOutputIndex].clear();
  					decOutputBuffers[decOutputIndex].get(mDecodedVideo[j], 0, length);
  					// Converts the decoded frame to NV21
  					convertToNV21(j);
  					if (j>=NB_DECODED-1) {
  						flushMediaCodec(mDecoder);
  						if (VERBOSE) Log.v(TAG, "Decoding "+n+" frames took "+elapsed/1000+" ms");
  						return elapsed;
  					}
  					j++;
  				}
  				mDecoder.releaseOutputBuffer(decOutputIndex, false);
  				n++;
  			}	
  			elapsed = timestamp() - now;
  		}
  
  		throw new RuntimeException("The decoder did not decode anything.");
  
  	}
  
  	/**
  	 * Makes sure the NAL has a header or not.
  	 * @param nal If set to true, the NAL will be preceeded with 0x00000001.
  	 */
  	private boolean hasPrefix(byte[] nal) {
  		if (nal[0] == 0 && nal[1] == 0 && nal[2] == 0 && nal[3] == 0x01)
  			return true;
  		else
  			return false;
  	}
  	
  	private void encodeDecode() {
  		encode();
  		try {
  			configureDecoder();
  			decode(true);
  		} catch (IOException e) {
  			e.printStackTrace();
  		} finally {
  			releaseDecoder();
  		}
  	}
  
  	private void flushMediaCodec(MediaCodec mc) {
  		int index = 0;
  		BufferInfo info = new BufferInfo();
  		while (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
  			index = mc.dequeueOutputBuffer(info, 1000000/FRAMERATE);
  			if (index>=0) {
  				mc.releaseOutputBuffer(index, false);
  			}
  		}
  	}
  
  	private void check(boolean cond, String message) {
  		if (!cond) {
  			if (VERBOSE) Log.e(TAG,message);
  			throw new IllegalStateException(message);
  		}
  	}
  
  	private long timestamp() {
  		return System.nanoTime()/1000;
  	}
  
  }
  