<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 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 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;
import android.hardware.Camera.Parameters;
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;


	protected final static String TAG = "VideoStream";

	protected VideoQuality mQuality = VideoQuality.DEFAULT_VIDEO_QUALITY.clone();
	protected VideoQuality mActualQuality = mQuality.clone(); 
	protected SurfaceHolder.Callback mSurfaceHolderCallback = null;
	protected SurfaceView mSurfaceView = null;
	protected SharedPreferences mSettings = null;
	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 mFlashEnabled = 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);
	}	

		super();
		setCamera(camera);
	}

	/**
	 * Called by subclasses. 
	 */
	protected void init(String mimeType) {
		mCameraImageFormat = ImageFormat.NV21;
		mMimeType = mimeType;
	}

	/**
	 * 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.
		setCamera(camera);
	}

	/**
	 * Sets the camera that will be used to capture video.
		for (int i=0;i<numberOfCameras;i++) {
			Camera.getCameraInfo(i, cameraInfo);
			if (cameraInfo.facing == camera) {
				this.mCameraId = i;
				break;
			}
		}
			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;
		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()}.
		setFlashState(!mFlashState);
	}

	public boolean getFlashState() {
		return mFlashState;
	}

	/** 
	 * Modifies the resolution of the stream. You can call this method at any time 
	 * and changes will take effect next time you call {@link #start()}.
	 * {@link #setVideoQuality(VideoQuality)} may be more convenient.
	 * @param width Width of the stream
	 * @param height height of the stream
	 */
	public void setVideoSize(int width, int height) {
		if (mQuality.resX != width || mQuality.resY != height) {
			mQuality.resX = width;
			mQuality.resY = height;
		}
	}

	/** 
	 * Modifies the framerate of the stream. You can call this method at any time 
	 * and changes will take effect next time you call {@link #start()}.
	 * {@link #setVideoQuality(VideoQuality)} may be more convenient.
	 * @param rate Framerate of the stream
	 */	
	public void setVideoFramerate(int rate) {
		if (mQuality.framerate != rate) {
			mQuality.framerate = rate;
		}
	}

	/** 
	 * Modifies the bitrate of the stream. You can call this method at any time 
	 * and changes will take effect next time you call {@link #start()}.
	 * {@link #setVideoQuality(VideoQuality)} may be more convenient.
	 * @param bitrate Bitrate of the stream in bit per second
	 */	
	public void setVideoEncodingBitrate(int bitrate) {
		if (mQuality.bitrate != bitrate) {
			mQuality.bitrate = bitrate;
		}
	}

	/** 
	 * Modifies the quality of the stream. You can call this method at any time 
	 * and changes will take effect next time you call {@link #start()}.
	 * @param videoQuality Quality of the stream
	 */
	public void setVideoQuality(VideoQuality videoQuality) {
		if (!mQuality.equals(videoQuality)) {
			mQuality = videoQuality.clone();
		}
	}

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

	/** 
	 * Modifies the videoEncoder of the stream. You can call this method at any time 
	 * and changes will take effect next time you call {@link #start()}.
	 * @param videoEncoder Encoder of the stream
	 */
	protected void setVideoEncoder(int videoEncoder) {
		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) {
	/** 
	 * 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;
		if (!mPreviewStarted) {
			createCamera();
			updateCamera();
		}
	}

	/**
	 * Stops the preview.
	 */
	public synchronized void stopPreview() {
		mCameraOpenedManually = false;
		stop();
	}

	/**
	 * Video encoding is done by a MediaRecorder.
		mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
		mMediaRecorder.setVideoEncoder(mVideoEncoder);
		mMediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface());
		mMediaRecorder.setVideoSize(mQuality.resX,mQuality.resY);
		mMediaRecorder.setVideoFrameRate(mQuality.framerate);

		// 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

		// The camera must be unlocked before the MediaRecorder can use it
		unlockCamera();

		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));
		} catch (IOException e) {
			Log.e(TAG,"Couldn't skip mp4 header :/");
			stop();
		}

		try {
			// mReceiver.getInputStream contains the data from the camera
			// the mPacketizer encapsulates this stream in an RTP stream and send it over the network
			mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
			mPacketizer.setInputStream(mReceiver.getInputStream());
			mPacketizer.start();
			mStreaming = true;
		} catch (IOException e) {
			stop();
			throw new IOException("Something happened with the local sockets :/ Start failed !");
		}

	}

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

	}
			}
		}

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

		mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
		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,debugger.getEncoderColorFormat());
		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
		mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
	}	

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

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

		try {
			// mReceiver.getInputStream contains the data from the camera
			// the mPacketizer encapsulates this 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;
		} catch (IOException e) {
			stop();
			throw new IOException("Something happened with the local sockets :/ Start failed !");
		}

	}

		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.");
		createCamera();
		updateCamera();

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

		mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
		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, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
		mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
		mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
					mCamera.addCallbackBuffer(data);
		((SurfaceView)mSurfaceView).addMediaCodecSurface(surface);
		mMediaCodec.start();
		
		try {
			mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
			mPacketizer.setInputStream(new MediaCodecInputStream(mMediaCodec));
			mPacketizer.start();
			mStreaming = true;
		} catch (IOException e) {
			stop();
			throw new IOException("Something happened with the local sockets :/ Start failed !");
		}

	}

	public abstract String generateSessionDescription() throws IllegalStateException, IOException;

	/**
	 * Opens the camera in a new Looper thread so that the preview callback is not called from the main thread
	 * Video encoding is done by a MediaCodec.
	 * But here we will use the buffer-to-surface method
	 */
	@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);
	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();
	}

	/**
	 * 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.
	 * @throws RuntimeException Might happen if another app is already using the camera.
	 */
	private void openCamera() throws RuntimeException {
		final Semaphore lock = new Semaphore(0);
			});

			try {
				Parameters parameters = mCamera.getParameters();
				if (mFlashState) {
					if (parameters.getFlashMode()==null) {
					mCamera = Camera.open(mCameraId);
				} catch (RuntimeException e) {
					exception[0] = e;
				} finally {
					}
				}

				int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
				VideoQuality quality = new VideoQuality(352,288);
				quality = VideoQuality.determineClosestSupportedResolution(parameters, quality);

				parameters.setPreviewFpsRange(max[0], max[1]);
				parameters.setPreviewSize(quality.resX, quality.resY);
				mCamera.setParameters(parameters);
				mCamera.setDisplayOrientation(mQuality.orientation);
				
		if (mSurfaceView.getHolder() == null || !mSurfaceReady) 
			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
				// 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 {
			}

			Parameters parameters = mCamera.getParameters();

			mActualQuality.resX = mQuality.resX;
			mActualQuality.resY = mQuality.resY;
			mActualQuality.bitrate = mQuality.bitrate;

			mActualQuality = VideoQuality.determineClosestSupportedResolution(parameters, mActualQuality);
			int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);

			parameters.setPreviewFormat(mCameraImageFormat);
			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.startPreview();
				mPreviewStarted = true;
				measureActualFramerate();
	}

	protected synchronized void destroyCamera() {
		if (mCamera != null) {
			if (mStreaming) super.stop();
			lockCamera();
			mCamera.stopPreview();
			try {
				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);
	 * 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;
	protected void lockCamera() {
		if (mUnlocked) {
			Log.d(TAG,"Locking camera");
			try {
				mCamera.reconnect();
			} catch (Exception e) {
					count++;
				}
				if (i>15) {
					mActualQuality.framerate = (int) (1000000/(t/count)+1);
					Log.d(TAG,"Actual framerate: "+mActualQuality.framerate);
					lock.release();
				}
				oldnow = now;
			Log.d(TAG,"Unlocking camera");
			try {	
				mCamera.unlock();
			} catch (Exception e) {
				Log.e(TAG,e.getMessage());
			}
			mUnlocked = true;
		}
	}


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

		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>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> A Session now has its own Looper thread in which Stream objects are manipulated. A callback can be implemented to have feedback on what's going on.

<DFF> @@ -33,7 +33,6 @@ 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;
 import android.hardware.Camera.Parameters;
@@ -54,8 +53,8 @@ public abstract class VideoStream extends MediaStream {
 
 	protected final static String TAG = "VideoStream";
 
-	protected VideoQuality mQuality = VideoQuality.DEFAULT_VIDEO_QUALITY.clone();
-	protected VideoQuality mActualQuality = mQuality.clone(); 
+	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;
@@ -93,15 +92,7 @@ public abstract class VideoStream extends MediaStream {
 		super();
 		setCamera(camera);
 	}
-
-	/**
-	 * Called by subclasses. 
-	 */
-	protected void init(String mimeType) {
-		mCameraImageFormat = ImageFormat.NV21;
-		mMimeType = mimeType;
-	}
-
+	
 	/**
 	 * 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.
@@ -113,7 +104,7 @@ public abstract class VideoStream extends MediaStream {
 		for (int i=0;i<numberOfCameras;i++) {
 			Camera.getCameraInfo(i, cameraInfo);
 			if (cameraInfo.facing == camera) {
-				this.mCameraId = i;
+				mCameraId = i;
 				break;
 			}
 		}
@@ -224,56 +215,19 @@ public abstract class VideoStream extends MediaStream {
 		setFlashState(!mFlashState);
 	}
 
+	/** Indicates whether or not the flash of the phone is on. */
 	public boolean getFlashState() {
 		return mFlashState;
 	}
 
 	/** 
-	 * Modifies the resolution of the stream. You can call this method at any time 
-	 * and changes will take effect next time you call {@link #start()}.
-	 * {@link #setVideoQuality(VideoQuality)} may be more convenient.
-	 * @param width Width of the stream
-	 * @param height height of the stream
-	 */
-	public void setVideoSize(int width, int height) {
-		if (mQuality.resX != width || mQuality.resY != height) {
-			mQuality.resX = width;
-			mQuality.resY = height;
-		}
-	}
-
-	/** 
-	 * Modifies the framerate of the stream. You can call this method at any time 
-	 * and changes will take effect next time you call {@link #start()}.
-	 * {@link #setVideoQuality(VideoQuality)} may be more convenient.
-	 * @param rate Framerate of the stream
-	 */	
-	public void setVideoFramerate(int rate) {
-		if (mQuality.framerate != rate) {
-			mQuality.framerate = rate;
-		}
-	}
-
-	/** 
-	 * Modifies the bitrate of the stream. You can call this method at any time 
-	 * and changes will take effect next time you call {@link #start()}.
-	 * {@link #setVideoQuality(VideoQuality)} may be more convenient.
-	 * @param bitrate Bitrate of the stream in bit per second
-	 */	
-	public void setVideoEncodingBitrate(int bitrate) {
-		if (mQuality.bitrate != bitrate) {
-			mQuality.bitrate = bitrate;
-		}
-	}
-
-	/** 
-	 * Modifies the quality of the stream. You can call this method at any time 
-	 * and changes will take effect next time you call {@link #start()}.
+	 * 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 (!mQuality.equals(videoQuality)) {
-			mQuality = videoQuality.clone();
+		if (!mRequestedQuality.equals(videoQuality)) {
+			mRequestedQuality = videoQuality.clone();
 		}
 	}
 
@@ -281,20 +235,11 @@ public abstract class VideoStream extends MediaStream {
 	 * Returns the quality of the stream.  
 	 */
 	public VideoQuality getVideoQuality() {
-		return mQuality;
+		return mRequestedQuality;
 	}	
 
-	/** 
-	 * Modifies the videoEncoder of the stream. You can call this method at any time 
-	 * and changes will take effect next time you call {@link #start()}.
-	 * @param videoEncoder Encoder of the stream
-	 */
-	protected void setVideoEncoder(int videoEncoder) {
-		this.mVideoEncoder = videoEncoder;
-	}
-
 	/**
-	 * Some data (SPS and PPS params) needs to be stored when {@link #generateSessionDescription()} is called 
+	 * 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) {
@@ -381,11 +326,11 @@ public abstract class VideoStream extends MediaStream {
 		mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
 		mMediaRecorder.setVideoEncoder(mVideoEncoder);
 		mMediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface());
-		mMediaRecorder.setVideoSize(mQuality.resX,mQuality.resY);
-		mMediaRecorder.setVideoFrameRate(mQuality.framerate);
+		mMediaRecorder.setVideoSize(mRequestedQuality.resX,mRequestedQuality.resY);
+		mMediaRecorder.setVideoFrameRate(mRequestedQuality.framerate);
 
 		// The bandwidth actually consumed is often above what was requested 
-		mMediaRecorder.setVideoEncodingBitRate((int)(mQuality.bitrate*0.8));
+		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
@@ -408,19 +353,15 @@ public abstract class VideoStream extends MediaStream {
 		} catch (IOException e) {
 			Log.e(TAG,"Couldn't skip mp4 header :/");
 			stop();
+			throw e;
 		}
 
-		try {
-			// mReceiver.getInputStream contains the data from the camera
-			// the mPacketizer encapsulates this stream in an RTP stream and send it over the network
-			mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
-			mPacketizer.setInputStream(mReceiver.getInputStream());
-			mPacketizer.start();
-			mStreaming = true;
-		} catch (IOException e) {
-			stop();
-			throw new IOException("Something happened with the local sockets :/ Start failed !");
-		}
+		// The packetizer encapsulates the bit stream in an RTP stream and send it over the network
+		mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
+		mPacketizer.setInputStream(mReceiver.getInputStream());
+		mPacketizer.start();
+		
+		mStreaming = true;
 
 	}
 
@@ -461,13 +402,13 @@ public abstract class VideoStream extends MediaStream {
 			}
 		}
 
-		EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mActualQuality.resX, mActualQuality.resY);
+		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", mActualQuality.resX, mActualQuality.resY);
-		mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mActualQuality.bitrate);
-		mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mActualQuality.framerate);	
+		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);
@@ -502,17 +443,12 @@ public abstract class VideoStream extends MediaStream {
 		for (int i=0;i<10;i++) mCamera.addCallbackBuffer(new byte[convertor.getBufferSize()]);
 		mCamera.setPreviewCallbackWithBuffer(callback);
 
-		try {
-			// mReceiver.getInputStream contains the data from the camera
-			// the mPacketizer encapsulates this 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;
-		} catch (IOException e) {
-			stop();
-			throw new IOException("Something happened with the local sockets :/ Start failed !");
-		}
+		// 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;
 
 	}
 
@@ -529,12 +465,12 @@ public abstract class VideoStream extends MediaStream {
 		createCamera();
 		updateCamera();
 
-		EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mActualQuality.resX, mActualQuality.resY);
+		EncoderDebugger debugger = EncoderDebugger.debug(mSettings, mQuality.resX, mQuality.resY);
 
 		mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
-		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 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);
@@ -542,19 +478,16 @@ public abstract class VideoStream extends MediaStream {
 		((SurfaceView)mSurfaceView).addMediaCodecSurface(surface);
 		mMediaCodec.start();
 		
-		try {
-			mPacketizer.setDestination(mDestination, mRtpPort, mRtcpPort);
-			mPacketizer.setInputStream(new MediaCodecInputStream(mMediaCodec));
-			mPacketizer.start();
-			mStreaming = true;
-		} catch (IOException e) {
-			stop();
-			throw new IOException("Something happened with the local sockets :/ Start failed !");
-		}
+		// 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 generateSessionDescription() throws IllegalStateException, IOException;
+	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
@@ -587,8 +520,8 @@ public abstract class VideoStream extends MediaStream {
 	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 (mMode != MODE_MEDIACODEC_API_2 && (mSurfaceView.getHolder() == null || !mSurfaceReady)) 
+			throw new IllegalStateException("Invalid surface view !");
 
 		if (mCamera == null) {
 			openCamera();
@@ -611,6 +544,7 @@ public abstract class VideoStream extends MediaStream {
 			});
 
 			try {
+				
 				Parameters parameters = mCamera.getParameters();
 				if (mFlashState) {
 					if (parameters.getFlashMode()==null) {
@@ -621,12 +555,13 @@ public abstract class VideoStream extends MediaStream {
 					}
 				}
 
-				int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
 				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);
 				
@@ -670,22 +605,16 @@ public abstract class VideoStream extends MediaStream {
 			}
 
 			Parameters parameters = mCamera.getParameters();
-
-			mActualQuality.resX = mQuality.resX;
-			mActualQuality.resY = mQuality.resY;
-			mActualQuality.bitrate = mQuality.bitrate;
-
-			mActualQuality = VideoQuality.determineClosestSupportedResolution(parameters, mActualQuality);
+			mQuality = VideoQuality.determineClosestSupportedResolution(parameters, mQuality);
 			int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters);
-
 			parameters.setPreviewFormat(mCameraImageFormat);
-			parameters.setPreviewSize(mActualQuality.resX, mActualQuality.resY);
-			//parameters.setPreviewFpsRange(max[0], max[1]);
+			parameters.setPreviewSize(mQuality.resX, mQuality.resY);
+			parameters.setPreviewFpsRange(max[0], max[1]);
 
 			try {
-				Log.e(TAG,"FPS: "+mActualQuality.framerate+" X: "+mActualQuality.resX+" Y: "+mActualQuality.resY);
+				Log.e(TAG,"FPS: "+mQuality.framerate+" X: "+mQuality.resX+" Y: "+mQuality.resY);
 				mCamera.setParameters(parameters);
-				mCamera.setDisplayOrientation(mQuality.orientation);
+				mCamera.setDisplayOrientation(mRequestedQuality.orientation);
 				mCamera.startPreview();
 				mPreviewStarted = true;
 				measureActualFramerate();
@@ -727,18 +656,17 @@ public abstract class VideoStream extends MediaStream {
 	 * 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-"+mQuality.framerate+","+mCameraImageFormat+","+mQuality.resX+","+mQuality.resY;
+			String key = PREF_PREFIX+"fps-"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+","+mRequestedQuality.resY;
 			if (mSettings.contains(key)) {
-				mActualQuality.framerate = mSettings.getInt(key, 0);
-				Log.d(TAG,"Actual framerate: "+mActualQuality.framerate);
+				mQuality.framerate = mSettings.getInt(key, 0);
+				Log.d(TAG,"Actual framerate: "+mQuality.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;
@@ -751,8 +679,8 @@ public abstract class VideoStream extends MediaStream {
 					count++;
 				}
 				if (i>15) {
-					mActualQuality.framerate = (int) (1000000/(t/count)+1);
-					Log.d(TAG,"Actual framerate: "+mActualQuality.framerate);
+					mQuality.framerate = (int) (1000000/(t/count)+1);
+					Log.d(TAG,"Actual framerate: "+mQuality.framerate);
 					lock.release();
 				}
 				oldnow = now;
@@ -769,7 +697,7 @@ public abstract class VideoStream extends MediaStream {
 
 		if (mSettings != null) {
 			Editor editor = mSettings.edit();
-			editor.putInt(PREF_PREFIX+"fps"+mQuality.framerate+","+mCameraImageFormat+","+mQuality.resX+mQuality.resY, mActualQuality.framerate);
+			editor.putInt(PREF_PREFIX+"fps"+mRequestedQuality.framerate+","+mCameraImageFormat+","+mRequestedQuality.resX+mRequestedQuality.resY, mQuality.framerate);
 			editor.commit();
 		}
 

A Session now has its own Looper thread in which Stream objects are manipulated. A callback can be implemented to have feedback on what's going on.