davidmigloz/go-bees

View on GitHub
app/src/main/java/com/davidmiguel/gobees/monitoring/camera/AndroidCameraImpl.java

Summary

Maintainability
A
2 hrs
Test Coverage
/*
 * GoBees
 * Copyright (c) 2016 - 2017 David Miguel Lozano
 *
 * This program 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
 * any later version.
 *
 * This program 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 program. If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 */

package com.davidmiguel.gobees.monitoring.camera;

import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback;

import com.davidmiguel.gobees.logging.Log;

import org.opencv.core.CvType;
import org.opencv.core.Mat;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import static com.makeramen.roundedimageview.RoundedImageView.TAG;

/**
 * Implementation of the Android camera that retrieves the frames in OpenCV Mat format.
 * Based on:
 * - http://stackoverflow.com/a/24206165/6305235.
 * - http://stackoverflow.com/q/2386025/6305235.
 * Notes:
 * OpenCV JavaCameraView cannot be used in a service because it is a view. That's why we have
 * to implement our own camera to retrieve the frames in Mat format.
 * AndroidCamera on android platform can't stream video until it given valid preview surface.
 * We use an invisible SurfaceTexture as preview.
 * The camera is handled in a different thread.
 */
@SuppressWarnings("deprecation")
public class AndroidCameraImpl implements AndroidCamera, PreviewCallback {


    private final AndroidCameraListener user;
    private final CameraHandlerThread cameraHandlerThread;
    private final int cameraIndex;
    private Camera camera;
    private int maxFrameWidth;
    private int maxFrameHeight;
    private int zoomRatio;
    private long initialDelay;
    private long frameRate;
    private TakePhotoTask takePhotoTask;
    private Timer timer;
    private CameraFrame cameraFrame;
    private SurfaceTexture texture = new SurfaceTexture(0);

    /**
     * AndroidCameraImpl constructor.
     *
     * @param user           camera client.
     * @param cameraIndex    desired camera
     *                       (CameraInfo.CAMERA_FACING_FRONT or CameraInfo.CAMERA_FACING_BACK...).
     * @param maxFrameWidth  max. frame width.
     * @param maxFrameHeight max. frame height.
     * @param zoomRatio      zoom ratio of the desired zoom value.
     */
    public AndroidCameraImpl(AndroidCameraListener user, int cameraIndex,
                             int maxFrameWidth, int maxFrameHeight, int zoomRatio,
                             long initialDelay, long frameRate) {
        this.user = user;
        this.cameraIndex = cameraIndex;
        this.maxFrameWidth = maxFrameWidth;
        this.maxFrameHeight = maxFrameHeight;
        this.zoomRatio = zoomRatio;
        this.initialDelay = initialDelay;
        this.frameRate = frameRate;
        this.cameraHandlerThread = new CameraHandlerThread(this);
        this.takePhotoTask = new TakePhotoTask();
        this.timer = new Timer();
    }

    @Override
    public void onPreviewFrame(byte[] frame, Camera camera) {
        cameraFrame.putFrameData(frame);
        user.onPreviewFrame(cameraFrame);
    }

    @Override
    public synchronized void connect() {
        if (!user.isOpenCvLoaded()) {
            return;
        }
        // Start camera
        synchronized (cameraHandlerThread) {
            cameraHandlerThread.openCamera();
        }
    }

    @Override
    public void release() {
        synchronized (this) {
            // Stop task
            timer.cancel();
            timer = null;
            takePhotoTask = null;
            // Release thread
            cameraHandlerThread.interrupt();
            // Release camera
            if (camera != null) {
                camera.stopPreview();
                camera.setPreviewCallback(null);
                try {
                    camera.setPreviewTexture(null);
                } catch (IOException e) {
                    Log.e(e, "Could not release preview-texture from camera.");
                }
                camera.release();
                camera = null;
            }
            // Release camera frame
            if (cameraFrame != null) {
                cameraFrame.release();
            }
            // Release texture
            if (texture != null) {
                texture.release();
            }
        }
    }

    @Override
    public boolean isConnected() {
        return camera != null;
    }

    @Override
    public void updateFrameRate(long delay, long period) {
        this.timer.cancel();
        this.timer = new Timer();
        this.takePhotoTask = new TakePhotoTask();
        this.initialDelay = delay;
        this.frameRate = period;
        timer.scheduleAtFixedRate(takePhotoTask, delay, period);
    }

