app/src/main/java/com/davidmiguel/gobees/monitoring/camera/AndroidCameraImpl.java
/*
* 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);
}
}
}