Back to project page spydroid-ipcamera.
The source code is released under:
GNU General Public License
If you think the Android project spydroid-ipcamera listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com * /*w w w . j a va 2 s. c o m*/ * 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 * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This source code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this source code; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package net.majorkernelpanic.streaming.video; 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; import net.majorkernelpanic.streaming.rtp.MediaCodecInputStream; import android.annotation.SuppressLint; 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.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 mFlashEnabled = false; protected boolean mSurfaceReady = false; protected boolean mUnlocked = false; protected boolean mPreviewStarted = 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") public VideoStream(int camera) { 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. * @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; 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(); } 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.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; } } /** Toggle the LED of the phone if it has one. */ 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; } /** * 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) { mRequestedQuality = videoQuality.clone(); } /** * 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 dispay the preview * if {@link #startPreview()} has not aready 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, ConfNotSupportedException, RuntimeException { mCameraOpenedManually = true; if (!mPreviewStarted) { createCamera(); updateCamera(); try { mCamera.startPreview(); mPreviewStarted = true; } catch (RuntimeException e) { destroyCamera(); throw e; } } } /** * Stops the preview. */ public synchronized void stopPreview() { mCameraOpenedManually = false; stop(); } /** * Video encoding is done by a MediaRecorder. */ protected void encodeWithMediaRecorder() throws IOException { Log.d(TAG,"Video encoded using the MediaRecorder API"); // We need a local socket to forward data output by the camera to the packetizer createSockets(); // Reopens the camera if needed destroyCamera(); createCamera(); // 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)); // 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(); 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.setDestination(mDestination, mRtpPort, mRtcpPort); mPacketizer.setInputStream(mReceiver.getInputStream()); mPacketizer.start(); 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 framerate 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(); 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); } } }; for (int i=0;i<10;i++) mCamera.addCallbackBuffer(new byte[convertor.getBufferSize()]); mCamera.setPreviewCallbackWithBuffer(callback); // 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; } /** * Video encoding is done by a MediaCodec. * But here we will use the buffer-to-surface methode */ @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 framerate 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.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; /** * 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); } catch (RuntimeException e) { exception[0] = e; } finally { lock.release(); Looper.loop(); } } }); mCameraThread.start(); lock.acquireUninterruptibly(); if (exception[0] != null) throw new CameraInUseException(exception[0].getMessage()); } protected synchronized void createCamera() throws RuntimeException { if (mSurfaceView == null) throw new InvalidSurfaceException("Invalid surface !"); if (mSurfaceView.getHolder() == null || !mSurfaceReady) throw new InvalidSurfaceException("Invalid surface !"); if (mCamera == null) { openCamera(); 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 optimisation 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; } } } 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 { if (mPreviewStarted) { mPreviewStarted = false; mCamera.stopPreview(); } Parameters parameters = mCamera.getParameters(); mQuality = VideoQuality.determineClosestSupportedResolution(parameters, mQuality); int[] max = VideoQuality.determineMaximumSupportedFramerate(parameters); 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; } catch (RuntimeException e) { destroyCamera(); throw e; } } protected void lockCamera() { if (mUnlocked) { Log.d(TAG,"Locking camera"); try { mCamera.reconnect(); } catch (Exception e) { Log.e(TAG,e.getMessage()); } mUnlocked = false; } } 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 framerate with the MediaCodec. * Blocks the thread in which this function is called. */ private void measureFramerate() { 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); } }