/*
 * Copyright (C) 2011-2014 GUIGUI Simon, [email protected]
 * 
 * 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 java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;

import net.majorkernelpanic.streaming.hw.CodecManager.Codec;
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;

/**
 * 
 * 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()  {
		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() {
		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 withPrefix 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);
		} 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;
	}

}