package eu.hgross.blaubotcam.video; import android.content.Context; import android.graphics.ImageFormat; import android.graphics.Rect; import android.graphics.YuvImage; import android.hardware.Camera; import android.hardware.Camera.Parameters; import android.view.SurfaceHolder; import android.view.SurfaceView; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Observable; import eu.hgross.blaubot.util.Log; /** * Given a SurfaceView and a Camera object, this class provides some methods to start a simple MJPEG server serving the camera's preview image. * * To integrate the preview into your app, you can use getSurfaceView() and add it to your layout. * * @author Henning Gross {@literal (mail.to@henning-gross.de)} */ public class CameraReader extends Observable implements Camera.PreviewCallback, SurfaceHolder.Callback { private static final String LOG_TAG = "CameraReader"; private static final int DEFAULT_FPS = 15; private static final int DEFAULT_JPEG_QUALITY = 30; private final SurfaceView surfaceView; private byte[] lastPreviewJpeg; private Camera camera; private Parameters cameraParameters; /** * The jpeg quality to use */ private int jpegQuality; /** * The camera needs some time to stop/start preview. Some android devices have concurrency problems with fast start() stop() calls so we give them this period of time. */ private static final long CAMERA_SLEEP_PERIOD = 550; // ms; private boolean showingPreview = false; private boolean flashLightOn = false; private long lastRenderedFrameTime = 0; /** * @param context the android context to be used */ public CameraReader(Context context) { setMaxFps(DEFAULT_FPS); setJpegQuality(DEFAULT_JPEG_QUALITY); this.surfaceView = new SurfaceView(context); SurfaceHolder holder = this.surfaceView.getHolder(); holder.setSizeFromLayout(); holder.addCallback(this); } public void acquireCamera() { Log.d(LOG_TAG, "Acquiring camera"); try { this.camera = Camera.open(); } catch (RuntimeException e) { Log.w(LOG_TAG, "Catched a runtime exception acquiring a back facing camera!", e); } if (this.camera == null) { // no backfacing camera - try if we can find a front facing camera Log.d(LOG_TAG, "Could not find a back facing camera - trying to get another ..."); try { this.camera = Camera.open(0); } catch (RuntimeException e) { Log.w(LOG_TAG, "Catched a runtime exception acquiring a camera!", e); } } if (this.camera != null) { Log.d(LOG_TAG, "Got a camera ..."); this.camera.lock(); } else { Log.w(LOG_TAG, "Could not acquire camera"); } } public void releaseCamera() { Log.d(LOG_TAG, "Releasing camera ..."); if (this.camera != null) { this.camera.release(); } this.camera = null; } /** * The used surface view to show the image. * * @return */ public SurfaceView getSurfaceView() { return surfaceView; } /** * Turns the LED flash on. Note that if this (re)starts the previewing! */ public synchronized void turnFlashlightOn() { if (flashLightOn) { return; } flashLightOn = true; if (showingPreview) { Log.d(LOG_TAG, "Restarting preview to turn on flashlight."); // restart preview stopPreview(); try { Thread.sleep(CAMERA_SLEEP_PERIOD); // to overcome another android bug -.- } catch (InterruptedException e) { } startPreview(); } else { startPreview(); try { Thread.sleep(CAMERA_SLEEP_PERIOD); // to overcome another android bug -.- } catch (InterruptedException e) { } stopPreview(); } Log.d(LOG_TAG, "Flashlight is now turned on."); } /** * Turns of the flashlight LED. */ public synchronized void turnFlashlightOff() { if (!flashLightOn) { return; } flashLightOn = false; if (showingPreview) { Log.d(LOG_TAG, "Restarting preview to turn of flashlight."); // restart preview stopPreview(); try { Thread.sleep(CAMERA_SLEEP_PERIOD); // to overcome another android bug -.- } catch (InterruptedException e) { } startPreview(); } else { startPreview(); try { Thread.sleep(CAMERA_SLEEP_PERIOD); // to overcome another android bug -.- } catch (InterruptedException e) { } stopPreview(); } Log.d(LOG_TAG, "Flashlight is now turned off."); } private final ByteArrayOutputStream jpgOut = new ByteArrayOutputStream(); private boolean processing = false; @Override public void onPreviewFrame(byte[] data, Camera camera) { if (processing) { return; } if (this.camera == null) { // do nothing if the camera is gone (overcomes some concurrency issues on poorly implemented android cameras) return; } long now = System.currentTimeMillis(); long diff = now - lastRenderedFrameTime; if (diff < maxFpsPeriod) { return; } try { cameraParameters = camera.getParameters(); int imageFormat = cameraParameters.getPreviewFormat(); if (imageFormat == ImageFormat.NV21) { processing = true; int previewSizeWidth = cameraParameters.getPreviewSize().width; int previewSizeHeight = cameraParameters.getPreviewSize().height; Rect rect = new Rect(0, 0, previewSizeWidth, previewSizeHeight); YuvImage img = new YuvImage(data, ImageFormat.NV21, previewSizeWidth, previewSizeHeight, null); img.compressToJpeg(rect, jpegQuality, jpgOut); lastPreviewJpeg = jpgOut.toByteArray(); jpgOut.reset(); lastRenderedFrameTime = now; setChanged(); notifyObservers(lastPreviewJpeg); processing = false; } } catch (RuntimeException e) { Log.e(LOG_TAG, "Something went wrong capturing a previeFrame", e); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } /** * Toggles the video stream on and off. */ public synchronized void toggleVideoStream() { if (showingPreview) { stopPreview(); } else { startPreview(); } } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } /** * @return true if the camera is currently fetching preview images, false otherwise */ public boolean isShowingPreview() { return showingPreview; } /** * onResume */ public synchronized void startPreview() { if (showingPreview) return; acquireCamera(); if (camera == null) { Log.d(LOG_TAG, "No acquired camera found. Not starting preview."); return; } Parameters params = null; try { params = camera.getParameters(); } catch (RuntimeException e) { Log.e(LOG_TAG, "Stream not started! Failed to get camera parameters - this happens occasionally if the headlights are toggled to fast! Seems to be an android issue. Message: " + e.getMessage(), e); return; } // List<Size> sizes = params.getSupportedPreviewSizes(); // Size size = sizes.get(1); // params.setPictureSize(size.width, size.height); // params.setPreviewSize(size.width, size.height); params.setJpegQuality(jpegQuality); boolean flashModeSupported = params.getSupportedFlashModes() == null ? false : params.getSupportedFlashModes().contains(Parameters.FLASH_MODE_TORCH); if (flashModeSupported) { params.setFlashMode(flashLightOn ? Parameters.FLASH_MODE_TORCH : Parameters.FLASH_MODE_OFF); } else { Log.w(LOG_TAG, "Flashlight not supported by device."); } // find the highest maxFps range int[] range = null; for (int[] ints : params.getSupportedPreviewFpsRange()) { if (range == null || ints[0] > range[0]) range = ints; } params.setPreviewFpsRange(range[0], range[1]); try { try { camera.setPreviewDisplay(surfaceView.getHolder()); } catch (IOException e) { Log.e(LOG_TAG, "IO Exception setting previewDisplay for camera", e); } camera.setParameters(params); camera.setPreviewCallback(this); camera.startPreview(); this.showingPreview = true; } catch (RuntimeException e) { Log.w(LOG_TAG, "Something went wrong starting the camera preview - catching the exception and going back to disabled state.", e); if (camera != null) { camera.setPreviewCallback(null); stopPreview(); } } } /** * Stop the camera's preview. */ public synchronized void stopPreview() { if (camera == null) return; try { camera.setPreviewDisplay(null); } catch (IOException e) { Log.e(LOG_TAG, "IO Exception setting previewDisplay for camera", e); } camera.setPreviewCallback(null); camera.stopPreview(); this.showingPreview = false; releaseCamera(); } /** * Retrieves the flashlight state. * * @return true, if the flashlight is set to on */ public boolean isFlashLightOn() { return flashLightOn; } /** * the configured fps limit */ private long maxFps; /** * min delay between pictures to maintain the max fps */ private long maxFpsPeriod; /** * The configured frames per seconds * * @return */ public long getMAxFps() { return maxFps; } /** * sets the max frames per second * * @param fps */ public void setMaxFps(long fps) { this.maxFps = fps; this.maxFpsPeriod = 1000 / fps; } /** * the jpeg quality * * @return [1, 100] */ public int getJpegQuality() { return jpegQuality; } /** * Sets the jpeg quality * * @param jpegQuality the jpeg quality in percent, higher is better [1,100] */ public void setJpegQuality(int jpegQuality) { if (jpegQuality < 1 || jpegQuality > 100) { throw new IllegalArgumentException("JPEG quality has to be in [1,100]"); } this.jpegQuality = jpegQuality; } }