<NME> VideoStream.java
<BEF> /*
 * Copyright (C) 2011-2015 GUIGUI Simon, fyhertz@gmail.com
 *
 * This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.majorkernelpanic.streaming.video;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import net.majorkernelpanic.streaming.MediaStream;
import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream;
import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;

/** 
 * Don't use this class directly.
 */
public abstract class VideoStream extends MediaStream {

	protected final static String TAG = "VideoStream";
	private static HashMap<String,SparseArray<ArrayList<String>>> sSupportedColorFormats = new HashMap<String, SparseArray<ArrayList<String>>>(); 

	protected VideoQuality mQuality = VideoQuality.DEFAULT_VIDEO_QUALITY.clone();
	protected SurfaceHolder.Callback mSurfaceHolderCallback = null;
	protected SurfaceHolder mSurfaceHolder = null;
	protected int mVideoEncoder, mCameraId = 0;
	protected Camera mCamera;
	protected boolean mCameraOpenedManually = true;
	protected boolean mFlashState = false;
	protected boolean mSurfaceReady = false;
	protected boolean mUnlocked = false;
	protected boolean mPreviewStarted = false;

	protected int mEncoderColorFormat = 0;
	protected String mEncoderName = null;
	protected int mCameraImageFormat = 0;

	// The FPS really achieved when asking for a certain FPS 
	protected int mActualFramerate = 0;
	// The FPS we need to use with the Camera API to obtain the desired FPS
	protected int mCorrectedFramerate = 0;
	// The maximum possible FPS, depends on the resolution and on the Camera
	protected int mMaxFps = 0;	

	/** 
	 * Don't use this class directly.
	 * Uses CAMERA_FACING_BACK by default.
	 */
	public VideoStream() {
		this(CameraInfo.CAMERA_FACING_BACK);
	}	

	/** 
	 * Don't use this class directly
	 * @param camera Can be either CameraInfo.CAMERA_FACING_BACK or CameraInfo.CAMERA_FACING_FRONT
	 */
	@SuppressLint("InlinedApi")
	public VideoStream(int camera) {
		super();
		mCameraImageFormat = ImageFormat.YV12;
		setCamera(camera);
		if ((sSuggestedMode&MODE_MEDIACODEC_API)!=0) {
			int yuvPlanar = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
			int yuvSemiPlanar = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
			ArrayList<String> codecs;

			// Finds the supported color formats by encoders of the phone.
			SparseArray<ArrayList<String>> list = findSupportedColorFormats("video/avc");

			if (list.get(yuvPlanar) == null && 
					list.get(yuvSemiPlanar) == null) {
				Log.e(TAG,"No color format supported !");
				// Fallback on the MediaReocrder API
				setMode(MODE_MEDIARECORDER_API);
				return;
			}

			// We don't want to use the sofware encoder OMX.google.h264.encoder because
			// it is not using the hardware capabilities of the phone
			try {
				codecs = (ArrayList<String>) list.get(yuvPlanar).clone();
				codecs.remove("OMX.google.h264.encoder");
				mEncoderColorFormat = yuvPlanar;
				mEncoderName = codecs.get(0);
			} catch (Exception e0) {

				try {
					codecs = (ArrayList<String>) list.get(yuvSemiPlanar).clone();
					codecs.remove("OMX.google.h264.encoder");
					mEncoderColorFormat = yuvSemiPlanar;
					mEncoderName = codecs.get(0);
				} catch (Exception e1) {

					// If not, we will use another one
					if (list.get(yuvPlanar) != null) {
						mEncoderColorFormat = yuvPlanar;
						mEncoderName = list.get(yuvPlanar).get(0);
					} else if (list.get(yuvSemiPlanar) != null) {
						mEncoderColorFormat = yuvSemiPlanar;
						mEncoderName = list.get(yuvSemiPlanar).get(0);
					}

				}

			}

			Log.v(TAG, "Selected encoder: "+mEncoderName);
			Log.v(TAG, "Selected clolor format: "+mEncoderColorFormat);

		}
	}

