<NME> VideoStream.java
<BEF> /*
 * Copyright (C) 2011-2013 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
 * 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.io.InputStream;
import java.nio.ByteBuffer;
import java.util.concurrent.Semaphore;

import net.majorkernelpanic.streaming.MediaStream;
import net.majorkernelpanic.streaming.gl.SurfaceView;
import net.majorkernelpanic.streaming.hw.EncoderDebugger;
import net.majorkernelpanic.streaming.hw.NV21Convertor;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.hardware.Camera;
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";

	protected VideoQuality mRequestedQuality = VideoQuality.DEFAULT_VIDEO_QUALITY.clone();
	protected VideoQuality mQuality = mRequestedQuality.clone(); 
	protected SurfaceHolder.Callback mSurfaceHolderCallback = null;
	protected SurfaceView mSurfaceView = null;
	protected SharedPreferences mSettings = null;
	protected int mVideoEncoder, mCameraId = 0;
	protected int mRequestedOrientation = 0, mOrientation = 0;
	protected Camera mCamera;
	protected Thread mCameraThread;
	protected Looper mCameraLooper;

	protected boolean mCameraOpenedManually = true;
	protected boolean mFlashState = false;
	protected boolean mSurfaceReady = false;
	protected boolean mUnlocked = false;
	protected boolean mPreviewStarted = false;
	protected boolean mUpdated = false;
	
	protected String mMimeType;
	protected String mEncoderName;
	protected int mEncoderColorFormat;
	protected int mCameraImageFormat;
	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")
		super();
		setCamera(camera);
	}
	
	/**
	 * Sets the camera that will be used to capture video.
	 * You can call this method at any time and changes will take effect next time you start the stream.
	 * You can call this method at any time and changes will take effect next time you start the stream.
	 * @param camera Can be either CameraInfo.CAMERA_FACING_BACK or CameraInfo.CAMERA_FACING_FRONT
	 */
	public void setCamera(int camera) {
		CameraInfo cameraInfo = new CameraInfo();
		int numberOfCameras = Camera.getNumberOfCameras();
		for (int i=0;i<numberOfCameras;i++) {
			Camera.getCameraInfo(i, cameraInfo);
			if (cameraInfo.facing == camera) {
				mCameraId = i;
				break;
			}
		}
	}

	/**	Switch between the front facing and the back facing camera of the phone. 
	 * If {@link #startPreview()} has been called, the preview will be  briefly interrupted. 
	 * If {@link #start()} has been called, the stream will be  briefly interrupted.
	 * You should not call this method from the main thread if you are already streaming. 
	 * @throws IOException 
	 * @throws RuntimeException 
	 **/
	public void switchCamera() throws RuntimeException, IOException {
		if (Camera.getNumberOfCameras() == 1) throw new IllegalStateException("Phone only has one camera !");
		boolean streaming = mStreaming;
		mCameraId = (mCameraId == CameraInfo.CAMERA_FACING_BACK) ? CameraInfo.CAMERA_FACING_FRONT : CameraInfo.CAMERA_FACING_BACK; 
		setCamera(mCameraId);
		stopPreview();
		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 surfaceCreated(SurfaceHolder holder) {
					mSurfaceReady = true;
					try {
						startPreview();
					} catch (RuntimeException e) {
						e.printStackTrace();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
				@Override
				public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
				@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 !");

	/** Turns the LED on or off if phone has one. */
	public synchronized void setFlashState(boolean state) {

		// FIXME: Is it possible to toggle the flash while streaming on android 2.3 ?
		// FIXME: It works on android 4.2 and 4.3

		mFlashState = state;

		// If the camera has already been opened, we apply the change immediately
		// FIXME: Will this work on Android 2.3 ?
		if (mCamera != null) {

			// Needed on Android 2.3
			if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
				lockCamera();
			}
		// If the camera has already been opened, we apply the change immediately
		if (mCamera != null) {

			if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
				lockCamera();
				// 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(mFlashState?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
				try {
					mCamera.setParameters(parameters);
				} catch (RuntimeException e) {
					throw new RuntimeException("Can't turn the flash on !");	
				}
			}

			// Needed on Android 2.3
			if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
				unlockCamera();
			}

		}
	}

	/** Toggle the LED of the phone if it has one. */
	public void toggleFlash() {
		setFlashState(!mFlashState);
	}

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

	/** 
		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 VideoQuality getVideoQuality() {
		return mRequestedQuality;
	}	

	/**
	 * Some data (SPS and PPS params) needs to be stored when {@link #getSessionDescription()} is called 
	 * and changes will take effect next time you call {@link #configure()}.
	 */
	public void setPreferences(SharedPreferences prefs) {
		mSettings = prefs;
	}	

	/**
	 * Starts the stream.
	}

	/** 
	public synchronized void start() throws IllegalStateException, IOException {
		if (!mPreviewStarted) mCameraOpenedManually = false;
		super.start();
	}	

	/** Stops the stream. */
	public synchronized void stop() {
	 * @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;
					startPreview();
				} catch (RuntimeException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	public synchronized void startPreview() throws RuntimeException, IOException {
		if (!mPreviewStarted) {
			createCamera();
			try {
				mCamera.startPreview();
				mPreviewStarted = true;
				mCameraOpenedManually = true;
			} catch (RuntimeException e) {
				destroyCamera();
				throw e;
			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 {
		
		// Reopens the camera if needed
		destroyCamera();
		createCamera();
		
		// The camera must be unlocked before the MediaRecorder can use it
		unlockCamera();

		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 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
		// can then be manipulated at the other end of the socket
		mMediaRecorder.setOutputFile(mSender.getFileDescriptor());

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

		// This will skip the MPEG4 header if this step fails we can't stream anything :(
		InputStream is = mReceiver.getInputStream();
			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;
		mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
		mPacketizer.setInputStream(mReceiver.getInputStream());
		mPacketizer.start();
		
		mStreaming = true;

	}
			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()) {

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

		// Reopens the camera if needed
		createCamera();
		updateCamera();

		// Starts the preview if needed
		if (!mPreviewStarted) {
			try {

		mStreaming = true;

	}


	/**
	 * Video encoding is done by a MediaCodec.
	 */
	protected void encodeWithMediaCodec() throws RuntimeException, IOException {
		if (mMode == MODE_MEDIACODEC_API_2) {
			// Uses the method MediaCodec.createInputSurface to feed the encoder
			encodeWithMediaCodecMethod2();
		} else {
			// Uses dequeueInputBuffer to feed the encoder
			encodeWithMediaCodecMethod1();
		}
	}	

	/**
	 * 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);
		mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
		mPacketizer.setInputStream(new MediaCodecInputStream(mMediaCodec));
		mPacketizer.start();
		
		mStreaming = true;

	}
		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;

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

		// Reopens the camera if needed
		createCamera();
		updateCamera();

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

		mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
					}
				} finally {
					mCamera.addCallbackBuffer(data);
				}				
			}
		};
		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.setDestination(mDestination, mRtpPort, mRtcpPort);
		mPacketizer.setInputStream(new MediaCodecInputStream(mMediaCodec));
		mPacketizer.start();
		
		mStreaming = true;

	}

	public abstract String getSessionDescription() throws IllegalStateException;

	/**
		// Updates the parameters of the camera if needed
		createCamera();
		updateCamera();
	 */
	private void openCamera() throws RuntimeException {
		final Semaphore lock = new Semaphore(0);
		final RuntimeException[] exception = new RuntimeException[1]; 
		mCameraThread = new Thread(new Runnable() {
			@Override
			public void run() {
		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
		});
		mCameraThread.start();
		lock.acquireUninterruptibly();
		if (exception[0] != null) throw exception[0];
	}

	protected synchronized void createCamera() throws RuntimeException, IOException {
		if (mSurfaceView == null)
			throw new IllegalStateException("Invalid surface view !");
		if (mMode != MODE_MEDIACODEC_API_2 && (mSurfaceView.getHolder() == null || !mSurfaceReady)) 
			throw new IllegalStateException("Invalid surface view !");

		if (mCamera == null) {
			openCamera();
			mUnlocked = false;
	/**
	 * 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.
	 * @throws RuntimeException Might happen if another app is already using the camera.
	 */
	private void openCamera() throws RuntimeException {
		final Semaphore lock = new Semaphore(0);
		final RuntimeException[] exception = new RuntimeException[1];
		mCameraThread = new Thread(new Runnable() {
			@Override
			public void run() {
				Looper.prepare();
				mCameraLooper = Looper.myLooper();
				try {
					mCamera = Camera.open(mCameraId);
			});

			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);
					}
				}

				VideoQuality quality = new VideoQuality(352,288);
				quality = VideoQuality.determineClosestSupportedResolution(parameters, quality);
				int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
				
				parameters.setPreviewFpsRange(max[0], max[1]);
				parameters.setPreviewSize(quality.resX, quality.resY);
				parameters.setRecordingHint(true);
				mCamera.setParameters(parameters);
				mCamera.setDisplayOrientation(mQuality.orientation);
				
				if (mMode == MODE_MEDIACODEC_API_2) {
					mSurfaceView.startGLThread();
					mCamera.setPreviewTexture(mSurfaceView.getSurfaceTexture());
				} else {
					mCamera.setPreviewDisplay(mSurfaceView.getHolder());
				}
				
			} catch (RuntimeException e) {
				destroyCamera();
				throw e;
				}
			});

			try {

				// If the phone has a flash, we turn it on/off according to mFlashEnabled
				// setRecordingHint(true) is a very nice optimization if you plane to only use the Camera for recording
				Parameters parameters = mCamera.getParameters();
				if (parameters.getFlashMode()!=null) {
					parameters.setFlashMode(mFlashEnabled?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
				}
				parameters.setRecordingHint(true);
				mCamera.setParameters(parameters);
				mCamera.setDisplayOrientation(mOrientation);

				try {
					if (mMode == MODE_MEDIACODEC_API_2) {
						mSurfaceView.startGLThread();
						mCamera.setPreviewTexture(mSurfaceView.getSurfaceTexture());
					} else {
						mCamera.setPreviewDisplay(mSurfaceView.getHolder());
					}
				} catch (IOException e) {
					throw new InvalidSurfaceException("Invalid surface !");
				}

			} catch (RuntimeException e) {
				destroyCamera();
				throw e;
			}

		}
	}

			parameters.setPreviewFpsRange(max[0], max[1]);

			try {
				Log.e(TAG,"FPS: "+mQuality.framerate+" X: "+mQuality.resX+" Y: "+mQuality.resY);
				mCamera.setParameters(parameters);
				mCamera.setDisplayOrientation(mRequestedQuality.orientation);
				mCamera.startPreview();
				mPreviewStarted = true;
				measureActualFramerate();
			} catch (RuntimeException e) {
				destroyCamera();
				throw e;
			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);
	 * We will then use this average framerate with the MediaCodec.  
	 * Blocks the thread in which this function is called.
	 */
	private void measureActualFramerate() {
		final Semaphore lock = new Semaphore(0);

		if (mSettings != null) {
			String key = PREF_PREFIX+"fps-"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+","+mRequestedQuality.resY;
			if (mSettings.contains(key)) {
				mQuality.framerate = mSettings.getInt(key, 0);
				Log.d(TAG,"Actual framerate: "+mQuality.framerate);
				return;
			}
		}

		final Camera.PreviewCallback callback = new Camera.PreviewCallback() {
			int i = 0, t = 0;
			long now, oldnow, count = 0;
	protected void lockCamera() {
		if (mUnlocked) {
			Log.d(TAG,"Locking camera");
			try {
				mCamera.reconnect();
					t += now - oldnow;
					count++;
				}
				if (i>15) {
					mQuality.framerate = (int) (1000000/(t/count)+1);
					Log.d(TAG,"Actual framerate: "+mQuality.framerate);
					lock.release();
				}
				oldnow = now;
		if (!mUnlocked) {
			Log.d(TAG,"Unlocking camera");
			try {	
		mCamera.setPreviewCallback(callback);

		try {
			lock.acquire();
		} catch (InterruptedException e) {}

		mCamera.setPreviewCallback(null);

		if (mSettings != null) {
			Editor editor = mSettings.edit();
			editor.putInt(PREF_PREFIX+"fps"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+mRequestedQuality.resY, mQuality.framerate);
			editor.commit();
		}

	}	

}
		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>20) {
					mQuality.framerate = (int) (1000000/(t/count)+1);
					lock.release();
				}
				oldnow = now;
			}
		};

		mCamera.setPreviewCallback(callback);

		try {
			lock.tryAcquire(2,TimeUnit.SECONDS);
			Log.d(TAG,"Actual framerate: "+mQuality.framerate);
			if (mSettings != null) {
				Editor editor = mSettings.edit();
				editor.putInt(PREF_PREFIX+"fps"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+mRequestedQuality.resY, mQuality.framerate);
				editor.commit();
			}
		} catch (InterruptedException e) {}

		mCamera.setPreviewCallback(null);

	}	

}

<MSG> Improvements of the new Session object. Updated GPL headers. Several bug fixes.

<DFF> @@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2011-2013 GUIGUI Simon, fyhertz@gmail.com
+ * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com
  * 
- * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)
+ * This file is part of libstreaming (https://github.com/fyhertz/libstreaming)
  * 
  * Spydroid is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,8 +24,13 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
 import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
 
 import net.majorkernelpanic.streaming.MediaStream;
+import net.majorkernelpanic.streaming.Stream;
+import net.majorkernelpanic.streaming.exceptions.CameraInUseException;
+import net.majorkernelpanic.streaming.exceptions.ConfNotSupportedException;
+import net.majorkernelpanic.streaming.exceptions.InvalidSurfaceException;
 import net.majorkernelpanic.streaming.gl.SurfaceView;
 import net.majorkernelpanic.streaming.hw.EncoderDebugger;
 import net.majorkernelpanic.streaming.hw.NV21Convertor;
@@ -64,7 +69,7 @@ public abstract class VideoStream extends MediaStream {
 	protected Looper mCameraLooper;
 
 	protected boolean mCameraOpenedManually = true;
-	protected boolean mFlashState = false;
+	protected boolean mFlashEnabled = false;
 	protected boolean mSurfaceReady = false;
 	protected boolean mUnlocked = false;
 	protected boolean mPreviewStarted = false;
@@ -92,7 +97,7 @@ public abstract class VideoStream extends MediaStream {
 		super();
 		setCamera(camera);
 	}
-	
+
 	/**
 	 * Sets the camera that will be used to capture video.
 	 * You can call this method at any time and changes will take effect next time you start the stream.
@@ -124,6 +129,7 @@ public abstract class VideoStream extends MediaStream {
 		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(); 
 	}
@@ -152,13 +158,6 @@ public abstract class VideoStream extends MediaStream {
 				@Override
 				public void surfaceCreated(SurfaceHolder holder) {
 					mSurfaceReady = true;
-					try {
-						startPreview();
-					} catch (RuntimeException e) {
-						e.printStackTrace();
-					} catch (IOException e) {
-						e.printStackTrace();
-					}
 				}
 				@Override
 				public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
@@ -172,17 +171,9 @@ public abstract class VideoStream extends MediaStream {
 
 	/** Turns the LED on or off if phone has one. */
 	public synchronized void setFlashState(boolean state) {
-
-		// FIXME: Is it possible to toggle the flash while streaming on android 2.3 ?
-		// FIXME: It works on android 4.2 and 4.3
-
-		mFlashState = state;
-
 		// If the camera has already been opened, we apply the change immediately
-		// FIXME: Will this work on Android 2.3 ?
 		if (mCamera != null) {
 
-			// Needed on Android 2.3
 			if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
 				lockCamera();
 			}
@@ -194,30 +185,32 @@ public abstract class VideoStream extends MediaStream {
 				// 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(mFlashState?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
+				parameters.setFlashMode(state?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
 				try {
 					mCamera.setParameters(parameters);
+					mFlashEnabled = state;
 				} catch (RuntimeException e) {
-					throw new RuntimeException("Can't turn the flash on !");	
+					mFlashEnabled = false;
+					throw new RuntimeException("Can't turn the flash on !");
+				} finally {
+					if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
+						unlockCamera();
+					}
 				}
 			}
-
-			// Needed on Android 2.3
-			if (mStreaming && mMode == MODE_MEDIARECORDER_API) {
-				unlockCamera();
-			}
-
+		} else {
+			mFlashEnabled = state;
 		}
 	}
 
 	/** Toggle the LED of the phone if it has one. */
-	public void toggleFlash() {
-		setFlashState(!mFlashState);
+	public synchronized void toggleFlash() {
+		setFlashState(!mFlashEnabled);
 	}
 
 	/** Indicates whether or not the flash of the phone is on. */
 	public boolean getFlashState() {
-		return mFlashState;
+		return mFlashEnabled;
 	}
 
 	/** 
@@ -236,7 +229,7 @@ public abstract class VideoStream extends MediaStream {
 	 */
 	public VideoQuality getVideoQuality() {
 		return mRequestedQuality;
-	}	
+	}
 
 	/**
 	 * Some data (SPS and PPS params) needs to be stored when {@link #getSessionDescription()} is called 
@@ -244,7 +237,7 @@ public abstract class VideoStream extends MediaStream {
 	 */
 	public void setPreferences(SharedPreferences prefs) {
 		mSettings = prefs;
-	}	
+	}
 
 	/**
 	 * Starts the stream.
@@ -254,7 +247,8 @@ public abstract class VideoStream extends MediaStream {
 	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() {
@@ -274,20 +268,19 @@ public abstract class VideoStream extends MediaStream {
 					startPreview();
 				} catch (RuntimeException e) {
 					e.printStackTrace();
-				} catch (IOException e) {
-					e.printStackTrace();
 				}
 			}
 		}
 	}
 
-	public synchronized void startPreview() throws RuntimeException, IOException {
+	public synchronized void startPreview() 
+			throws CameraInUseException, InvalidSurfaceException, ConfNotSupportedException, RuntimeException {
+		mCameraOpenedManually = true;
 		if (!mPreviewStarted) {
 			createCamera();
 			try {
 				mCamera.startPreview();
 				mPreviewStarted = true;
-				mCameraOpenedManually = true;
 			} catch (RuntimeException e) {
 				destroyCamera();
 				throw e;
@@ -316,29 +309,34 @@ public abstract class VideoStream extends MediaStream {
 		// Reopens the camera if needed
 		destroyCamera();
 		createCamera();
-		
+
 		// The camera must be unlocked before the MediaRecorder can use it
 		unlockCamera();
 
-		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 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
-		// can then be manipulated at the other end of the socket
-		mMediaRecorder.setOutputFile(mSender.getFileDescriptor());
-
-		mMediaRecorder.prepare();
-		mMediaRecorder.start();
+		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 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
+			// can then be manipulated at the other end of the socket
+			mMediaRecorder.setOutputFile(mSender.getFileDescriptor());
+
+			mMediaRecorder.prepare();
+			mMediaRecorder.start();
+
+		} catch (Exception e) {
+			throw new ConfNotSupportedException(e.getMessage());
+		}
 
 		// This will skip the MPEG4 header if this step fails we can't stream anything :(
 		InputStream is = mReceiver.getInputStream();
@@ -360,7 +358,7 @@ public abstract class VideoStream extends MediaStream {
 		mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
 		mPacketizer.setInputStream(mReceiver.getInputStream());
 		mPacketizer.start();
-		
+
 		mStreaming = true;
 
 	}
@@ -387,10 +385,13 @@ public abstract class VideoStream extends MediaStream {
 
 		Log.d(TAG,"Video encoded using the MediaCodec API with a buffer");
 
-		// Reopens the camera if needed
+		// Updates the parameters of the camera if needed
 		createCamera();
 		updateCamera();
 
+		// Estimates the framerate of the camera
+		measureFramerate();
+
 		// Starts the preview if needed
 		if (!mPreviewStarted) {
 			try {
@@ -447,7 +448,7 @@ public abstract class VideoStream extends MediaStream {
 		mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
 		mPacketizer.setInputStream(new MediaCodecInputStream(mMediaCodec));
 		mPacketizer.start();
-		
+
 		mStreaming = true;
 
 	}
@@ -461,10 +462,13 @@ public abstract class VideoStream extends MediaStream {
 
 		Log.d(TAG,"Video encoded using the MediaCodec API with a surface");
 
-		// Reopens the camera if needed
+		// Updates the parameters of the camera if needed
 		createCamera();
 		updateCamera();
 
+		// Estimates the framerate of the camera
+		measureFramerate();
+
 		EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY);
 
 		mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
@@ -477,16 +481,21 @@ public abstract class VideoStream extends MediaStream {
 		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.setDestination(mDestination, mRtpPort, mRtcpPort);
 		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;
 
 	/**
@@ -496,7 +505,7 @@ public abstract class VideoStream extends MediaStream {
 	 */
 	private void openCamera() throws RuntimeException {
 		final Semaphore lock = new Semaphore(0);
-		final RuntimeException[] exception = new RuntimeException[1]; 
+		final RuntimeException[] exception = new RuntimeException[1];
 		mCameraThread = new Thread(new Runnable() {
 			@Override
 			public void run() {
@@ -514,15 +523,14 @@ public abstract class VideoStream extends MediaStream {
 		});
 		mCameraThread.start();
 		lock.acquireUninterruptibly();
-		if (exception[0] != null) throw exception[0];
+		if (exception[0] != null) throw new CameraInUseException(exception[0].getMessage());
 	}
 
-	protected synchronized void createCamera() throws RuntimeException, IOException {
+	protected synchronized void createCamera() throws RuntimeException {
 		if (mSurfaceView == null)
-			throw new IllegalStateException("Invalid surface view !");
-		if (mMode != MODE_MEDIACODEC_API_2 && (mSurfaceView.getHolder() == null || !mSurfaceReady)) 
-			throw new IllegalStateException("Invalid surface view !");
-
+			throw new InvalidSurfaceException("Invalid surface !");
+		if (mSurfaceView.getHolder() == null || !mSurfaceReady) 
+			throw new InvalidSurfaceException("Invalid surface !");
 		if (mCamera == null) {
 			openCamera();
 			mUnlocked = false;
@@ -544,34 +552,37 @@ public abstract class VideoStream extends MediaStream {
 			});
 
 			try {
-				
+
 				Parameters parameters = mCamera.getParameters();
-				if (mFlashState) {
+				if (mFlashEnabled) {
 					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);
+						parameters.setFlashMode(mFlashEnabled?Parameters.FLASH_MODE_TORCH:Parameters.FLASH_MODE_OFF);
 					}
 				}
 
 				VideoQuality quality = new VideoQuality(352,288);
 				quality = VideoQuality.determineClosestSupportedResolution(parameters, quality);
 				int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
-				
+
 				parameters.setPreviewFpsRange(max[0], max[1]);
 				parameters.setPreviewSize(quality.resX, quality.resY);
 				parameters.setRecordingHint(true);
 				mCamera.setParameters(parameters);
 				mCamera.setDisplayOrientation(mQuality.orientation);
-				
-				if (mMode == MODE_MEDIACODEC_API_2) {
-					mSurfaceView.startGLThread();
-					mCamera.setPreviewTexture(mSurfaceView.getSurfaceTexture());
-				} else {
-					mCamera.setPreviewDisplay(mSurfaceView.getHolder());
+
+				try {
+					if (mMode == MODE_MEDIACODEC_API_2) {
+						mSurfaceView.startGLThread();
+						mCamera.setPreviewTexture(mSurfaceView.getSurfaceTexture());
+					} else {
+						mCamera.setPreviewDisplay(mSurfaceView.getHolder());
+					}
+				} catch (IOException e) {
+					throw new InvalidSurfaceException("Invalid surface !");
 				}
-				
+
 			} catch (RuntimeException e) {
 				destroyCamera();
 				throw e;
@@ -612,12 +623,10 @@ public abstract class VideoStream extends MediaStream {
 			parameters.setPreviewFpsRange(max[0], max[1]);
 
 			try {
-				Log.e(TAG,"FPS: "+mQuality.framerate+" X: "+mQuality.resX+" Y: "+mQuality.resY);
 				mCamera.setParameters(parameters);
-				mCamera.setDisplayOrientation(mRequestedQuality.orientation);
+				mCamera.setDisplayOrientation(mQuality.orientation);
 				mCamera.startPreview();
 				mPreviewStarted = true;
-				measureActualFramerate();
 			} catch (RuntimeException e) {
 				destroyCamera();
 				throw e;
@@ -655,18 +664,9 @@ public abstract class VideoStream extends MediaStream {
 	 * We will then use this average framerate with the MediaCodec.  
 	 * Blocks the thread in which this function is called.
 	 */
-	private void measureActualFramerate() {
+	private void measureFramerate() {
 		final Semaphore lock = new Semaphore(0);
 
-		if (mSettings != null) {
-			String key = PREF_PREFIX+"fps-"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+","+mRequestedQuality.resY;
-			if (mSettings.contains(key)) {
-				mQuality.framerate = mSettings.getInt(key, 0);
-				Log.d(TAG,"Actual framerate: "+mQuality.framerate);
-				return;
-			}
-		}
-
 		final Camera.PreviewCallback callback = new Camera.PreviewCallback() {
 			int i = 0, t = 0;
 			long now, oldnow, count = 0;
@@ -678,9 +678,8 @@ public abstract class VideoStream extends MediaStream {
 					t += now - oldnow;
 					count++;
 				}
-				if (i>15) {
+				if (i>20) {
 					mQuality.framerate = (int) (1000000/(t/count)+1);
-					Log.d(TAG,"Actual framerate: "+mQuality.framerate);
 					lock.release();
 				}
 				oldnow = now;
@@ -690,17 +689,17 @@ public abstract class VideoStream extends MediaStream {
 		mCamera.setPreviewCallback(callback);
 
 		try {
-			lock.acquire();
+			lock.tryAcquire(2,TimeUnit.SECONDS);
+			Log.d(TAG,"Actual framerate: "+mQuality.framerate);
+			if (mSettings != null) {
+				Editor editor = mSettings.edit();
+				editor.putInt(PREF_PREFIX+"fps"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+mRequestedQuality.resY, mQuality.framerate);
+				editor.commit();
+			}
 		} catch (InterruptedException e) {}
 
 		mCamera.setPreviewCallback(null);
 
-		if (mSettings != null) {
-			Editor editor = mSettings.edit();
-			editor.putInt(PREF_PREFIX+"fps"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+mRequestedQuality.resY, mQuality.framerate);
-			editor.commit();
-		}
-
 	}	
 
 }

Improvements of the new Session object. Updated GPL headers. Several bug fixes.