I encapsulated the usage of the Android camera into one single class (CameraAccess) that uses an invisible SurfaceTexture as preview and which implements the Camera.PreviewCallback. Within this callback I get the byte array of the current frame which I then want to use on multiple views/fragments.
My problem is the life-cycle management. Usually the camera is used within a single View and initialized/release in onSurfaceCreated and onSurfaceDestroyed (see SurfaceHolder.Callback). But in my scenario I need to use the preview on more than one View. Each view adds itself as a callback to the CameraAccess class.
I thought to put the CameraAccess as a member into the Application class. But when you press the home button then the Application is still alive but all views are destroyed. How would you handle the init and release of the camera?
Here's how I solved it.
I put all the logic into a singleton class called CameraAccess. All views that want to access the camera/preview will implement CameraFrameCallback and add/remove themselves in onSurfaceCreated/onSurfaceDestroyed. Whenever a frame is received, it will be handled by an encapsulated frame class that takes care of color-space conversion and conversion to bitmap. It is also responsible to allocate memory for a Bitmap that is then used by all views when they draw the content onto their canvas. As you will see, I used OpenCV for easy color-space conversion. OpenCV Mat (Matrix class) is also good for later image processing.
public class CameraAccess implements Camera.PreviewCallback,
LoaderCallbackInterface {
// see http://developer.android.com/guide/topics/media/camera.html for more
// details
final static String TAG = "CameraAccess";
Context context;
int cameraIndex; // example: CameraInfo.CAMERA_FACING_FRONT or
// CameraInfo.CAMERA_FACING_BACK
Camera mCamera;
int mFrameWidth;
int mFrameHeight;
Mat mFrame;
CameraAccessFrame mCameraFrame;
List<CameraFrameCallback> mCallbacks = new ArrayList<CameraFrameCallback>();
boolean mOpenCVloaded;
byte mBuffer[]; // needed to avoid OpenCV error:
// "queueBuffer: BufferQueue has been abandoned!"
private static CameraAccess mInstance;
public static CameraAccess getInstance(Context context, int cameraIndex) {
if (mInstance != null)
return mInstance;
mInstance = new CameraAccess(context, cameraIndex);
return mInstance;
}
private CameraAccess(Context context, int cameraIndex) {
this.context = context;
this.cameraIndex = cameraIndex;
if (!OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_7, context,
this)) {
Log.e(TAG, "Cannot connect to OpenCVManager");
} else
Log.d(TAG, "OpenCVManager successfully connected");
}
private boolean checkCameraHardware() {
if (context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA)) {
// this device has a camera
return true;
} else {
// no camera on this device
return false;
}
}
public static Camera getCameraInstance(int cameraIndex) {
Camera c = null;
try {
c = Camera.open(cameraIndex); // attempt to get a
// Camera
// instance
Log.d(TAG, "Camera opened. index: " + cameraIndex);
} catch (Exception e) {
// Camera is not available (in use or does not exist)
}
return c; // returns null if camera is unavailable
}
public void addCallback(CameraFrameCallback callback) {
// we don't care if the callback is already in the list
this.mCallbacks.add(callback);
if (mCamera != null)
callback.onCameraInitialized(mFrameWidth, mFrameHeight);
else if (mOpenCVloaded)
connectCamera();
}
public void removeCallback(CameraFrameCallback callback) {
boolean removed = false;
do {
// someone might have added the callback multiple times
removed = this.mCallbacks.remove(callback);
if (removed)
callback.onCameraReleased();
} while (removed == true);
if (mCallbacks.size() == 0)
releaseCamera();
}
@Override
public void onPreviewFrame(byte[] frame, Camera arg1) {
mFrame.put(0, 0, frame);
mCameraFrame.invalidate();
for (CameraFrameCallback callback : mCallbacks)
callback.onFrameReceived(mCameraFrame);
if (mCamera != null)
mCamera.addCallbackBuffer(mBuffer);
}
private void connectCamera() {
synchronized (this) {
if (true) {// checkCameraHardware()) {
mCamera = getCameraInstance(cameraIndex);
Parameters params = mCamera.getParameters();
List<Camera.Size> sizes = params.getSupportedPreviewSizes();
// Camera.Size previewSize = sizes.get(0);
Collections.sort(sizes, new PreviewSizeComparer());
Camera.Size previewSize = null;
for (Camera.Size s : sizes) {
if (s == null)
break;
previewSize = s;
}
// List<Integer> formats = params.getSupportedPictureFormats();
// params.setPreviewFormat(ImageFormat.NV21);
params.setPreviewSize(previewSize.width, previewSize.height);
mCamera.setParameters(params);
params = mCamera.getParameters();
mFrameWidth = params.getPreviewSize().width;
mFrameHeight = params.getPreviewSize().height;
int size = mFrameWidth * mFrameHeight;
size = size
* ImageFormat
.getBitsPerPixel(params.getPreviewFormat()) / 8;
mBuffer = new byte[size];
mFrame = new Mat(mFrameHeight + (mFrameHeight / 2),
mFrameWidth, CvType.CV_8UC1);
mCameraFrame = new CameraAccessFrame(mFrame, mFrameWidth,
mFrameHeight);
SurfaceTexture texture = new SurfaceTexture(0);
try {
mCamera.setPreviewTexture(texture);
mCamera.addCallbackBuffer(mBuffer);
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.startPreview();
Log.d(TAG, "Camera preview started");
} catch (Exception e) {
Log.d(TAG,
"Error starting camera preview: " + e.getMessage());
}
for (CameraFrameCallback callback : mCallbacks)
callback.onCameraInitialized(mFrameWidth, mFrameHeight);
}
}
}
private void releaseCamera() {
synchronized (this) {
if (mCamera != null) {
mCamera.stopPreview();
mCamera.setPreviewCallback(null);
mCamera.release();
Log.d(TAG, "Preview stopped and camera released");
}
mCamera = null;
if (mFrame != null) {
mFrame.release();
}
if (mCameraFrame != null) {
mCameraFrame.release();
}
for (CameraFrameCallback callback : mCallbacks)
callback.onCameraReleased();
}
}
private class CameraAccessFrame implements CameraFrame {
private Mat mYuvFrameData;
private Mat mRgba;
private int mWidth;
private int mHeight;
private Bitmap mCachedBitmap;
private boolean mRgbaConverted;
private boolean mBitmapConverted;
@Override
public Mat gray() {
return mYuvFrameData.submat(0, mHeight, 0, mWidth);
}
@Override
public Mat rgba() {
if (!mRgbaConverted) {
Imgproc.cvtColor(mYuvFrameData, mRgba,
Imgproc.COLOR_YUV2BGR_NV12, 4);
mRgbaConverted = true;
}
return mRgba;
}
@Override
public Bitmap toBitmap() {
if (mBitmapConverted)
return mCachedBitmap;
Mat rgba = this.rgba();
Utils.matToBitmap(rgba, mCachedBitmap);
mBitmapConverted = true;
return mCachedBitmap;
}
public CameraAccessFrame(Mat Yuv420sp, int width, int height) {
super();
mWidth = width;
mHeight = height;
mYuvFrameData = Yuv420sp;
mRgba = new Mat();
this.mCachedBitmap = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
}
public void release() {
mRgba.release();
mCachedBitmap.recycle();
}
public void invalidate() {
mRgbaConverted = false;
mBitmapConverted = false;
}
};
public interface CameraFrameCallback {
void onCameraInitialized(int frameWidth, int frameHeight);
void onFrameReceived(CameraFrame frame);
void onCameraReleased();
}
@Override
public void onManagerConnected(int status) {
mOpenCVloaded = true;
if (mCallbacks.size() > 0)
connectCamera();
}
@Override
public void onPackageInstall(int operation,
InstallCallbackInterface callback) {
}
private class PreviewSizeComparer implements Comparator<Camera.Size> {
@Override
public int compare(Size arg0, Size arg1) {
if (arg0 != null && arg1 == null)
return -1;
if (arg0 == null && arg1 != null)
return 1;
if (arg0.width < arg1.width)
return -1;
else if (arg0.width > arg1.width)
return 1;
else
return 0;
}
}
}
public class CameraCanvasView extends SurfaceView implements CameraFrameCallback, SurfaceHolder.Callback {
Context context;
CameraAccess mCamera;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Rect mBackgroundSrc = new Rect();
public CameraCanvasView(Context context) {
super(context);
this.context = context;
SurfaceHolder sh = this.getHolder();
sh.addCallback(this);
setFocusable(true);
this.mCamera = CameraAccess.getInstance(context,
CameraInfo.CAMERA_FACING_BACK);
}
@Override
public void onCameraInitialized(int frameWidth, int frameHeight) {
}
@Override
public void onFrameReceived(CameraFrame frame) {
this.setBackgroundImage(frame.toBitmap());
}
@Override
public void onCameraReleased() {
setBackgroundImage(null);
}
@Override
public void surfaceCreated(SurfaceHolder arg0) {
this.setWillNotDraw(false);
this.mCamera.addCallback(this);
}
@Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
}
@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
this.mCamera.removeCallback(this);
}
public void setBackgroundImage(Bitmap image) {
this.mBackground = image;
if (image != null)
this.mBackgroundSrc.set(0, 0, image.getWidth(), image.getHeight());
else
this.mBackgroundSrc.setEmpty();
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
if (mBackground != null && !mBackground.isRecycled())
canvas.drawBitmap(mBackground, mBackgroundSrc, boundingBox, paint);
}
}