		boolean previewing = mCamera!=null && mCameraOpenedManually; 
		mCameraId = (mCameraId == CameraInfo.CAMERA_FACING_BACK) ? CameraInfo.CAMERA_FACING_FRONT : CameraInfo.CAMERA_FACING_BACK; 
		setCamera(mCameraId);
		stopPreview();
		mFlashEnabled = false;
		if (previewing) startPreview();
		if (streaming) start(); 
	}

	/**
	 * Returns the id of the camera currently selected. 
	 * Can be either {@link CameraInfo#CAMERA_FACING_BACK} or 
	 * {@link CameraInfo#CAMERA_FACING_FRONT}.
	 */
	public int getCamera() {
		return mCameraId;
	}

	/**
	 * Sets a Surface to show a preview of recorded media (video). 
	 * You can call this method at any time and changes will take effect next time you call {@link #start()}.
	 */
	public synchronized void setSurfaceView(SurfaceView view) {
		mSurfaceView = view;
		if (mSurfaceHolderCallback != null && mSurfaceView != null && mSurfaceView.getHolder() != null) {
			mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
		}
		if (mSurfaceView != null && mSurfaceView.getHolder() != null) {
			mSurfaceHolderCallback = new Callback() {
				@Override
				public void surfaceDestroyed(SurfaceHolder holder) {
					mSurfaceReady = false;
					stopPreview();
					Log.d(TAG,"Surface destroyed !");
				}
				@Override
				public void surfaceCreated(SurfaceHolder holder) {
					mSurfaceReady = true;
				}
				@Override
				public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
					Log.d(TAG,"Surface Changed !");
				}
			};
			mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
			mSurfaceReady = true;
		}
	}

	/** Turns the LED on or off if phone has one. */
	public synchronized void setFlashState(boolean state) {
		// If the camera has already been opened, we apply the change immediately
		if (mCamera != null) {

			if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
				lockCamera();
			}

			Parameters parameters = mCamera.getParameters();

			// We test if the phone has a flash
			if (parameters.getFlashMode()==null) {
				// The phone has no flash or the choosen camera can not toggle the flash
				throw new RuntimeException("Can't turn the flash on !");
			} else {
				parameters.setFlashMode(state?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
				try {
					mCamera.setParameters(parameters);
					mFlashEnabled = state;
				} catch (RuntimeException e) {
					mFlashEnabled = false;
					throw new RuntimeException("Can't turn the flash on !");
				} finally {
					if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
						unlockCamera();
					}
				}
			}
		} else {
			mFlashEnabled = state;
		}
	}

	/** 
	 * Toggles the LED of the phone if it has one.
	 * You can get the current state of the flash with {@link VideoStream#getFlashState()}.
	 */
	public synchronized void toggleFlash() {
		setFlashState(!mFlashEnabled);
	}

	/** Indicates whether or not the flash of the phone is on. */
	public boolean getFlashState() {
		return mFlashEnabled;
	}

	/** 
	 * Sets the orientation of the preview.
	 * @param orientation The orientation of the preview
	 */
	public void setPreviewOrientation(int orientation) {
		mRequestedOrientation = orientation;
		mUpdated = false;
	}
	
	/** 
	 * Sets the configuration of the stream. You can call this method at any time 
	 * and changes will take effect next time you call {@link #configure()}.
	 * @param videoQuality Quality of the stream
	 */
	public void setVideoQuality(VideoQuality videoQuality) {
		if (!mRequestedQuality.equals(videoQuality)) {
			mRequestedQuality = videoQuality.clone();
			mUpdated = false;
		}
	}

	/** 
	 * Returns the quality of the stream.  
	 */
	public VideoQuality getVideoQuality() {
		return mRequestedQuality;
	}

	/**
	 * Some data (SPS and PPS params) needs to be stored when {@link #getSessionDescription()} is called 
	 * @param prefs The SharedPreferences that will be used to save SPS and PPS parameters
	 */
	public void setPreferences(SharedPreferences prefs) {
		mSettings = prefs;
	}

	/**
	 * Configures the stream. You need to call this before calling {@link #getSessionDescription()} 
	 * to apply your configuration of the stream.
	 */
	public synchronized void configure() throws IllegalStateException, IOException {
		super.configure();
		mOrientation = mRequestedOrientation;
	}	
	
	/**
	 * Starts the stream.
	 * This will also open the camera and display the preview 
	 * if {@link #startPreview()} has not already been called.
	 */
	public synchronized void start() throws IllegalStateException, IOException {
		if (!mPreviewStarted) mCameraOpenedManually = false;
		super.start();
		Log.d(TAG,"Stream configuration: FPS: "+mQuality.framerate+" Width: "+mQuality.resX+" Height: "+mQuality.resY);
	}

	/** Stops the stream. */
	public synchronized void stop() {
		if (mCamera != null) {
			if (mMode == MODE_MEDIACODEC_API) {
				mCamera.setPreviewCallbackWithBuffer(null);
			}
			if (mMode == MODE_MEDIACODEC_API_2) {
				((SurfaceView)mSurfaceView).removeMediaCodecSurface();
			}
			super.stop();
			// We need to restart the preview
			if (!mCameraOpenedManually) {
				destroyCamera();
			} else {
				try {
					startPreview();
				} catch (RuntimeException e) {
					e.printStackTrace();
				}
			}
		}
	}

	public synchronized void startPreview() 
			throws CameraInUseException, 
			InvalidSurfaceException, 
			RuntimeException {
		
		mCameraOpenedManually = true;
		this.mVideoEncoder = videoEncoder;
	}

	/**
	 * Starts the stream.
	 * This will also open the camera and dispay the preview 
	/**
	 * Video encoding is done by a MediaRecorder.
	 */
	protected void encodeWithMediaRecorder() throws IOException, ConfNotSupportedException {

		Log.d(TAG,"Video encoded using the MediaRecorder API");

	/** Stops the stream. */
	public synchronized void stop() {
		if (mCamera != null) {
			if ((mMode&MODE_MEDIACODEC_API)!=0) {
				mCamera.setPreviewCallback(null);
			}
			super.stop();
			// We need to restart the preview
		try {
			mMediaRecorder = new MediaRecorder();
			mMediaRecorder.setCamera(mCamera);
			mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
			mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
			mMediaRecorder.setVideoEncoder(mVideoEncoder);
			mMediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface());
			mMediaRecorder.setVideoSize(mRequestedQuality.resX,mRequestedQuality.resY);
			mMediaRecorder.setVideoFrameRate(mRequestedQuality.framerate);

			// The bandwidth actually consumed is often above what was requested 
			mMediaRecorder.setVideoEncodingBitRate((int)(mRequestedQuality.bitrate*0.8));

			// We write the output of the camera in a local socket instead of a file !			
			// This one little trick makes streaming feasible quiet simply: data from the camera
			// can then be manipulated at the other end of the socket
			FileDescriptor fd = null;
			if (sPipeApi == PIPE_API_PFD) {
				fd = mParcelWrite.getFileDescriptor();
			} else  {
				fd = mSender.getFileDescriptor();
			}
			mMediaRecorder.setOutputFile(fd);

			mMediaRecorder.prepare();
			mMediaRecorder.start();

		} catch (Exception e) {
			throw new ConfNotSupportedException(e.getMessage());
		}

		InputStream is = null;

		if (sPipeApi == PIPE_API_PFD) {
			is = new ParcelFileDescriptor.AutoCloseInputStream(mParcelRead);
		} else  {
			is = mReceiver.getInputStream();
		}

		// This will skip the MPEG4 header if this step fails we can't stream anything :(
		try {
			byte buffer[] = new byte[4];
			// Skip all atoms preceding mdat atom
			while (!Thread.interrupted()) {
				while (is.read() != 'm');
				is.read(buffer,0,3);
				if (buffer[0] == 'd' && buffer[1] == 'a' && buffer[2] == 't') break;
			}
		} catch (IOException e) {
			Log.e(TAG,"Couldn't skip mp4 header :/");
			stop();
			throw e;
		}

		// The packetizer encapsulates the bit stream in an RTP stream and send it over the network
		mPacketizer.setInputStream(is);
		mPacketizer.start();

		mStreaming = true;

	}


	/**
		mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
		mMediaRecorder.setVideoSize(mQuality.resX,mQuality.resY);
		mMediaRecorder.setVideoFrameRate(mQuality.framerate);
		mMediaRecorder.setVideoEncodingBitRate(mQuality.bitrate);

		// We write the ouput of the camera in a local socket instead of a file !			
		// This one little trick makes streaming feasible quiet simply: data from the camera
		}
	}	

	/**
	 * Video encoding is done by a MediaCodec.
	 */
	@SuppressLint("NewApi")
	protected void encodeWithMediaCodecMethod1() throws RuntimeException, IOException {

		Log.d(TAG,"Video encoded using the MediaCodec API with a buffer");

		// Updates the parameters of the camera if needed
		createCamera();
		updateCamera();

		// Estimates the frame rate of the camera
		measureFramerate();

		// Starts the preview if needed
		if (!mPreviewStarted) {
			try {
				mCamera.startPreview();
				mPreviewStarted = true;
			} catch (RuntimeException e) {
				destroyCamera();
				throw e;
			}
		}

		EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY);
		final NV21Convertor convertor = debugger.getNV21Convertor();

		mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
		MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mQuality.resX, mQuality.resY);
		mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitrate);
		mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mQuality.framerate);	
		mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,debugger.getEncoderColorFormat());
		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
		mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
		mMediaCodec.start();

		Camera.PreviewCallback callback = new Camera.PreviewCallback() {
			long now = System.nanoTime()/1000, oldnow = now, i=0;
			ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
			@Override
			public void onPreviewFrame(byte[] data, Camera camera) {
				oldnow = now;
				now = System.nanoTime()/1000;
				if (i++>3) {
					i = 0;
					//Log.d(TAG,"Measured: "+1000000L/(now-oldnow)+" fps.");
				}
				try {
					int bufferIndex = mMediaCodec.dequeueInputBuffer(500000);
					if (bufferIndex>=0) {
						inputBuffers[bufferIndex].clear();
						if (data == null) Log.e(TAG,"Symptom of the \"Callback buffer was to small\" problem...");
						else convertor.convert(data, inputBuffers[bufferIndex]);
						mMediaCodec.queueInputBuffer(bufferIndex, 0, inputBuffers[bufferIndex].position(), now, 0);
					} else {
						Log.e(TAG,"No buffer available !");
					}
				} finally {
					mCamera.addCallbackBuffer(data);
				}				
			}
			}
		}

		final ColorFormatTranslator convertor = new ColorFormatTranslator(mCameraImageFormat,mEncoderColorFormat,mQuality.resX,mQuality.resY);
		
		mMediaCodec = MediaCodec.createByCodecName(mEncoderName);
		MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mQuality.resX, mQuality.resY);
		mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitrate);
		mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mActualFramerate!=0?mActualFramerate:mQuality.framerate);	
		mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mEncoderColorFormat);
		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 4);
		mediaFormat.setInteger("stride", convertor.getYStride());
		mediaFormat.setInteger("slice-height", convertor.getUVStride());
		mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
		mMediaCodec.start();

		final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();

		for (int i=0;i<10;i++) mCamera.addCallbackBuffer(new byte[convertor.getBufferSize()]);

		mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
			private long now = System.nanoTime()/1000, oldnow = 0;
			private int bufferIndex;
			@Override
			public void onPreviewFrame(byte[] data, Camera camera) {
				oldnow = now;
				now = System.nanoTime()/1000;
				//Log.d(TAG,"Measured: "+1000000L/(now-oldnow)+" fps.");
				try {
					bufferIndex = mMediaCodec.dequeueInputBuffer(0);
					if (bufferIndex>=0) {
						byte[] buffer = convertor.translate(data);
						int min = inputBuffers[bufferIndex].capacity()<buffer.length?inputBuffers[bufferIndex].capacity():buffer.length;
						inputBuffers[bufferIndex].clear();
						inputBuffers[bufferIndex].put(buffer, 0, min);
						Log.d(TAG, "Encoder: "+inputBuffers[bufferIndex].capacity()+" Camera: "+buffer.length+" Cv: "+convertor.getBufferSize());
						mMediaCodec.queueInputBuffer(bufferIndex, 0, min, now, 0);
					} else {
						Log.e(TAG,"No buffer available !");
					}
				} catch (IllegalStateException e) {
				} finally {
					mCamera.addCallbackBuffer(data);
				}
			}
		});

		try {
			// mReceiver.getInputStream contains the data from the camera
	@SuppressLint({ "InlinedApi", "NewApi" })	
	protected void encodeWithMediaCodecMethod2() throws RuntimeException, IOException {

		Log.d(TAG,"Video encoded using the MediaCodec API with a surface");

		// Updates the parameters of the camera if needed
		createCamera();
		updateCamera();

		// Estimates the frame rate of the camera
		measureFramerate();

		EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY);

		mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
		MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mQuality.resX, mQuality.resY);
		mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitrate);
		mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mQuality.framerate);	
		mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
		mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
		Surface surface = mMediaCodec.createInputSurface();
		((SurfaceView)mSurfaceView).addMediaCodecSurface(surface);
		mMediaCodec.start();

		// The packetizer encapsulates the bit stream in an RTP stream and send it over the network
		mPacketizer.setInputStream(new MediaCodecInputStream(mMediaCodec));
		mPacketizer.start();

		mStreaming = true;

	}

	/**
	 * Returns a description of the stream using SDP. 
	 * This method can only be called after {@link Stream#configure()}.
	 * @throws IllegalStateException Thrown when {@link Stream#configure()} wa not called.
	 */	
	public abstract String getSessionDescription() throws IllegalStateException;

	/**
	 * Opens the camera in a new Looper thread so that the preview callback is not called from the main thread
	 * If an exception is thrown in this Looper thread, we bring it back into the main thread.
				}
			});

			Parameters parameters = mCamera.getParameters();

			if (mFlashState) {
				if (parameters.getFlashMode()==null) {
					// The phone has no flash or the choosen camera can not toggle the flash
					throw new IllegalStateException("Can't turn the flash on !");
				} else {
					parameters.setFlashMode(mFlashState?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
				}
			}

			try {
				mCamera.setParameters(parameters);
				mCamera.setDisplayOrientation(mQuality.orientation);
				mCamera.setPreviewDisplay(mSurfaceHolder);
			} catch (RuntimeException e) {
				destroyCamera();
				throw e;
			throw new InvalidSurfaceException("Invalid surface !");

		if (mCamera == null) {
			openCamera();
			mUpdated = false;
			mUnlocked = false;
			mCamera.setErrorCallback(new Camera.ErrorCallback() {
				@Override
				public void onError(int error, Camera camera) {
					// On some phones when trying to use the camera facing front the media server will die
					// Whether or not this callback may be called really depends on the phone
					if (error == Camera.CAMERA_ERROR_SERVER_DIED) {
						// In this case the application must release the camera and instantiate a new one
						Log.e(TAG,"Media server died !");
						// We don't know in what thread we are so stop needs to be synchronized
						mCameraOpenedManually = false;
						stop();
					} else {
						Log.e(TAG,"Error unknown with the camera: "+error);
					}	
				}
			});

			try {

				// If the phone has a flash, we turn it on/off according to mFlashEnabled
				mPreviewStarted = false;
				mCamera.stopPreview();
			}
			Parameters parameters = mCamera.getParameters();
			parameters.setPreviewFormat(mCameraImageFormat);
			determineClosestSupportedResolution(parameters);
			parameters.setPreviewSize(mQuality.resX, mQuality.resY);
			determineClosestSupportedFramerate(parameters);
			//parameters.setPreviewFpsRange((mQuality.framerate-10)*1000,(mQuality.framerate+10)*1000);
			parameters.setPreviewFrameRate(mQuality.framerate);
			try {
				mCamera.setParameters(parameters);
				mCamera.setDisplayOrientation(mQuality.orientation);
				mCamera.setPreviewDisplay(mSurfaceHolder);
				mCamera.startPreview();
				mPreviewStarted = false;
			} catch (RuntimeException e) {
				destroyCamera();
				throw e;
				mCamera.release();
			} catch (Exception e) {
				Log.e(TAG,e.getMessage()!=null?e.getMessage():"unknown error");
			}
			mCamera = null;
			mCameraLooper.quit();
			mUnlocked = false;
			mPreviewStarted = false;
		}	
	}

	protected synchronized void updateCamera() throws RuntimeException {
		
		// The camera is already correctly configured
		if (mUpdated) return;
		
		if (mPreviewStarted) {
			mPreviewStarted = false;
			mCamera.stopPreview();
		}

		Parameters parameters = mCamera.getParameters();
		mQuality = VideoQuality.determineClosestSupportedResolution(parameters, mQuality);
		int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
		
		double ratio = (double)mQuality.resX/(double)mQuality.resY;
		mSurfaceView.requestAspectRatio(ratio);
		
		parameters.setPreviewFormat(mCameraImageFormat);
		parameters.setPreviewSize(mQuality.resX, mQuality.resY);
		parameters.setPreviewFpsRange(max[0], max[1]);

		try {
			mCamera.setParameters(parameters);
			mCamera.setDisplayOrientation(mOrientation);
			mCamera.startPreview();
			mPreviewStarted = true;
			mUpdated = true;
		} catch (RuntimeException e) {
			destroyCamera();
			throw e;
		}
	}

	protected void lockCamera() {
		if (mUnlocked) {
			Log.d(TAG,"Locking camera");
			try {
				mCamera.reconnect();
		return list;
	}

	/** 
	 * Checks if the requested resolution is supported by the camera.
	 * If not, it modifies it by supported parameters. 
	 **/
	protected void determineClosestSupportedResolution(Camera.Parameters parameters) {
		int minDist = Integer.MAX_VALUE, newResX = mQuality.resX, newResY = mQuality.resY;
		String supportedSizesStr = "Supported resolutions: ";
		List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
		for (Iterator<Size> it = supportedSizes.iterator(); it.hasNext();) {
			Size size = it.next();
			supportedSizesStr += size.width+"x"+size.height+(it.hasNext()?", ":"");
			int dist = Math.abs(mQuality.resX - size.width);
			if (dist<minDist) {
				minDist = dist;
				newResX = size.width;
				newResY = size.height;
			}
		}
		Log.v(TAG,supportedSizesStr);
		mQuality.resX = newResX;
		mQuality.resY = newResY;
	}

	/** 
	 * Checks if the framerate is supported by the camera.
	 * If not, it modifies it by supported parameters. 
	 **/	
	protected void determineClosestSupportedFramerate(Camera.Parameters parameters) {
		// Frame rates
		String supportedFrameRatesStr = "Supported frame rates: ";
		List<Integer> supportedFrameRates = parameters.getSupportedPreviewFrameRates();
		for (Iterator<Integer> it = supportedFrameRates.iterator(); it.hasNext();) {
			supportedFrameRatesStr += it.next()+"fps"+(it.hasNext()?", ":"");
		}
		Log.v(TAG,supportedFrameRatesStr);

		// Frame rates
		String supportedFpsRangesStr = "Supported frame rates: ";
		int maxFps = 0;
		List<int[]> supportedFpsRanges = parameters.getSupportedPreviewFpsRange();
		for (Iterator<int[]> it = supportedFpsRanges.iterator(); it.hasNext();) {
			int[] interval = it.next();
			supportedFpsRangesStr += interval[0]+"-"+interval[1]+"fps"+(it.hasNext()?", ":"");
			if (interval[1]/1000>maxFps) maxFps = interval[1]/1000; 
		}
		this.mMaxFps = maxFps;
		Log.v(TAG,supportedFpsRangesStr);

		int minDist = Integer.MAX_VALUE, newFps = mQuality.framerate;
		if (!supportedFrameRates.contains(mQuality.framerate)) {
			for (Iterator<Integer> it = supportedFrameRates.iterator(); it.hasNext();) {
				int fps = it.next();
				int dist = Math.abs(fps - mQuality.framerate);
				if (dist<minDist) {
					minDist = dist;
					newFps = fps;
				}
			}
			Log.v(TAG,"Frame rate modified: "+mQuality.framerate+"->"+newFps);
			mQuality.framerate = newFps;
		}
	}

	protected void lockCamera() {
		if (mUnlocked) {
			Log.d(TAG,"Locking camera");

	protected void unlockCamera() {
		if (!mUnlocked) {
			Log.d(TAG,"Unlocking camera");
			try {	
				mCamera.unlock();
			} catch (Exception e) {
				Log.e(TAG,e.getMessage());
			}
			mUnlocked = true;
		}
	}


	/**
	 * Computes the average frame rate at which the preview callback is called.
	 * We will then use this average frame rate with the MediaCodec.  
	 * Blocks the thread in which this function is called.
		}
	}

}

<MSG> Lots of improvements, streaming H264 with the media codec API works all right on my phones at 352x288.

<DFF> @@ -27,10 +27,13 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.Semaphore;
 
 import net.majorkernelpanic.streaming.MediaStream;
 import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream;
 import android.annotation.SuppressLint;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
 import android.graphics.ImageFormat;
 import android.hardware.Camera;
 import android.hardware.Camera.CameraInfo;
@@ -56,25 +59,26 @@ public abstract class VideoStream extends MediaStream {
 	private static HashMap<String,SparseArray<ArrayList<String>>> sSupportedColorFormats = new HashMap<String, SparseArray<ArrayList<String>>>(); 
 
 	protected VideoQuality mQuality = VideoQuality.DEFAULT_VIDEO_QUALITY.clone();
+	protected VideoQuality mActualQuality = mQuality.clone(); 
 	protected SurfaceHolder.Callback mSurfaceHolderCallback = null;
 	protected SurfaceHolder mSurfaceHolder = null;
+	protected SharedPreferences mSettings = null;
 	protected int mVideoEncoder, mCameraId = 0;
 	protected Camera mCamera;
+	protected CameraFifo mCameraFifo;
+
+
 	protected boolean mCameraOpenedManually = true;
 	protected boolean mFlashState = false;
 	protected boolean mSurfaceReady = false;
 	protected boolean mUnlocked = false;
 	protected boolean mPreviewStarted = false;
 
-	protected int mEncoderColorFormat = 0;
-	protected String mEncoderName = null;
-	protected int mCameraImageFormat = 0;
-
-	// The FPS really achieved when asking for a certain FPS 
-	protected int mActualFramerate = 0;
-	// The FPS we need to use with the Camera API to obtain the desired FPS
-	protected int mCorrectedFramerate = 0;
-	// The maximum possible FPS, depends on the resolution and on the Camera
+	protected CodecManager.Codecs mCodecs;
+	protected String mMimeType;
+	protected String mEncoderName;
+	protected int mEncoderColorFormat;
+	protected int mCameraImageFormat;
 	protected int mMaxFps = 0;	
 
 	/** 
@@ -92,56 +96,33 @@ public abstract class VideoStream extends MediaStream {
 	@SuppressLint("InlinedApi")
 	public VideoStream(int camera) {
 		super();
-		mCameraImageFormat = ImageFormat.YV12;
 		setCamera(camera);
-		if ((sSuggestedMode&MODE_MEDIACODEC_API)!=0) {
-			int yuvPlanar = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
-			int yuvSemiPlanar = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
-			ArrayList<String> codecs;
-
-			// Finds the supported color formats by encoders of the phone.
-			SparseArray<ArrayList<String>> list = findSupportedColorFormats("video/avc");
-
-			if (list.get(yuvPlanar) == null && 
-					list.get(yuvSemiPlanar) == null) {
-				Log.e(TAG,"No color format supported !");
-				// Fallback on the MediaReocrder API
-				setMode(MODE_MEDIARECORDER_API);
-				return;
-			}
+	}
 
-			// We don't want to use the sofware encoder OMX.google.h264.encoder because
-			// it is not using the hardware capabilities of the phone
-			try {
-				codecs = (ArrayList<String>) list.get(yuvPlanar).clone();
-				codecs.remove("OMX.google.h264.encoder");
-				mEncoderColorFormat = yuvPlanar;
-				mEncoderName = codecs.get(0);
-			} catch (Exception e0) {
+	/**
+	 * Called by subclasses. 
+	 */
+	protected void init(String mimeType) {
+		mCameraImageFormat = ImageFormat.NV21;
+		mMimeType = mimeType;
 
-				try {
-					codecs = (ArrayList<String>) list.get(yuvSemiPlanar).clone();
-					codecs.remove("OMX.google.h264.encoder");
-					mEncoderColorFormat = yuvSemiPlanar;
-					mEncoderName = codecs.get(0);
-				} catch (Exception e1) {
-
-					// If not, we will use another one
-					if (list.get(yuvPlanar) != null) {
-						mEncoderColorFormat = yuvPlanar;
-						mEncoderName = list.get(yuvPlanar).get(0);
-					} else if (list.get(yuvSemiPlanar) != null) {
-						mEncoderColorFormat = yuvSemiPlanar;
-						mEncoderName = list.get(yuvSemiPlanar).get(0);
-					}
+		if (sSuggestedMode == MODE_MEDIACODEC_API) {
+			mCodecs = CodecManager.Selector.findCodecsFormMimeType(mMimeType, false);
 
-				}
+			if (mCodecs.hardwareCodec == null && mCodecs.softwareCodec == null ) {
+				mMode = MODE_MEDIARECORDER_API;
+				Log.e(TAG,"No encoder can be used on this phone, we will use the MediaRecorder API");
 
+			} else {
+				if (mCodecs.hardwareCodec != null) {
+					mEncoderColorFormat = mCodecs.hardwareColorFormat;
+					mEncoderName = mCodecs.hardwareCodec;
+				} else {
+					mEncoderColorFormat = mCodecs.softwareColorFormat;
+					mEncoderName = mCodecs.softwareCodec;	
+				}
 			}
 
-			Log.v(TAG, "Selected encoder: "+mEncoderName);
-			Log.v(TAG, "Selected clolor format: "+mEncoderColorFormat);
-
 		}
 	}
 
@@ -329,6 +310,14 @@ public abstract class VideoStream extends MediaStream {
 		this.mVideoEncoder = videoEncoder;
 	}
 
+	/**
+	 * Some data (SPS and PPS params) needs to be stored when {@link #generateSessionDescription()} is called 
+	 * @param prefs The SharedPreferences that will be used to save SPS and PPS parameters
+	 */
+	public void setPreferences(SharedPreferences prefs) {
+		mSettings = prefs;
+	}	
+
 	/**
 	 * Starts the stream.
 	 * This will also open the camera and dispay the preview 
@@ -342,8 +331,10 @@ public abstract class VideoStream extends MediaStream {
 	/** Stops the stream. */
 	public synchronized void stop() {
 		if (mCamera != null) {
-			if ((mMode&MODE_MEDIACODEC_API)!=0) {
-				mCamera.setPreviewCallback(null);
+			if (mMode == MODE_MEDIACODEC_API) {
+				if (mCameraFifo != null) {
+					mCameraFifo.stop();
+				}
 			}
 			super.stop();
 			// We need to restart the preview
@@ -414,7 +405,9 @@ public abstract class VideoStream extends MediaStream {
 		mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
 		mMediaRecorder.setVideoSize(mQuality.resX,mQuality.resY);
 		mMediaRecorder.setVideoFrameRate(mQuality.framerate);
-		mMediaRecorder.setVideoEncodingBitRate(mQuality.bitrate);
+
+		// The bandwidth actually consumed is often above what was requested 
+		mMediaRecorder.setVideoEncodingBitRate((int)(mQuality.bitrate*0.8));
 
 		// We write the ouput of the camera in a local socket instead of a file !			
 		// This one little trick makes streaming feasible quiet simply: data from the camera
@@ -487,49 +480,17 @@ public abstract class VideoStream extends MediaStream {
 			}
 		}
 
-		final ColorFormatTranslator convertor = new ColorFormatTranslator(mCameraImageFormat,mEncoderColorFormat,mQuality.resX,mQuality.resY);
-		
 		mMediaCodec = MediaCodec.createByCodecName(mEncoderName);
-		MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mQuality.resX, mQuality.resY);
-		mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mQuality.bitrate);
-		mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mActualFramerate!=0?mActualFramerate:mQuality.framerate);	
+		MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mActualQuality.resX, mActualQuality.resY);
+		mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mActualQuality.bitrate);
+		mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mActualQuality.framerate);	
 		mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mEncoderColorFormat);
