| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * 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 |
| * |
| * 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.android.camera.one.v2; |
| |
| 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.CameraMetadata; |
| import android.hardware.camera2.CaptureRequest; |
| import android.hardware.camera2.CaptureResult; |
| import android.hardware.camera2.DngCreator; |
| import android.hardware.camera2.TotalCaptureResult; |
| import android.hardware.camera2.params.MeteringRectangle; |
| import android.hardware.camera2.params.StreamConfigurationMap; |
| import android.location.Location; |
| import android.media.Image; |
| import android.media.ImageReader; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.SystemClock; |
| import android.view.Surface; |
| |
| import com.android.camera.CaptureModuleUtil; |
| import com.android.camera.Exif; |
| import com.android.camera.Storage; |
| import com.android.camera.debug.DebugPropertyHelper; |
| import com.android.camera.debug.Log; |
| import com.android.camera.debug.Log.Tag; |
| import com.android.camera.exif.ExifInterface; |
| import com.android.camera.exif.ExifTag; |
| import com.android.camera.exif.Rational; |
| import com.android.camera.one.AbstractOneCamera; |
| import com.android.camera.one.CameraDirectionProvider; |
| import com.android.camera.one.OneCamera; |
| import com.android.camera.one.Settings3A; |
| import com.android.camera.one.v2.camera2proxy.AndroidCaptureResultProxy; |
| import com.android.camera.one.v2.camera2proxy.AndroidImageProxy; |
| import com.android.camera.one.v2.camera2proxy.CaptureResultProxy; |
| import com.android.camera.processing.imagebackend.TaskImageContainer; |
| import com.android.camera.session.CaptureSession; |
| import com.android.camera.ui.focus.LensRangeCalculator; |
| import com.android.camera.ui.motion.LinearScale; |
| import com.android.camera.util.CameraUtil; |
| import com.android.camera.util.CaptureDataSerializer; |
| import com.android.camera.util.ExifUtil; |
| import com.android.camera.util.JpegUtilNative; |
| import com.android.camera.util.Size; |
| import com.google.common.base.Optional; |
| import com.google.common.util.concurrent.FutureCallback; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| import com.google.common.util.concurrent.MoreExecutors; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| /** |
| * {@link OneCamera} implementation directly on top of the Camera2 API for |
| * cameras without API 2 FULL support (limited or legacy). |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| public class OneCameraImpl extends AbstractOneCamera { |
| /** Captures that are requested but haven't completed yet. */ |
| private static class InFlightCapture { |
| final PhotoCaptureParameters parameters; |
| final CaptureSession session; |
| Image image; |
| TotalCaptureResult totalCaptureResult; |
| |
| public InFlightCapture(PhotoCaptureParameters parameters, |
| CaptureSession session) { |
| this.parameters = parameters; |
| this.session = session; |
| } |
| |
| /** Set the image once it's been received. */ |
| public InFlightCapture setImage(Image capturedImage) { |
| image = capturedImage; |
| return this; |
| } |
| |
| /** Set the total capture result once it's been received. */ |
| public InFlightCapture setCaptureResult(TotalCaptureResult result) { |
| totalCaptureResult = result; |
| return this; |
| } |
| |
| /** |
| * Returns whether the capture is complete (which is the case once the |
| * image and capture result are both present. |
| */ |
| boolean isCaptureComplete() { |
| return image != null && totalCaptureResult != null; |
| } |
| } |
| |
| private static final Tag TAG = new Tag("OneCameraImpl2"); |
| |
| /** If true, will write data about each capture request to disk. */ |
| private static final boolean DEBUG_WRITE_CAPTURE_DATA = DebugPropertyHelper.writeCaptureData(); |
| /** If true, will log per-frame AF info. */ |
| private static final boolean DEBUG_FOCUS_LOG = DebugPropertyHelper.showFrameDebugLog(); |
| |
| /** Default JPEG encoding quality. */ |
| private static final Byte JPEG_QUALITY = 90; |
| |
| /** |
| * Set to ImageFormat.JPEG, to use the hardware encoder, or |
| * ImageFormat.YUV_420_888 to use the software encoder. You can also try |
| * RAW_SENSOR experimentally. |
| */ |
| private static final int sCaptureImageFormat = DebugPropertyHelper.isCaptureDngEnabled() ? |
| ImageFormat.RAW_SENSOR : ImageFormat.JPEG; |
| |
| /** Duration to hold after manual focus tap. */ |
| private static final int FOCUS_HOLD_MILLIS = Settings3A.getFocusHoldMillis(); |
| /** Zero weight 3A region, to reset regions per API. */ |
| private static final MeteringRectangle[] ZERO_WEIGHT_3A_REGION = AutoFocusHelper |
| .getZeroWeightRegion(); |
| |
| /** |
| * CaptureRequest tags. |
| * <ul> |
| * <li>{@link #PRESHOT_TRIGGERED_AF}</li> |
| * <li>{@link #CAPTURE}</li> |
| * </ul> |
| */ |
| public static enum RequestTag { |
| /** Request that is part of a pre shot trigger. */ |
| PRESHOT_TRIGGERED_AF, |
| /** Capture request (purely for logging). */ |
| CAPTURE, |
| /** Tap to focus (purely for logging). */ |
| TAP_TO_FOCUS |
| } |
| |
| /** Directory to store raw DNG files in. */ |
| private static final File RAW_DIRECTORY = new File(Storage.DIRECTORY, "DNG"); |
| |
| /** Current CONTROL_AF_MODE request to Camera2 API. */ |
| private int mControlAFMode = CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE; |
| /** Last OneCamera.AutoFocusState reported. */ |
| private AutoFocusState mLastResultAFState = AutoFocusState.INACTIVE; |
| /** Flag to take a picture when the lens is stopped. */ |
| private boolean mTakePictureWhenLensIsStopped = false; |
| /** Takes a (delayed) picture with appropriate parameters. */ |
| private Runnable mTakePictureRunnable; |
| /** Keep PictureCallback for last requested capture. */ |
| private PictureCallback mLastPictureCallback = null; |
| /** Last time takePicture() was called in uptimeMillis. */ |
| private long mTakePictureStartMillis; |
| /** Runnable that returns to CONTROL_AF_MODE = AF_CONTINUOUS_PICTURE. */ |
| private final Runnable mReturnToContinuousAFRunnable = new Runnable() { |
| @Override |
| public void run() { |
| mAFRegions = ZERO_WEIGHT_3A_REGION; |
| mAERegions = ZERO_WEIGHT_3A_REGION; |
| mControlAFMode = CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE; |
| repeatingPreview(null); |
| } |
| }; |
| |
| /** Current zoom value. 1.0 is no zoom. */ |
| private float mZoomValue = 1f; |
| /** Current crop region: set from mZoomValue. */ |
| private Rect mCropRegion; |
| /** Current AF and AE regions */ |
| private MeteringRectangle[] mAFRegions = ZERO_WEIGHT_3A_REGION; |
| private MeteringRectangle[] mAERegions = ZERO_WEIGHT_3A_REGION; |
| /** Last frame for which CONTROL_AF_STATE was received. */ |
| private long mLastControlAfStateFrameNumber = 0; |
| |
| /** |
| * Common listener for preview frame metadata. |
| */ |
| private final CameraCaptureSession.CaptureCallback mCaptureCallback = |
| new CameraCaptureSession.CaptureCallback() { |
| @Override |
| public void onCaptureStarted(CameraCaptureSession session, |
| CaptureRequest request, long timestamp, |
| long frameNumber) { |
| if (request.getTag() == RequestTag.CAPTURE |
| && mLastPictureCallback != null) { |
| mLastPictureCallback.onQuickExpose(); |
| } |
| } |
| |
| // AF state information is sometimes available 1 frame before |
| // onCaptureCompleted(), so we take advantage of that. |
| @Override |
| public void onCaptureProgressed(CameraCaptureSession session, |
| CaptureRequest request, CaptureResult partialResult) { |
| autofocusStateChangeDispatcher(partialResult); |
| super.onCaptureProgressed(session, request, partialResult); |
| } |
| |
| @Override |
| public void onCaptureCompleted(CameraCaptureSession session, |
| CaptureRequest request, TotalCaptureResult result) { |
| autofocusStateChangeDispatcher(result); |
| // This checks for a HAL implementation error where |
| // TotalCaptureResult |
| // is missing CONTROL_AF_STATE. This should not happen. |
| if (result.get(CaptureResult.CONTROL_AF_STATE) == null) { |
| AutoFocusHelper.checkControlAfState(result); |
| } |
| if (DEBUG_FOCUS_LOG) { |
| AutoFocusHelper.logExtraFocusInfo(result); |
| } |
| |
| Float diopter = result.get(CaptureResult.LENS_FOCUS_DISTANCE); |
| if (diopter != null && mFocusDistanceListener != null) { |
| mFocusDistanceListener.onFocusDistance(diopter, mLensRange); |
| } |
| |
| if (request.getTag() == RequestTag.CAPTURE) { |
| // Add the capture result to the latest in-flight |
| // capture. If all the data for that capture is |
| // complete, store the image on disk. |
| InFlightCapture capture = null; |
| synchronized (mCaptureQueue) { |
| if (mCaptureQueue.getFirst().setCaptureResult(result) |
| .isCaptureComplete()) { |
| capture = mCaptureQueue.removeFirst(); |
| } |
| } |
| if (capture != null) { |
| OneCameraImpl.this.onCaptureCompleted(capture); |
| } |
| } |
| super.onCaptureCompleted(session, request, result); |
| } |
| }; |
| /** Thread on which the camera operations are running. */ |
| private final HandlerThread mCameraThread; |
| /** Handler of the {@link #mCameraThread}. */ |
| private final Handler mCameraHandler; |
| /** The characteristics of this camera. */ |
| private final CameraCharacteristics mCharacteristics; |
| private final LinearScale mLensRange; |
| /** The underlying Camera2 API camera device. */ |
| private final CameraDevice mDevice; |
| private final CameraDirectionProvider mDirectionProvider; |
| |
| /** |
| * The aspect ratio (width/height) of the full resolution for this camera. |
| * Usually the native aspect ratio of this camera. |
| */ |
| private final float mFullSizeAspectRatio; |
| /** The Camera2 API capture session currently active. */ |
| private CameraCaptureSession mCaptureSession; |
| /** The surface onto which to render the preview. */ |
| private Surface mPreviewSurface; |
| /** |
| * A queue of capture requests that have been requested but are not done |
| * yet. |
| */ |
| private final LinkedList<InFlightCapture> mCaptureQueue = |
| new LinkedList<InFlightCapture>(); |
| /** Whether closing of this device has been requested. */ |
| private volatile boolean mIsClosed = false; |
| |
| /** Receives the normal captured images. */ |
| private final ImageReader mCaptureImageReader; |
| ImageReader.OnImageAvailableListener mCaptureImageListener = |
| new ImageReader.OnImageAvailableListener() { |
| @Override |
| public void onImageAvailable(ImageReader reader) { |
| // Add the image data to the latest in-flight capture. |
| // If all the data for that capture is complete, store the |
| // image data. |
| InFlightCapture capture = null; |
| synchronized (mCaptureQueue) { |
| if (mCaptureQueue.getFirst().setImage(reader.acquireLatestImage()) |
| .isCaptureComplete()) { |
| capture = mCaptureQueue.removeFirst(); |
| } |
| } |
| if (capture != null) { |
| onCaptureCompleted(capture); |
| } |
| } |
| }; |
| |
| /** |
| * Instantiates a new camera based on Camera 2 API. |
| * |
| * @param device The underlying Camera 2 device. |
| * @param characteristics The device's characteristics. |
| * @param pictureSize the size of the final image to be taken. |
| */ |
| OneCameraImpl(CameraDevice device, CameraCharacteristics characteristics, Size pictureSize) { |
| mDevice = device; |
| mCharacteristics = characteristics; |
| mLensRange = LensRangeCalculator.getDiopterToRatioCalculator(characteristics); |
| mDirectionProvider = new CameraDirectionProvider(characteristics); |
| mFullSizeAspectRatio = calculateFullSizeAspectRatio(characteristics); |
| |
| // Override pictureSize for RAW (our picture size settings don't include |
| // RAW, which typically only supports one size (sensor size). This also |
| // typically differs from the larges JPEG or YUV size. |
| // TODO: If we ever want to support RAW properly, it should be one entry |
| // in the picture quality list, which should then lead to the right |
| // pictureSize being passes into here. |
| if (sCaptureImageFormat == ImageFormat.RAW_SENSOR) { |
| pictureSize = getDefaultPictureSize(); |
| } |
| |
| mCameraThread = new HandlerThread("OneCamera2"); |
| mCameraThread.start(); |
| mCameraHandler = new Handler(mCameraThread.getLooper()); |
| |
| mCaptureImageReader = ImageReader.newInstance(pictureSize.getWidth(), |
| pictureSize.getHeight(), |
| sCaptureImageFormat, 2); |
| mCaptureImageReader.setOnImageAvailableListener(mCaptureImageListener, mCameraHandler); |
| Log.d(TAG, "New Camera2 based OneCameraImpl created."); |
| } |
| |
| /** |
| * Take picture, initiating an auto focus scan if needed. |
| */ |
| @Override |
| public void takePicture(final PhotoCaptureParameters params, final CaptureSession session) { |
| // Do not do anything when a picture is already requested. |
| if (mTakePictureWhenLensIsStopped) { |
| return; |
| } |
| |
| // Not ready until the picture comes back. |
| broadcastReadyState(false); |
| |
| mTakePictureRunnable = new Runnable() { |
| @Override |
| public void run() { |
| takePictureNow(params, session); |
| } |
| }; |
| mLastPictureCallback = params.callback; |
| mTakePictureStartMillis = SystemClock.uptimeMillis(); |
| |
| // This class implements a very simple version of AF, which |
| // only delays capture if the lens is scanning. |
| if (mLastResultAFState == AutoFocusState.ACTIVE_SCAN) { |
| Log.v(TAG, "Waiting until scan is done before taking shot."); |
| mTakePictureWhenLensIsStopped = true; |
| } else { |
| // We could do CONTROL_AF_TRIGGER_START and wait until lens locks, |
| // but this would slow down the capture. |
| takePictureNow(params, session); |
| } |
| } |
| |
| /** |
| * Take picture immediately. Parameters passed through from takePicture(). |
| */ |
| public void takePictureNow(PhotoCaptureParameters params, CaptureSession session) { |
| long dt = SystemClock.uptimeMillis() - mTakePictureStartMillis; |
| Log.v(TAG, "Taking shot with extra AF delay of " + dt + " ms."); |
| try { |
| // JPEG capture. |
| CaptureRequest.Builder builder = mDevice |
| .createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); |
| builder.setTag(RequestTag.CAPTURE); |
| addBaselineCaptureKeysToRequest(builder); |
| |
| // Enable lens-shading correction for even better DNGs. |
| if (sCaptureImageFormat == ImageFormat.RAW_SENSOR) { |
| builder.set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE, |
| CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON); |
| } else if (sCaptureImageFormat == ImageFormat.JPEG) { |
| builder.set(CaptureRequest.JPEG_QUALITY, JPEG_QUALITY); |
| builder.set(CaptureRequest.JPEG_ORIENTATION, |
| CameraUtil.getJpegRotation(params.orientation, mCharacteristics)); |
| } |
| |
| builder.addTarget(mPreviewSurface); |
| builder.addTarget(mCaptureImageReader.getSurface()); |
| CaptureRequest request = builder.build(); |
| |
| if (DEBUG_WRITE_CAPTURE_DATA) { |
| final String debugDataDir = makeDebugDir(params.debugDataFolder, |
| "normal_capture_debug"); |
| Log.i(TAG, "Writing capture data to: " + debugDataDir); |
| CaptureDataSerializer.toFile("Normal Capture", request, new File(debugDataDir, |
| "capture.txt")); |
| } |
| |
| mCaptureSession.capture(request, mCaptureCallback, mCameraHandler); |
| } catch (CameraAccessException e) { |
| Log.e(TAG, "Could not access camera for still image capture."); |
| broadcastReadyState(true); |
| params.callback.onPictureTakingFailed(); |
| return; |
| } |
| synchronized (mCaptureQueue) { |
| mCaptureQueue.add(new InFlightCapture(params, session)); |
| } |
| } |
| |
| @Override |
| public void startPreview(Surface previewSurface, CaptureReadyCallback listener) { |
| mPreviewSurface = previewSurface; |
| setupAsync(mPreviewSurface, listener); |
| } |
| |
| @Override |
| public void close() { |
| if (mIsClosed) { |
| Log.w(TAG, "Camera is already closed."); |
| return; |
| } |
| try { |
| if (mCaptureSession != null) { |
| mCaptureSession.abortCaptures(); |
| } |
| } catch (CameraAccessException e) { |
| Log.e(TAG, "Could not abort captures in progress."); |
| } |
| mIsClosed = true; |
| mCameraThread.quitSafely(); |
| mDevice.close(); |
| } |
| |
| public Size[] getSupportedPreviewSizes() { |
| StreamConfigurationMap config = mCharacteristics |
| .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); |
| return Size.convert(config.getOutputSizes(SurfaceTexture.class)); |
| } |
| |
| public float getFullSizeAspectRatio() { |
| return mFullSizeAspectRatio; |
| } |
| |
| @Override |
| public Facing getDirection() { |
| return mDirectionProvider.getDirection(); |
| } |
| |
| private void saveJpegPicture(byte[] jpegData, final PhotoCaptureParameters captureParams, |
| CaptureSession session, CaptureResult result) { |
| int heading = captureParams.heading; |
| int width = 0; |
| int height = 0; |
| int rotation = 0; |
| ExifInterface exif = null; |
| try { |
| exif = new ExifInterface(); |
| exif.readExif(jpegData); |
| |
| Integer w = exif.getTagIntValue(ExifInterface.TAG_PIXEL_X_DIMENSION); |
| width = (w == null) ? width : w; |
| Integer h = exif.getTagIntValue(ExifInterface.TAG_PIXEL_Y_DIMENSION); |
| height = (h == null) ? height : h; |
| |
| // Get image rotation from EXIF. |
| rotation = Exif.getOrientation(exif); |
| |
| // Set GPS heading direction based on sensor, if location is on. |
| if (heading >= 0) { |
| ExifTag directionRefTag = exif.buildTag( |
| ExifInterface.TAG_GPS_IMG_DIRECTION_REF, |
| ExifInterface.GpsTrackRef.MAGNETIC_DIRECTION); |
| ExifTag directionTag = exif.buildTag( |
| ExifInterface.TAG_GPS_IMG_DIRECTION, |
| new Rational(heading, 1)); |
| exif.setTag(directionRefTag); |
| exif.setTag(directionTag); |
| } |
| new ExifUtil(exif).populateExif(Optional.<TaskImageContainer.TaskImage> absent(), |
| Optional.of((CaptureResultProxy) new AndroidCaptureResultProxy(result)), |
| Optional.<Location> absent()); |
| } catch (IOException e) { |
| Log.w(TAG, "Could not read exif from gcam jpeg", e); |
| exif = null; |
| } |
| ListenableFuture<Optional<Uri>> futureUri = session.saveAndFinish(jpegData, width, height, |
| rotation, exif); |
| Futures.addCallback(futureUri, new FutureCallback<Optional<Uri>>() { |
| @Override |
| public void onSuccess(Optional<Uri> uriOptional) { |
| captureParams.callback.onPictureSaved(uriOptional.orNull()); |
| } |
| |
| @Override |
| public void onFailure(Throwable throwable) { |
| captureParams.callback.onPictureSaved(null); |
| } |
| }, MoreExecutors.directExecutor()); |
| } |
| |
| /** |
| * Asynchronously sets up the capture session. |
| * |
| * @param previewSurface the surface onto which the preview should be |
| * rendered. |
| * @param listener called when setup is completed. |
| */ |
| private void setupAsync(final Surface previewSurface, final CaptureReadyCallback listener) { |
| mCameraHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| setup(previewSurface, listener); |
| } |
| }); |
| } |
| |
| /** |
| * Configures and attempts to create a capture session. |
| * |
| * @param previewSurface the surface onto which the preview should be |
| * rendered. |
| * @param listener called when the setup is completed. |
| */ |
| private void setup(Surface previewSurface, final CaptureReadyCallback listener) { |
| try { |
| if (mCaptureSession != null) { |
| mCaptureSession.abortCaptures(); |
| mCaptureSession = null; |
| } |
| List<Surface> outputSurfaces = new ArrayList<Surface>(2); |
| outputSurfaces.add(previewSurface); |
| outputSurfaces.add(mCaptureImageReader.getSurface()); |
| |
| mDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { |
| |
| @Override |
| public void onConfigureFailed(CameraCaptureSession session) { |
| listener.onSetupFailed(); |
| } |
| |
| @Override |
| public void onConfigured(CameraCaptureSession session) { |
| mCaptureSession = session; |
| mAFRegions = ZERO_WEIGHT_3A_REGION; |
| mAERegions = ZERO_WEIGHT_3A_REGION; |
| mZoomValue = 1f; |
| mCropRegion = cropRegionForZoom(mZoomValue); |
| boolean success = repeatingPreview(null); |
| if (success) { |
| listener.onReadyForCapture(); |
| } else { |
| listener.onSetupFailed(); |
| } |
| } |
| |
| @Override |
| public void onClosed(CameraCaptureSession session) { |
| super.onClosed(session); |
| } |
| }, mCameraHandler); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Could not set up capture session", ex); |
| listener.onSetupFailed(); |
| } |
| } |
| |
| /** |
| * Adds current regions to CaptureRequest and base AF mode + |
| * AF_TRIGGER_IDLE. |
| * |
| * @param builder Build for the CaptureRequest |
| */ |
| private void addBaselineCaptureKeysToRequest(CaptureRequest.Builder builder) { |
| builder.set(CaptureRequest.CONTROL_AF_REGIONS, mAFRegions); |
| builder.set(CaptureRequest.CONTROL_AE_REGIONS, mAERegions); |
| builder.set(CaptureRequest.SCALER_CROP_REGION, mCropRegion); |
| builder.set(CaptureRequest.CONTROL_AF_MODE, mControlAFMode); |
| builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); |
| // Enable face detection |
| builder.set(CaptureRequest.STATISTICS_FACE_DETECT_MODE, |
| CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL); |
| builder.set(CaptureRequest.CONTROL_SCENE_MODE, |
| CaptureRequest.CONTROL_SCENE_MODE_FACE_PRIORITY); |
| } |
| |
| /** |
| * Request preview capture stream with AF_MODE_CONTINUOUS_PICTURE. |
| * |
| * @return true if request was build and sent successfully. |
| * @param tag |
| */ |
| private boolean repeatingPreview(Object tag) { |
| try { |
| CaptureRequest.Builder builder = mDevice. |
| createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); |
| builder.addTarget(mPreviewSurface); |
| builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); |
| addBaselineCaptureKeysToRequest(builder); |
| mCaptureSession.setRepeatingRequest(builder.build(), mCaptureCallback, |
| mCameraHandler); |
| Log.v(TAG, String.format("Sent repeating Preview request, zoom = %.2f", mZoomValue)); |
| return true; |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Could not access camera setting up preview.", ex); |
| return false; |
| } |
| } |
| |
| /** |
| * Request preview capture stream with auto focus trigger cycle. |
| */ |
| private void sendAutoFocusTriggerCaptureRequest(Object tag) { |
| try { |
| // Step 1: Request single frame CONTROL_AF_TRIGGER_START. |
| CaptureRequest.Builder builder; |
| builder = mDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); |
| builder.addTarget(mPreviewSurface); |
| builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); |
| mControlAFMode = CameraMetadata.CONTROL_AF_MODE_AUTO; |
| addBaselineCaptureKeysToRequest(builder); |
| builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); |
| builder.setTag(tag); |
| mCaptureSession.capture(builder.build(), mCaptureCallback, mCameraHandler); |
| |
| // Step 2: Call repeatingPreview to update mControlAFMode. |
| repeatingPreview(tag); |
| resumeContinuousAFAfterDelay(FOCUS_HOLD_MILLIS); |
| } catch (CameraAccessException ex) { |
| Log.e(TAG, "Could not execute preview request.", ex); |
| } |
| } |
| |
| /** |
| * Resume AF_MODE_CONTINUOUS_PICTURE after FOCUS_HOLD_MILLIS. |
| */ |
| private void resumeContinuousAFAfterDelay(int millis) { |
| mCameraHandler.removeCallbacks(mReturnToContinuousAFRunnable); |
| mCameraHandler.postDelayed(mReturnToContinuousAFRunnable, millis); |
| } |
| |
| /** |
| * This method takes appropriate action if camera2 AF state changes. |
| * <ol> |
| * <li>Reports changes in camera2 AF state to OneCamera.FocusStateListener.</li> |
| * <li>Take picture after AF scan if mTakePictureWhenLensIsStopped true.</li> |
| * </ol> |
| */ |
| private void autofocusStateChangeDispatcher(CaptureResult result) { |
| if (result.getFrameNumber() < mLastControlAfStateFrameNumber || |
| result.get(CaptureResult.CONTROL_AF_STATE) == null) { |
| return; |
| } |
| mLastControlAfStateFrameNumber = result.getFrameNumber(); |
| |
| // Convert to OneCamera mode and state. |
| AutoFocusState resultAFState = AutoFocusHelper. |
| stateFromCamera2State(result.get(CaptureResult.CONTROL_AF_STATE)); |
| |
| // TODO: Consider using LENS_STATE. |
| boolean lensIsStopped = resultAFState == AutoFocusState.ACTIVE_FOCUSED || |
| resultAFState == AutoFocusState.ACTIVE_UNFOCUSED || |
| resultAFState == AutoFocusState.PASSIVE_FOCUSED || |
| resultAFState == AutoFocusState.PASSIVE_UNFOCUSED; |
| |
| if (mTakePictureWhenLensIsStopped && lensIsStopped) { |
| // Take the shot. |
| mCameraHandler.post(mTakePictureRunnable); |
| mTakePictureWhenLensIsStopped = false; |
| } |
| |
| // Report state change when AF state has changed. |
| if (resultAFState != mLastResultAFState && mFocusStateListener != null) { |
| mFocusStateListener.onFocusStatusUpdate(resultAFState, result.getFrameNumber()); |
| } |
| mLastResultAFState = resultAFState; |
| } |
| |
| @Override |
| public void triggerFocusAndMeterAtPoint(float nx, float ny) { |
| int sensorOrientation = mCharacteristics.get( |
| CameraCharacteristics.SENSOR_ORIENTATION); |
| mAERegions = AutoFocusHelper.aeRegionsForNormalizedCoord(nx, ny, mCropRegion, |
| sensorOrientation); |
| mAFRegions = AutoFocusHelper.afRegionsForNormalizedCoord(nx, ny, mCropRegion, |
| sensorOrientation); |
| |
| sendAutoFocusTriggerCaptureRequest(RequestTag.TAP_TO_FOCUS); |
| } |
| |
| @Override |
| public float getMaxZoom() { |
| return mCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); |
| } |
| |
| @Override |
| public void setZoom(float zoom) { |
| mZoomValue = zoom; |
| mCropRegion = cropRegionForZoom(zoom); |
| repeatingPreview(null); |
| } |
| |
| @Override |
| public Size pickPreviewSize(Size pictureSize, Context context) { |
| if (pictureSize == null) { |
| // TODO The default should be selected by the caller, and |
| // pictureSize should never be null. |
| pictureSize = getDefaultPictureSize(); |
| } |
| float pictureAspectRatio = pictureSize.getWidth() / (float) pictureSize.getHeight(); |
| Size[] supportedSizes = getSupportedPreviewSizes(); |
| |
| // Since devices only have one raw resolution we need to be more |
| // flexible for selecting a matching preview resolution. |
| Double aspectRatioTolerance = sCaptureImageFormat == ImageFormat.RAW_SENSOR ? 10d : null; |
| Size size = CaptureModuleUtil.getOptimalPreviewSize(supportedSizes, |
| pictureAspectRatio, aspectRatioTolerance); |
| Log.d(TAG, "Selected preview size: " + size); |
| return size; |
| } |
| |
| private Rect cropRegionForZoom(float zoom) { |
| return AutoFocusHelper.cropRegionForZoom(mCharacteristics, zoom); |
| } |
| |
| /** |
| * Calculate the aspect ratio of the full size capture on this device. |
| * |
| * @param characteristics the characteristics of the camera device. |
| * @return The aspect ration, in terms of width/height of the full capture |
| * size. |
| */ |
| private static float calculateFullSizeAspectRatio(CameraCharacteristics characteristics) { |
| Rect activeArraySize = |
| characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); |
| return ((float) (activeArraySize.width())) / activeArraySize.height(); |
| } |
| |
| /* |
| * Called when a capture that is in flight is completed. |
| * @param capture the in-flight capture which needs to contain the received |
| * image and capture data |
| */ |
| private void onCaptureCompleted(InFlightCapture capture) { |
| |
| // Experimental support for writing RAW. We do not have a usable JPEG |
| // here, so we don't use the usual capture session mechanism and instead |
| // just store the RAW file in its own directory. |
| // TODO: If we make this a real feature we should probably put the DNGs |
| // into the Camera directly. |
| if (sCaptureImageFormat == ImageFormat.RAW_SENSOR) { |
| if (!RAW_DIRECTORY.exists()) { |
| if (!RAW_DIRECTORY.mkdirs()) { |
| throw new RuntimeException("Could not create RAW directory."); |
| } |
| } |
| File dngFile = new File(RAW_DIRECTORY, capture.session.getTitle() + ".dng"); |
| writeDngBytesAndClose(capture.image, capture.totalCaptureResult, |
| mCharacteristics, dngFile); |
| } else { |
| // Since this is not an HDR+ session, we will just save the |
| // result. |
| byte[] imageBytes = acquireJpegBytesAndClose(capture.image); |
| saveJpegPicture(imageBytes, capture.parameters, capture.session, |
| capture.totalCaptureResult); |
| } |
| broadcastReadyState(true); |
| capture.parameters.callback.onPictureTaken(capture.session); |
| } |
| |
| /** |
| * Take the given RAW image and capture result, convert it to a DNG and |
| * write it to disk. |
| * |
| * @param image the image containing the 16-bit RAW data (RAW_SENSOR) |
| * @param captureResult the capture result for the image |
| * @param characteristics the camera characteristics of the camera that took |
| * the RAW image |
| * @param dngFile the destination to where the resulting DNG data is written |
| * to |
| */ |
| private static void writeDngBytesAndClose(Image image, TotalCaptureResult captureResult, |
| CameraCharacteristics characteristics, File dngFile) { |
| try (DngCreator dngCreator = new DngCreator(characteristics, captureResult); |
| FileOutputStream outputStream = new FileOutputStream(dngFile)) { |
| // TODO: Add DngCreator#setThumbnail and add the DNG to the normal |
| // filmstrip. |
| dngCreator.writeImage(outputStream, image); |
| outputStream.close(); |
| image.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "Could not store DNG file", e); |
| return; |
| } |
| Log.i(TAG, "Successfully stored DNG file: " + dngFile.getAbsolutePath()); |
| } |
| |
| /** |
| * Given an image reader, this extracts the final image. If the image in the |
| * reader is JPEG, we extract and return it as is. If the image is YUV, we |
| * convert it to JPEG and return the result. |
| * |
| * @param image the image we got from the image reader. |
| * @return A valid JPEG image. |
| */ |
| private static byte[] acquireJpegBytesAndClose(Image image) { |
| ByteBuffer buffer; |
| if (image.getFormat() == ImageFormat.JPEG) { |
| Image.Plane plane0 = image.getPlanes()[0]; |
| buffer = plane0.getBuffer(); |
| } else if (image.getFormat() == ImageFormat.YUV_420_888) { |
| buffer = ByteBuffer.allocateDirect(image.getWidth() * image.getHeight() * 3); |
| |
| Log.v(TAG, "Compressing JPEG with software encoder."); |
| int numBytes = JpegUtilNative.compressJpegFromYUV420Image( |
| new AndroidImageProxy(image), buffer, JPEG_QUALITY); |
| |
| if (numBytes < 0) { |
| throw new RuntimeException("Error compressing jpeg."); |
| } |
| buffer.limit(numBytes); |
| } else { |
| throw new RuntimeException("Unsupported image format."); |
| } |
| |
| byte[] imageBytes = new byte[buffer.remaining()]; |
| buffer.get(imageBytes); |
| buffer.rewind(); |
| image.close(); |
| return imageBytes; |
| } |
| |
| /** |
| * @return The largest supported picture size. |
| */ |
| public Size getDefaultPictureSize() { |
| StreamConfigurationMap configs = |
| mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); |
| android.util.Size[] supportedSizes = configs.getOutputSizes(sCaptureImageFormat); |
| |
| // Find the largest supported size. |
| android.util.Size largestSupportedSize = supportedSizes[0]; |
| long largestSupportedSizePixels = |
| largestSupportedSize.getWidth() * largestSupportedSize.getHeight(); |
| for (int i = 1; i < supportedSizes.length; i++) { |
| long numPixels = supportedSizes[i].getWidth() * supportedSizes[i].getHeight(); |
| if (numPixels > largestSupportedSizePixels) { |
| largestSupportedSize = supportedSizes[i]; |
| largestSupportedSizePixels = numPixels; |
| } |
| } |
| return new Size(largestSupportedSize.getWidth(), largestSupportedSize.getHeight()); |
| } |
| } |