/** * Copyright (c) 2015 CommonsWare, LLC * <p/> * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * <p/> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.commonsware.cwac.cam2; import android.annotation.TargetApi; import android.content.Context; import android.graphics.ImageFormat; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.Image; import android.media.ImageReader; import android.media.MediaActionSound; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import android.view.Surface; import com.commonsware.cwac.cam2.util.Size; import org.greenrobot.eventbus.EventBus; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * Implementation of a CameraEngine that supports the * Android 5.0+ android.hardware.camera2 API. */ @SuppressWarnings("ResourceType") @TargetApi(Build.VERSION_CODES.LOLLIPOP) public class CameraTwoEngine extends CameraEngine { private final Context ctxt; private CameraManager mgr; final private HandlerThread handlerThread=new HandlerThread(getClass().getSimpleName(), android.os.Process.THREAD_PRIORITY_BACKGROUND); final private Handler handler; final private Semaphore lock=new Semaphore(1); // private CountDownLatch closeLatch=null; private MediaActionSound shutter=new MediaActionSound(); private List<Descriptor> descriptors=null; /** * Standard constructor * * @param ctxt any Context will do */ public CameraTwoEngine(Context ctxt) { this.ctxt=ctxt.getApplicationContext(); mgr=(CameraManager)this.ctxt. getSystemService(Context.CAMERA_SERVICE); handlerThread.start(); handler=new Handler(handlerThread.getLooper()); shutter.load(MediaActionSound.SHUTTER_CLICK); } /** * {@inheritDoc} */ @Override public CameraSession.Builder buildSession(Context ctxt, CameraDescriptor descriptor) { return(new SessionBuilder(ctxt, descriptor)); } /** * {@inheritDoc} */ @Override public void loadCameraDescriptors(final CameraSelectionCriteria criteria) { getThreadPool().execute(new Runnable() { @Override public void run() { if (descriptors==null) { List<Descriptor> result=new ArrayList<Descriptor>(); try { for (String cameraId : mgr.getCameraIdList()) { CameraCharacteristics cc= mgr.getCameraCharacteristics(cameraId); Descriptor camera=new Descriptor(cameraId, cc); StreamConfigurationMap map=cc.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); android.util.Size[] rawSizes= map.getOutputSizes(SurfaceTexture.class); CameraConstraints constraint=CameraConstraints.get(); camera.setFacingFront( cc.get(CameraCharacteristics.LENS_FACING)== CameraCharacteristics.LENS_FACING_FRONT); List<Size> sizes=null; if (constraint!=null) { if (camera.isFacingFront) { sizes=constraint.getPreviewFFCSizeWhitelist(); } else { sizes=constraint.getPreviewRFCSizeWhitelist(); } } if (sizes==null) { sizes=new ArrayList<Size>(); for (android.util.Size size : rawSizes) { if (size.getWidth()<2160 && size.getHeight()<2160) { sizes.add( new Size(size.getWidth(), size.getHeight())); } } } camera.setPreviewSizes(sizes); sizes=null; if (constraint!=null) { if (camera.isFacingFront) { sizes=constraint.getPictureFFCSizeWhitelist(); } else { sizes=constraint.getPictureRFCSizeWhitelist(); } } if (sizes==null) { sizes=new ArrayList<>(); for (android.util.Size size : map.getOutputSizes(ImageFormat.JPEG)) { sizes.add(new Size(size.getWidth(), size.getHeight())); } } camera.setPictureSizes(sizes); result.add(camera); } descriptors=result; } catch (CameraAccessException e) { getBus().post( new CameraEngine.CameraDescriptorsEvent(e)); if (isDebug()) { Log.e(getClass().getSimpleName(), "Exception accessing camera", e); } } } List<CameraDescriptor> result= new ArrayList<CameraDescriptor>(); for (Descriptor camera : descriptors) { if (!criteria.getFacingExactMatch() || camera.getScore(criteria)>0) { result.add(camera); } } Collections.sort(result, new Comparator<CameraDescriptor>() { @Override public int compare(CameraDescriptor descriptor, CameraDescriptor t1) { Descriptor lhs=(Descriptor)descriptor; Descriptor rhs=(Descriptor)t1; // descending, so invert normal side-ness return (Integer.compare(rhs.getScore(criteria), lhs.getScore(criteria))); } }); getBus().post( new CameraEngine.CameraDescriptorsEvent(result)); } }); } /** * {@inheritDoc} */ @Override public void open(final CameraSession session, final SurfaceTexture texture) { getThreadPool().execute(new Runnable() { @Override public void run() { Descriptor camera=(Descriptor)session.getDescriptor(); try { CameraCharacteristics cc= mgr.getCameraCharacteristics(camera.getId()); eligibleFlashModes.clear(); int[] availModes=cc.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); for (FlashMode flashMode : preferredFlashModes) { for (int rawFlashMode : availModes) { if (rawFlashMode==flashMode.getCameraTwoMode()) { eligibleFlashModes.add(flashMode); break; } } } if (eligibleFlashModes.isEmpty()) { for (int rawFlashMode : availModes) { FlashMode flashMode=FlashMode.lookupCameraTwoMode( rawFlashMode); if (flashMode!=null) { eligibleFlashModes.add(flashMode); } } } session.setCurrentFlashMode(eligibleFlashModes.get(0)); if (!lock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { throw new RuntimeException("Time out waiting to lock camera opening."); } mgr.openCamera(camera.getId(), new InitPreviewTransaction(session, new Surface(texture)), handler); } catch (Exception e) { getBus().post(new OpenedEvent(e)); if (isDebug()) { Log.e(getClass().getSimpleName(), "Exception opening camera", e); } } } }); } /** * {@inheritDoc} */ @Override public void close(final CameraSession session) { final Session s=(Session)session; try { lock.acquire(); if (s.captureSession != null) { // closeLatch=new CountDownLatch(1); s.captureSession.close(); // closeLatch.await(2, TimeUnit.SECONDS); s.captureSession=null; } if (s.cameraDevice != null) { s.cameraDevice.close(); s.cameraDevice=null; } if (s.reader != null) { s.reader.close(); } s.setClosed(true); Descriptor camera=(Descriptor)session.getDescriptor(); camera.setDevice(null); session.destroy(); getBus().post(new ClosedEvent()); } catch (Exception e) { getBus().post(new ClosedEvent(e)); } finally { lock.release(); } } /** * {@inheritDoc} */ public void takePicture(CameraSession session, PictureTransaction xact) { final Session s=(Session)session; s.reader.setOnImageAvailableListener(new TakePictureTransaction(session.getContext(), getBus(), xact), handler); getThreadPool().execute(new Runnable() { @Override public void run() { try { // This is how to tell the camera to lock focus. s.previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); s.captureSession.setRepeatingRequest( s.previewRequestBuilder.build(), new RequestCaptureTransaction(s), handler); } catch (Exception e) { getBus().post(new PictureTakenEvent(e)); if (isDebug()) { Log.e(getClass().getSimpleName(), "Exception taking picture", e); } } } }); } @Override public void handleOrientationChange(CameraSession session, OrientationChangedEvent event) { // TODO } @Override public void recordVideo(CameraSession session, VideoTransaction xact) { // TODO } @Override public void stopVideoRecording(CameraSession session, boolean abandon) { // TODO } /** * {@inheritDoc} */ @Override public boolean supportsDynamicFlashModes() { return(true); } @Override public boolean supportsZoom(CameraSession session) { boolean result=false; Descriptor descriptor=(Descriptor)session.getDescriptor(); try { CameraCharacteristics cc= mgr.getCameraCharacteristics(descriptor.cameraId); float maxZoom=cc.get( CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); result=(maxZoom>=1.0f); } catch (CameraAccessException e) { getBus().post(new DeepImpactEvent(e)); } return(result); } @Override public boolean zoomTo(CameraSession session, int zoomLevel) { final Session s=(Session)session; if (session!=null) { final Descriptor descriptor=(Descriptor)session.getDescriptor(); if (s.previewRequest!=null) { try { final CameraCharacteristics cc= mgr.getCameraCharacteristics(descriptor.cameraId); final float maxZoom= cc.get( CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); // if <=1, zoom not possible, so eat the the event if (maxZoom>1.0f) { float zoomTo=1.0f+((float)zoomLevel*(maxZoom-1.0f)/100.0f); Rect zoomRect=cropRegionForZoom(cc, zoomTo); s.previewRequestBuilder .set(CaptureRequest.SCALER_CROP_REGION, zoomRect); s.setZoomRect(zoomRect); s.previewRequest=s.previewRequestBuilder.build(); s.captureSession.setRepeatingRequest(s.previewRequest, null, handler); } } catch (CameraAccessException e) { getBus().post(new DeepImpactEvent(e)); } } } return(false); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static Rect cropRegionForZoom(CameraCharacteristics cc, float zoomTo) { Rect sensor= cc.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); int sensorCenterX=sensor.width()/2; int sensorCenterY=sensor.height()/2; int deltaX=(int)(0.5f*sensor.width()/zoomTo); int deltaY=(int)(0.5f*sensor.height()/zoomTo); return(new Rect( sensorCenterX-deltaX, sensorCenterY-deltaY, sensorCenterX+deltaX, sensorCenterY+deltaY)); } private class InitPreviewTransaction extends CameraDevice.StateCallback { private final Session s; private final Surface surface; InitPreviewTransaction(CameraSession session, Surface surface) { this.s=(Session)session; this.surface=surface; } @Override public void onOpened(CameraDevice cameraDevice) { lock.release(); s.cameraDevice=cameraDevice; s.reader=s.buildImageReader(); Descriptor camera=(Descriptor)s.getDescriptor(); camera.setDevice(cameraDevice); try { cameraDevice.createCaptureSession( Arrays.asList(surface, s.reader.getSurface()), new StartPreviewTransaction(s, surface), handler); } catch (CameraAccessException e) { getBus().post(new OpenedEvent(e)); } catch (IllegalStateException e2) { getBus().post(new DeepImpactEvent(e2)); } } @Override public void onDisconnected(CameraDevice cameraDevice) { lock.release(); cameraDevice.close(); } @Override public void onError(CameraDevice cameraDevice, int i) { lock.release(); cameraDevice.close(); getBus().post(new CameraTwoPreviewErrorEvent(i)); } @Override public void onClosed(CameraDevice camera) { super.onClosed(camera); /* if (closeLatch != null) { closeLatch.countDown(); } */ } } private class StartPreviewTransaction extends CameraCaptureSession.StateCallback { private final Surface surface; private final Session s; StartPreviewTransaction(CameraSession session, Surface surface) { this.s=(Session)session; this.surface=surface; } @Override public void onConfigured(CameraCaptureSession session) { try { if (!s.isClosed()) { s.captureSession=session; s.previewRequestBuilder=session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); s.previewRequestBuilder.addTarget(surface); s.previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); s.previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); Descriptor camera=(Descriptor)s.getDescriptor(); CameraCharacteristics cc=mgr.getCameraCharacteristics(camera.cameraId); if (s.getZoomRect()!=null) { s .previewRequestBuilder .set(CaptureRequest.SCALER_CROP_REGION, s.getZoomRect()); } s.addToPreviewRequest(cc, s.previewRequestBuilder); s.previewRequest=s.previewRequestBuilder.build(); session.setRepeatingRequest(s.previewRequest, null, handler); getBus().post(new OpenedEvent()); } } catch (CameraAccessException e) { getBus().post(new OpenedEvent(e)); } catch (IllegalStateException e) { if (isDebug()) { Log.w(getClass().getSimpleName(), "Exception resetting focus", e); } } } @Override public void onConfigureFailed(CameraCaptureSession session) { getBus().post(new CameraTwoPreviewFailureEvent()); } } private class RequestCaptureTransaction extends CameraCaptureSession.CaptureCallback { private final Session s; boolean isWaitingForFocus=true; boolean isWaitingForPrecapture=false; boolean haveWeStartedCapture=false; RequestCaptureTransaction(CameraSession session) { this.s=(Session)session; } @Override public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) { capture(partialResult); } @Override public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { getBus().post(new PictureTakenEvent(new RuntimeException("generic camera2 capture failure"))); } @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { capture(result); } private void capture(CaptureResult result) { if (isWaitingForFocus) { isWaitingForFocus=false; Integer autoFocusState=result.get(CaptureResult.CONTROL_AF_STATE); if (autoFocusState!=null && (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == autoFocusState || CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == autoFocusState)) { Integer state=result.get(CaptureResult.CONTROL_AE_STATE); if (state == null || state == CaptureResult.CONTROL_AE_STATE_CONVERGED) { isWaitingForPrecapture=false; haveWeStartedCapture=true; capture(s); } else { isWaitingForPrecapture=true; precapture(s); } } } else if (isWaitingForPrecapture) { Integer state=result.get(CaptureResult.CONTROL_AE_STATE); if (state == null || state == CaptureResult.CONTROL_AE_STATE_PRECAPTURE || state == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) { isWaitingForPrecapture=false; } } else if (!haveWeStartedCapture) { Integer state=result.get(CaptureResult.CONTROL_AE_STATE); if (state == null || state != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { haveWeStartedCapture=true; capture(s); } } } private void precapture(Session s) { try { s.previewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); s.captureSession.capture(s.previewRequestBuilder.build(), this, handler); } catch (Exception e) { getBus().post(new PictureTakenEvent(e)); if (isDebug()) { Log.e(getClass().getSimpleName(), "Exception running precapture", e); } } } private void capture(Session s) { try { CaptureRequest.Builder captureBuilder= s.cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(s.reader.getSurface()); captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); Descriptor camera=(Descriptor)s.getDescriptor(); CameraCharacteristics cc=mgr.getCameraCharacteristics(camera.cameraId); if (s.getZoomRect()!=null) { captureBuilder .set(CaptureRequest.SCALER_CROP_REGION, s.getZoomRect()); } s.addToCaptureRequest(cc, camera.isFacingFront, captureBuilder); s.captureSession.stopRepeating(); s.captureSession.capture(captureBuilder.build(), new CapturePictureTransaction(s), null); } catch (Exception e) { getBus().post(new PictureTakenEvent(e)); if (isDebug()) { Log.e(getClass().getSimpleName(), "Exception running capture", e); } } } } private class CapturePictureTransaction extends CameraCaptureSession.CaptureCallback { private final Session s; CapturePictureTransaction(CameraSession session) { this.s=(Session)session; } @Override public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) { super.onCaptureStarted(session, request, timestamp, frameNumber); shutter.play(MediaActionSound.SHUTTER_CLICK); } @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { // TODO: something useful with the picture unlockFocus(); } @Override public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { getBus() .post(new PictureTakenEvent(new RuntimeException("generic camera2 capture failure"))); } private void unlockFocus() { try { if (!s.isClosed()) { s.previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); s.previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); CameraCaptureSession session=s.captureSession; if (session!=null) { session.capture(s.previewRequestBuilder.build(), null, handler); session.setRepeatingRequest(s.previewRequest, null, handler); } } } catch (CameraAccessException e) { getBus().post(new PictureTakenEvent(e)); if (isDebug()) { Log.e(getClass().getSimpleName(), "Exception resetting focus", e); } } catch (IllegalStateException e) { getBus().post(new DeepImpactEvent(e)); if (isDebug()) { Log.w(getClass().getSimpleName(), "Exception resetting focus", e); } } } } private static class AreaComparator implements Comparator<Size> { @Override public int compare(Size lhs, Size rhs) { long lhArea=(long)lhs.getWidth() * lhs.getHeight(); long rhArea=(long)rhs.getWidth() * rhs.getHeight(); return(Long.signum(lhArea - rhArea)); } } static class Descriptor implements CameraDescriptor { private final String cameraId; private CameraDevice device; private List<Size> pictureSizes; private List<Size> previewSizes; private boolean isFacingFront; private final Integer facing; private Descriptor(String cameraId, CameraCharacteristics cc) { this.cameraId=cameraId; facing=cc.get(CameraCharacteristics.LENS_FACING); } public String getId() { return (cameraId); } private void setDevice(CameraDevice device) { this.device=device; } private CameraDevice getDevice() { return (device); } @Override public boolean isPictureFormatSupported(int format) { return(ImageFormat.JPEG == format); } @Override public List<Size> getPreviewSizes() { return(previewSizes); } private void setPreviewSizes(List<Size> sizes) { previewSizes=sizes; } @Override public List<Size> getPictureSizes() { return(pictureSizes); } private void setPictureSizes(List<Size> sizes) { pictureSizes=sizes; } private void setFacingFront(boolean isFacingFront) { this.isFacingFront=isFacingFront; } private int getScore(CameraSelectionCriteria criteria) { int score=10; if (criteria != null) { if ((criteria.getFacing().isFront() && facing!=CameraCharacteristics.LENS_FACING_FRONT) || (!criteria.getFacing().isFront() && facing!=CameraCharacteristics.LENS_FACING_BACK)) { score=0; } } return(score); } } private static class Session extends CameraSession { CameraDevice cameraDevice=null; CameraCaptureSession captureSession=null; CaptureRequest.Builder previewRequestBuilder=null; CaptureRequest previewRequest; ImageReader reader; boolean isClosed=false; Rect zoomRect=null; private Session(Context ctxt, CameraDescriptor descriptor) { super(ctxt, descriptor); } ImageReader buildImageReader() { ImageReader result=null; for (CameraPlugin plugin : getPlugins()) { CameraTwoConfigurator configurator=plugin.buildConfigurator(CameraTwoConfigurator.class); if (configurator!=null) { result=configurator.buildImageReader(); } if (result!=null) break; } return(result); } void addToCaptureRequest(CameraCharacteristics cc, boolean isFacingFront, CaptureRequest.Builder captureBuilder) { for (CameraPlugin plugin : getPlugins()) { CameraTwoConfigurator configurator=plugin.buildConfigurator(CameraTwoConfigurator.class); if (configurator!=null) { configurator.addToCaptureRequest(this, cc, isFacingFront, captureBuilder); } } } void addToPreviewRequest(CameraCharacteristics cc, CaptureRequest.Builder captureBuilder) { for (CameraPlugin plugin : getPlugins()) { CameraTwoConfigurator configurator=plugin.buildConfigurator(CameraTwoConfigurator.class); if (configurator!=null) { configurator.addToPreviewRequest(this, cc, captureBuilder); } } } boolean isClosed() { return(isClosed); } void setClosed(boolean isClosed) { this.isClosed=isClosed; } Rect getZoomRect() { return(zoomRect); } void setZoomRect(Rect zoomRect) { this.zoomRect=zoomRect; } } private static class SessionBuilder extends CameraSession.Builder { private SessionBuilder(Context ctxt, CameraDescriptor descriptor) { super(new Session(ctxt, descriptor)); } } private static class TakePictureTransaction implements ImageReader.OnImageAvailableListener { private final EventBus bus; private final PictureTransaction xact; private final Context ctxt; TakePictureTransaction(Context ctxt, EventBus bus, PictureTransaction xact) { this.bus=bus; this.xact=xact; this.ctxt=ctxt.getApplicationContext(); } @Override public void onImageAvailable(ImageReader imageReader) { Image image=imageReader.acquireNextImage(); ByteBuffer buffer=image.getPlanes()[0].getBuffer(); byte[] bytes=new byte[buffer.remaining()]; buffer.get(bytes); image.close(); bus.post(new PictureTakenEvent(xact, xact.process(new ImageContext(ctxt, bytes)))); } } }