    /**
     * Starts capturing and converting preview frames.
     */
    @SuppressWarnings("ConstantConditions")
    void initCamera() {
        // Get camera instance
        camera = getCameraInstance(cameraIndex, maxFrameWidth, maxFrameHeight, zoomRatio);
        if (camera == null) {
            return;
        }
        // Save frame size
        Camera.Parameters params = camera.getParameters();
        int mFrameWidth = params.getPreviewSize().width;
        int mFrameHeight = params.getPreviewSize().height;
        // Create frame mat
        Mat mFrame = new Mat(mFrameHeight + (mFrameHeight / 2), mFrameWidth, CvType.CV_8UC1);
        cameraFrame = new CameraFrame(mFrame, mFrameWidth, mFrameHeight);
        // Config texture
        if (this.texture != null) {
            this.texture.release();
        }
        this.texture = new SurfaceTexture(0);
        // Call onCameraStart
        user.onCameraStarted(mFrameWidth, mFrameHeight);
        // Set camera callbacks and start capturing
        try {
            camera.setPreviewTexture(texture);
            camera.startPreview();
            timer.scheduleAtFixedRate(takePhotoTask, initialDelay, frameRate);
        } catch (Exception e) {
            Log.d(TAG, "Error starting camera preview: " + e.getMessage(), e);
        }
    }

    /**
     * Get an instance of the camera that meets the requirements (facing, size, zoom).
     *
     * @param facing CameraInfo.CAMERA_FACING_FRONT or CameraInfo.CAMERA_FACING_BACK.
     * @return camera instance or null if it is not available.
     */
    private Camera getCameraInstance(int facing, int maxFrameWidth,
                                     int maxFrameHeight, int zoomRatio) {
        // Get desired camera
        Camera cam = getCamera(facing);
        if (cam == null) {
            Log.e("Could not find any camera matching facing: " + facing);
            return null;
        }
        // Set frame size
        setFrameSize(cam, maxFrameWidth, maxFrameHeight);
        // Set zoom ratio
        setZoomRatio(cam, zoomRatio);
        // Set autofocus
        setAutofocus(cam);
        return cam;
    }

    /**
     * Gets and opens the desired camera if it exists.
     *
     * @param facing CameraInfo.CAMERA_FACING_FRONT or CameraInfo.CAMERA_FACING_BACK.
     * @return Camera (or null if it does not exist).
     */
    private Camera getCamera(int facing) {
        Camera cam = null;
        CameraInfo cameraInfo = new CameraInfo();
        int cameraCount = Camera.getNumberOfCameras();
        for (int camIndex = 0; camIndex < cameraCount; camIndex++) {
            Camera.getCameraInfo(camIndex, cameraInfo);
            if (cameraInfo.facing == facing) {
                try {
                    // Open camera
                    cam = Camera.open(camIndex);
                    break;
                } catch (RuntimeException e) {
                    Log.e(e, "AndroidCamera is not available (in use or does not exist).");
                }
            }
        }
        return cam;
    }

    /**
     * Sets the frame size with the given values.
     *
     * @param camera         camera to configure.
     * @param maxFrameWidth  max frame width (the closest smaller width will be chosen).
     * @param maxFrameHeight max frame height (the closest smaller height will be chosen).
     */
    private void setFrameSize(Camera camera, int maxFrameWidth, int maxFrameHeight) {
        Camera.Parameters params = camera.getParameters();
        List<Camera.Size> sizes = params.getSupportedPreviewSizes();
        Collections.sort(sizes, new PreviewSizeComparator());
        Camera.Size previewSize = null;
        for (Camera.Size size : sizes) {
            if (size == null || size.width > maxFrameWidth || size.height > maxFrameHeight) {
                break;
            }
            previewSize = size;
        }
        if (previewSize == null) {
            Log.e("Could not find any camera matching sizes: "
                    + maxFrameWidth + "x" + maxFrameHeight);
            return;
        }
        params.setPreviewSize(previewSize.width, previewSize.height);
        camera.setParameters(params);
    }

    /**
     * Sets the zoom ratio with the given value.
     *
     * @param camera    camera to configure.
     * @param zoomRatio zoom ratio.
     */
    private void setZoomRatio(Camera camera, int zoomRatio) {
        Camera.Parameters params = camera.getParameters();
        if (params.isZoomSupported()) {
            // Get supported ratios
            List<Integer> zoomRatios = params.getZoomRatios();
            // Chose closest ratio
            int i;
            for (i = 0; i < zoomRatios.size(); i++) {
                if (zoomRatio <= zoomRatios.get(i)) {
                    break;
                }
            }
            // Set zoom
            params.setZoom(i <= params.getMaxZoom() ? i : params.getMaxZoom());
            camera.setParameters(params);
        }
    }

    /**
     * Set focus mode.
     *
     * @param camera camera to configure.
     */
    private void setAutofocus(Camera camera) {
        Camera.Parameters params = camera.getParameters();
        if (params.getSupportedFocusModes().contains(
                Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
            params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
        }
        camera.setParameters(params);
    }

    // Timer task for continuous triggering of preview callbacks
    private class TakePhotoTask extends TimerTask {
        @Override
        public void run() {
            camera.setOneShotPreviewCallback(AndroidCameraImpl.this);
        }
    }
}