-		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 4);
-		mediaFormat.setInteger("stride", convertor.getYStride());
-		mediaFormat.setInteger("slice-height", convertor.getUVStride());
+		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
 		mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 		mMediaCodec.start();
 
-		final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
-
-		for (int i=0;i<10;i++) mCamera.addCallbackBuffer(new byte[convertor.getBufferSize()]);
-
-		mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
-			private long now = System.nanoTime()/1000, oldnow = 0;
-			private int bufferIndex;
-			@Override
-			public void onPreviewFrame(byte[] data, Camera camera) {
-				oldnow = now;
-				now = System.nanoTime()/1000;
-				//Log.d(TAG,"Measured: "+1000000L/(now-oldnow)+" fps.");
-				try {
-					bufferIndex = mMediaCodec.dequeueInputBuffer(0);
-					if (bufferIndex>=0) {
-						byte[] buffer = convertor.translate(data);
-						int min = inputBuffers[bufferIndex].capacity()<buffer.length?inputBuffers[bufferIndex].capacity():buffer.length;
-						inputBuffers[bufferIndex].clear();
-						inputBuffers[bufferIndex].put(buffer, 0, min);
-						Log.d(TAG, "Encoder: "+inputBuffers[bufferIndex].capacity()+" Camera: "+buffer.length+" Cv: "+convertor.getBufferSize());
-						mMediaCodec.queueInputBuffer(bufferIndex, 0, min, now, 0);
-					} else {
-						Log.e(TAG,"No buffer available !");
-					}
-				} catch (IllegalStateException e) {
-				} finally {
-					mCamera.addCallbackBuffer(data);
-				}
-			}
-		});
+		mCameraFifo = new CameraFifo();
+		mCameraFifo.start();
 
 		try {
 			// mReceiver.getInputStream contains the data from the camera
@@ -579,21 +540,29 @@ public abstract class VideoStream extends MediaStream {
 				}
 			});
 
-			Parameters parameters = mCamera.getParameters();
 
-			if (mFlashState) {
-				if (parameters.getFlashMode()==null) {
-					// The phone has no flash or the choosen camera can not toggle the flash
-					throw new IllegalStateException("Can't turn the flash on !");
-				} else {
-					parameters.setFlashMode(mFlashState?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
+			try {
+				Parameters parameters = mCamera.getParameters();
+
+				if (mFlashState) {
+					if (parameters.getFlashMode()==null) {
+						// The phone has no flash or the choosen camera can not toggle the flash
+						throw new IllegalStateException("Can't turn the flash on !");
+					} else {
+						parameters.setFlashMode(mFlashState?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
+					}
 				}
-			}
 
-			try {
+				int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
+				VideoQuality quality = new VideoQuality(320,240);
+				quality = VideoQuality.determineClosestSupportedResolution(parameters, mActualQuality);
+				
+				parameters.setPreviewFpsRange(max[0], max[1]);
+				parameters.setPreviewSize(quality.resX, quality.resY);
 				mCamera.setParameters(parameters);
 				mCamera.setDisplayOrientation(mQuality.orientation);
 				mCamera.setPreviewDisplay(mSurfaceHolder);
+
 			} catch (RuntimeException e) {
 				destroyCamera();
 				throw e;
@@ -626,19 +595,34 @@ public abstract class VideoStream extends MediaStream {
 				mPreviewStarted = false;
 				mCamera.stopPreview();
 			}
+
 			Parameters parameters = mCamera.getParameters();
+			
+			mActualQuality.resX = mQuality.resX;
+			mActualQuality.resY = mQuality.resY;
+			mActualQuality.bitrate = mQuality.bitrate;
+
+			// Hack needed for now because there are weird artefacts at lower resolutions (when resX or resY os not a multiple of 16)
+			if (mActualQuality.resX<352) {
+				mActualQuality.resX = 352;
+				mActualQuality.resY = 288;
+			}
+
+			mActualQuality = VideoQuality.determineClosestSupportedResolution(parameters, mActualQuality);
+			int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
+			
 			parameters.setPreviewFormat(mCameraImageFormat);
-			determineClosestSupportedResolution(parameters);
-			parameters.setPreviewSize(mQuality.resX, mQuality.resY);
-			determineClosestSupportedFramerate(parameters);
-			//parameters.setPreviewFpsRange((mQuality.framerate-10)*1000,(mQuality.framerate+10)*1000);
-			parameters.setPreviewFrameRate(mQuality.framerate);
+			parameters.setPreviewSize(mActualQuality.resX, mActualQuality.resY);
+			parameters.setPreviewFpsRange(max[0], max[1]);
+			
 			try {
+				Log.e(TAG,"FPS: "+mActualQuality.framerate+" X: "+mActualQuality.resX+" Y: "+mActualQuality.resY);
 				mCamera.setParameters(parameters);
 				mCamera.setDisplayOrientation(mQuality.orientation);
 				mCamera.setPreviewDisplay(mSurfaceHolder);
 				mCamera.startPreview();
-				mPreviewStarted = false;
+				mPreviewStarted = true;
+				measureActualFramerate();
 			} catch (RuntimeException e) {
 				destroyCamera();
 				throw e;
@@ -694,69 +678,6 @@ public abstract class VideoStream extends MediaStream {
 		return list;
 	}
 
-	/** 
-	 * Checks if the requested resolution is supported by the camera.
-	 * If not, it modifies it by supported parameters. 
-	 **/
-	protected void determineClosestSupportedResolution(Camera.Parameters parameters) {
-		int minDist = Integer.MAX_VALUE, newResX = mQuality.resX, newResY = mQuality.resY;
-		String supportedSizesStr = "Supported resolutions: ";
-		List<Size> supportedSizes = parameters.getSupportedPreviewSizes();
-		for (Iterator<Size> it = supportedSizes.iterator(); it.hasNext();) {
-			Size size = it.next();
-			supportedSizesStr += size.width+"x"+size.height+(it.hasNext()?", ":"");
-			int dist = Math.abs(mQuality.resX - size.width);
-			if (dist<minDist) {
-				minDist = dist;
-				newResX = size.width;
-				newResY = size.height;
-			}
-		}
-		Log.v(TAG,supportedSizesStr);
-		mQuality.resX = newResX;
-		mQuality.resY = newResY;
-	}
-
-	/** 
-	 * Checks if the framerate is supported by the camera.
-	 * If not, it modifies it by supported parameters. 
-	 **/	
-	protected void determineClosestSupportedFramerate(Camera.Parameters parameters) {
-		// Frame rates
-		String supportedFrameRatesStr = "Supported frame rates: ";
-		List<Integer> supportedFrameRates = parameters.getSupportedPreviewFrameRates();
-		for (Iterator<Integer> it = supportedFrameRates.iterator(); it.hasNext();) {
-			supportedFrameRatesStr += it.next()+"fps"+(it.hasNext()?", ":"");
-		}
-		Log.v(TAG,supportedFrameRatesStr);
-
-		// Frame rates
-		String supportedFpsRangesStr = "Supported frame rates: ";
-		int maxFps = 0;
-		List<int[]> supportedFpsRanges = parameters.getSupportedPreviewFpsRange();
-		for (Iterator<int[]> it = supportedFpsRanges.iterator(); it.hasNext();) {
-			int[] interval = it.next();
-			supportedFpsRangesStr += interval[0]+"-"+interval[1]+"fps"+(it.hasNext()?", ":"");
-			if (interval[1]/1000>maxFps) maxFps = interval[1]/1000; 
-		}
-		this.mMaxFps = maxFps;
-		Log.v(TAG,supportedFpsRangesStr);
-
-		int minDist = Integer.MAX_VALUE, newFps = mQuality.framerate;
-		if (!supportedFrameRates.contains(mQuality.framerate)) {
-			for (Iterator<Integer> it = supportedFrameRates.iterator(); it.hasNext();) {
-				int fps = it.next();
-				int dist = Math.abs(fps - mQuality.framerate);
-				if (dist<minDist) {
-					minDist = dist;
-					newFps = fps;
-				}
-			}
-			Log.v(TAG,"Frame rate modified: "+mQuality.framerate+"->"+newFps);
-			mQuality.framerate = newFps;
-		}
-	}
-
 	protected void lockCamera() {
 		if (mUnlocked) {
 			Log.d(TAG,"Locking camera");
@@ -781,4 +702,171 @@ public abstract class VideoStream extends MediaStream {
 		}
 	}
 
+
+	/**
+	 * Computes the average frame rate at which the preview callback is called.
+	 * We will then use this average framerate with the MediaCodec.  
+	 * Blocks the thread in which this function is called.
+	 */
+	private void measureActualFramerate() {
+
+		if (mSettings != null) {
+			String key = PREF_PREFIX+"fps"+mQuality.framerate+","+mCameraImageFormat+","+mQuality.resX+mQuality.resY;
+			if (mSettings.contains(key)) {
+				mActualQuality.framerate = mSettings.getInt(key, 0);
+				Log.d(TAG,"Actual framerate: "+mActualQuality.framerate);
+				return;
+			}
+		}
+		
+		final Semaphore lock = new Semaphore(0);
+
+		final Camera.PreviewCallback callback = new Camera.PreviewCallback() {
+			int i = 0, t = 0;
+			long now, oldnow, count = 0;
+			@Override
+			public void onPreviewFrame(byte[] data, Camera camera) {
+				i++;
+				now = System.nanoTime()/1000;
+				if (i>3) {
+					t += now - oldnow;
+					count++;
+				}
+				if (i>15) {
+					mActualQuality.framerate = (int) (1000000/(t/count)+1);
+					Log.d(TAG,"Actual framerate: "+mActualQuality.framerate);
+					lock.release();
+				}
+				oldnow = now;
+			}
+		};
+
+		mCamera.setPreviewCallback(callback);
+
+		try {
+			lock.acquire();
+		} catch (InterruptedException e) {}
+
+		mCamera.setPreviewCallback(null);
+
+		if (mSettings != null) {
+			Editor editor = mSettings.edit();
+			editor.putInt(PREF_PREFIX+"fps"+mQuality.framerate+","+mCameraImageFormat+","+mQuality.resX+mQuality.resY, mActualQuality.framerate);
+			editor.commit();
+		}
+
+	}	
+
+	private class CameraFifo {
+
+		private final int MAX = 10;
+
+		private CodecManager.NV21Translator mConvertor = null;
+		private byte[][] mBuffers = new byte[MAX][];
+		private long[] mTimestamps = new long[MAX];
+		private Thread mThread = null;
+		private int mHead, mTail;
+		private Semaphore mAvailable;
+		private Semaphore mRemaining;
+		private boolean mRunning; 
+
+		public void start() {
+			if (mThread == null) {
+
+				mHead = 0;
+				mTail = 0;
+				mAvailable = new Semaphore(0);
+				mRemaining = new Semaphore(MAX);
+
+				mRunning = true;
+				mThread = new Thread(mRunnable);
+				mThread.start();
+
+				mConvertor = new CodecManager.NV21Translator(mEncoderName, mEncoderColorFormat, mActualQuality.resX, mActualQuality.resY);
+				for (int i=0;i<MAX;i++) mCamera.addCallbackBuffer(new byte[mConvertor.getBufferSize()]);
+				mCamera.setPreviewCallbackWithBuffer(mCameraCallback);
+
+			}
+		} 
+
+		public void stop() {
+			if (mThread != null) {
+				mCamera.setPreviewCallback(null);
+				mRunning = false;
+				mThread.interrupt();
+				try {
+					mThread.join();
+				} catch (InterruptedException e) {}
+				mThread = null;
+			}
+		}
+
+		private void queue(byte[] buffer, long ts) {
+			try {
+				mRemaining.acquire();
+			} catch (InterruptedException e) {}
+			mBuffers[mHead] = buffer;
+			mTimestamps[mHead] = ts;
+			mHead++;
+			if (mHead>=MAX) mHead = 0;
+			mAvailable.release();
+		}
+
+		/**
+		 * This will be called from the main thread, doing the color format conversion and feeding the encoder 
+		 * from there was too long, that is why this FIFO is needed
+		 */
+		Camera.PreviewCallback mCameraCallback = new Camera.PreviewCallback() {
+			private long now = System.nanoTime()/1000, oldnow = now;
+			private int i=0;
+			@Override
+			public void onPreviewFrame(byte[] data, Camera camera) {
+				now = System.nanoTime()/1000;
+				if (i++>2) {
+					i = 0;
+					//Log.d(TAG,"Measured: "+1000000L/(now-oldnow)+" fps.");
+				}
+				oldnow = now;
+				queue(data, now);
+			}
+		};
+
+		/**
+		 * Thread in which we do the conversion between color formats and we feed the MediaCodec
+		 */
+		private Runnable mRunnable = new Runnable() {
+			@SuppressLint("NewApi")
+			@Override
+			public void run() {
+				ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
+
+				while (mRunning && !Thread.interrupted()) {
+					//Log.d(TAG, "a: "+mRemaining.availablePermits());
+					try {
+						mAvailable.acquire();
+					} catch (InterruptedException e) {
+						break;
+					}
+
+					try {
+						int bufferIndex = mMediaCodec.dequeueInputBuffer(500000);
+						if (bufferIndex>=0) {
+							mConvertor.translate(mBuffers[mTail], inputBuffers[bufferIndex]);
+							mMediaCodec.queueInputBuffer(bufferIndex, 0, inputBuffers[bufferIndex].position(), mTimestamps[mTail], 0);
+						} else {
+							Log.e(TAG,"No buffer available !");
+						}
+					} finally {
+						mCamera.addCallbackBuffer(mBuffers[mTail]);
+						mTail++;
+						if (mTail>=MAX) mTail = 0;
+						mRemaining.release();
+					}
+
+				}
+			}
+		};
+
+	}
+
 }

Lots of improvements, streaming H264 with the media codec API works all right on my phones at 352x288.