diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index f576ee524ca1..96fa52198359 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,20 @@ + +## 0.6.5 + +* Adds auto exposure support for Android and iOS implementations. + +## 0.6.4+3 + +* Detect if selected camera supports auto focus and act accordingly on Android. This solves a problem where front facing cameras are not capturing the picture because auto focus is not supported. + +## 0.6.4+2 + +* Set ImageStreamReader listener to null to prevent stale images when streaming images. + +## 0.6.4+1 + +* Added closeCaptureSession() to stopVideoRecording in Camera.java to fix an Android 6 crash + ## 0.6.4 * Adds auto exposure support for Android and iOS implementations. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 90fd21340162..2db097ceadf7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -33,12 +33,14 @@ import android.os.Build.VERSION_CODES; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.util.Range; import android.util.Rational; import android.util.Size; import android.view.OrientationEventListener; import android.view.Surface; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.PictureCaptureRequest.State; @@ -59,6 +61,11 @@ import java.util.Map; import java.util.concurrent.Executors; +@FunctionalInterface +interface ErrorCallback { + void onError(String errorCode, String errorMessage); +} + public class Camera { private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; @@ -73,6 +80,7 @@ public class Camera { private final CamcorderProfile recordingProfile; private final DartMessenger dartMessenger; private final CameraZoom cameraZoom; + private final CameraCharacteristics cameraCharacteristics; private CameraDevice cameraDevice; private CameraCaptureSession cameraCaptureSession; @@ -88,6 +96,8 @@ public class Camera { private PictureCaptureRequest pictureCaptureRequest; private CameraRegions cameraRegions; private int exposureOffset; + private boolean useAutoFocus; + private Range fpsRange; public Camera( final Activity activity, @@ -122,10 +132,12 @@ public void onOrientationChanged(int i) { }; orientationEventListener.enable(); - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + initFps(cameraCharacteristics); + sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); isFrontFacing = - characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); recordingProfile = CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); @@ -133,8 +145,29 @@ public void onOrientationChanged(int i) { previewSize = computeBestPreviewSize(cameraName, preset); cameraZoom = new CameraZoom( - characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + } + + private void initFps(CameraCharacteristics cameraCharacteristics) { + try { + Range[] ranges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (fpsRange == null || upper < fpsRange.getUpper()) { + fpsRange = range; + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + Log.i("Camera", "[FPS Range] is:" + fpsRange); } private void prepareMediaRecorder(String outputFilePath) throws IOException { @@ -221,6 +254,118 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { null); } + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + captureRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + captureRequestBuilder.addTarget(surface); + } + } + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + cameraCaptureSession = session; + + updateFpsRange(); + updateAutoFocus(); + updateFlash(flashMode); + updateExposure(exposureMode); + + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } + + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, null); + } + + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + if (cameraCaptureSession == null) { + return; + } + + try { + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + pictureCaptureCallback, + new Handler(Looper.getMainLooper())); + + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } + } + private void writeToFile(ByteBuffer buffer, File file) throws IOException { try (FileOutputStream outputStream = new FileOutputStream(file)) { while (0 < buffer.remaining()) { @@ -261,7 +406,11 @@ public void takePicture(@NonNull final Result result) { }, null); - runPictureAutoFocus(); + if (useAutoFocus) { + runPictureAutoFocus(); + } else { + runPicturePreCapture(); + } } private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = @@ -344,6 +493,7 @@ private void processCapture(CaptureResult result) { private void runPictureAutoFocus() { assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); lockAutoFocus(); } @@ -355,14 +505,13 @@ private void runPicturePreCapture() { captureRequestBuilder.set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - try { - cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + + refreshPreviewCaptureSession( + () -> + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE), + (code, message) -> pictureCaptureRequest.error(code, message, null)); } private void runPictureCapture() { @@ -409,125 +558,25 @@ public void onCaptureCompleted( private void lockAutoFocus() { captureRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - try { - cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); } private void unlockAutoFocus() { captureRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - initPreviewCaptureBuilder(); + updateAutoFocus(); try { cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); } catch (CameraAccessException ignored) { } captureRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); - try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), pictureCaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); - } - } - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); - } - } - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - try { - if (cameraDevice == null) { - dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); - return; - } - cameraCaptureSession = session; - initPreviewCaptureBuilder(); - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - pictureCaptureCallback, - new Handler(Looper.getMainLooper())); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - dartMessenger.sendCameraErrorEvent(e.getMessage()); - } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); - } - }; - - // Start the session - if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. - List configs = new ArrayList<>(); - configs.add(new OutputConfiguration(flutterSurface)); - for (Surface surface : remainingSurfaces) { - configs.add(new OutputConfiguration(surface)); - } - createCaptureSessionWithSessionConfig(configs, callback); - } else { - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - createCaptureSession(surfaceList, callback); - } - } - - @TargetApi(VERSION_CODES.P) - private void createCaptureSessionWithSessionConfig( - List outputConfigs, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession( - new SessionConfiguration( - SessionConfiguration.SESSION_REGULAR, - outputConfigs, - Executors.newSingleThreadExecutor(), - callback)); - } - - @TargetApi(VERSION_CODES.LOLLIPOP) - @SuppressWarnings("deprecation") - private void createCaptureSession( - List surfaces, CameraCaptureSession.StateCallback callback) - throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); } public void startVideoRecording(Result result) { @@ -560,7 +609,14 @@ public void stopVideoRecording(@NonNull final Result result) { try { recordingVideo = false; - mediaRecorder.stop(); + + try { + cameraCaptureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + mediaRecorder.reset(); startPreview(); result.success(videoRecordingFile.getAbsolutePath()); @@ -629,8 +685,8 @@ public void setFlashMode(@NonNull final Result result, FlashMode mode) // If switching directly from torch to auto or on, make sure we turn off the torch. if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { - this.flashMode = FlashMode.off; - initPreviewCaptureBuilder(); + updateFlash(FlashMode.off); + this.cameraCaptureSession.setRepeatingRequest( captureRequestBuilder.build(), new CaptureCallback() { @@ -646,8 +702,13 @@ public void onCaptureCompleted( } updateFlash(mode); - result.success(null); - isFinished = true; + refreshPreviewCaptureSession( + () -> { + result.success(null); + isFinished = true; + }, + (code, message) -> + result.error("setFlashModeFailed", "Could not set flash mode.", null)); } @Override @@ -666,26 +727,16 @@ public void onCaptureFailed( null); } else { updateFlash(mode); - result.success(null); - } - } - private void updateFlash(FlashMode mode) { - // Get flash - flashMode = mode; - initPreviewCaptureBuilder(); - try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), pictureCaptureCallback, null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); } } public void setExposureMode(@NonNull final Result result, ExposureMode mode) throws CameraAccessException { - this.exposureMode = mode; - initPreviewCaptureBuilder(); + updateExposure(mode); cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); result.success(null); } @@ -712,10 +763,9 @@ public void setExposurePoint(@NonNull final Result result, Double x, Double y) // Set the metering rectangle cameraRegions.setAutoExposureMeteringRectangleFromPoint(x, y); // Apply it - initPreviewCaptureBuilder(); - this.cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), pictureCaptureCallback, null); - result.success(null); + updateExposure(exposureMode); + refreshPreviewCaptureSession( + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); } @TargetApi(VERSION_CODES.P) @@ -801,13 +851,97 @@ public void setExposureOffset(@NonNull final Result result, double offset) double stepSize = getExposureOffsetStepSize(); exposureOffset = (int) (offset / stepSize); // Apply it - initPreviewCaptureBuilder(); + updateExposure(exposureMode); this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); result.success(offset); } - private void initPreviewCaptureBuilder() { - captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; + } + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } + + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; + } + + //Zoom area is calculated relative to sensor area (activeRect) + if (captureRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + } + + result.success(null); + } + + private void updateFpsRange() { + if (fpsRange == null) { + return; + } + + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + } + + private void updateAutoFocus() { + if (useAutoFocus) { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + // Auto focus is not supported + if (modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + useAutoFocus = false; + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } else { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + } + } else { + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } + } + + private void updateExposure(ExposureMode mode) { + exposureMode = mode; + + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + + switch (mode) { + case locked: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } + + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + } + + private void updateFlash(FlashMode mode) { + // Get flash + flashMode = mode; + // Applying flash modes switch (flashMode) { case off: @@ -832,24 +966,6 @@ private void initPreviewCaptureBuilder() { captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); break; } - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); - switch (exposureMode) { - case locked: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - // Applying auto focus - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); } public void startPreview() throws CameraAccessException { @@ -909,37 +1025,11 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i null); } - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; - } - - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; - } - - public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; - - if (zoom > maxZoom || zoom < minZoom) { - String errorMessage = - String.format( - Locale.ENGLISH, - "Zoom level out of bounds (zoom level should be between %f and %f).", - minZoom, - maxZoom); - result.error("ZOOM_ERROR", errorMessage, null); - return; - } - - //Zoom area is calculated relative to sensor area (activeRect) - if (captureRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + public void stopImageStream() throws CameraAccessException { + if (imageStreamReader != null) { + imageStreamReader.setOnImageAvailableListener(null, null); } - - result.success(null); + startPreview(); } private void closeCaptureSession() { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java index 5c58b0741af7..2285f67ad25c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java @@ -8,11 +8,9 @@ public final class CameraRegions { private Size maxBoundaries; public CameraRegions(Size maxBoundaries) { - assert (maxBoundaries != null); - assert (maxBoundaries.getWidth() > 0); - assert (maxBoundaries.getHeight() > 0); + assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); + assert (maxBoundaries == null || maxBoundaries.getHeight() > 0); this.maxBoundaries = maxBoundaries; - setAutoExposureMeteringRectangleFromPoint(0.5, 0.5); } public MeteringRectangle getAEMeteringRectangle() { @@ -23,6 +21,10 @@ public Size getMaxBoundaries() { return this.maxBoundaries; } + public void resetAutoExposureMeteringRectangle() { + this.aeMeteringRectangle = null; + } + public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); } @@ -30,7 +32,9 @@ public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { assert (x >= 0 && x <= 1); assert (y >= 0 && y <= 1); - assert (maxBoundaries != null); + if (maxBoundaries == null) + throw new IllegalStateException( + "Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries."); // Interpolate the target coordinate int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 78a10010f90b..2ceff845ed4b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -219,7 +219,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) case "stopImageStream": { try { - camera.startPreview(); + camera.stopImageStream(); result.success(null); } catch (Exception e) { handleException(e, result); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java index 56f8a20a5644..ca66918e2493 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java @@ -1,6 +1,8 @@ package io.flutter.plugins.camera; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; @@ -39,7 +41,7 @@ public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, -0.5); } - @Test(expected = AssertionError.class) + @Test(expected = IllegalStateException.class) public void getMeteringRectangleForPoint_should_throw_for_null_boundaries() { cameraRegions.getMeteringRectangleForPoint(null, 0, -0); } @@ -68,11 +70,6 @@ public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() assertEquals(new MeteringRectangle(89, 0, 10, 5, 1), r); } - @Test(expected = AssertionError.class) - public void constructor_should_throw_for_null_boundaries() { - new CameraRegions(null); - } - @Test(expected = AssertionError.class) public void constructor_should_throw_for_0_width_boundary() { new CameraRegions(new Size(0, 50)); @@ -87,13 +84,22 @@ public void constructor_should_throw_for_0_height_boundary() { public void constructor_should_initialize() { CameraRegions cr = new CameraRegions(new Size(100, 50)); assertEquals(new Size(100, 50), cr.getMaxBoundaries()); - assertEquals(new MeteringRectangle(45, 23, 10, 5, 1), cr.getAEMeteringRectangle()); + assertNull(cr.getAEMeteringRectangle()); } @Test - public void setAutoExposureMeteringRectangleFromPoin_should_set_aeMeteringRectangle_for_point() { + public void setAutoExposureMeteringRectangleFromPoint_should_set_aeMeteringRectangle_for_point() { CameraRegions cr = new CameraRegions(new Size(100, 50)); cr.setAutoExposureMeteringRectangleFromPoint(0, 0); assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAEMeteringRectangle()); } + + @Test + public void resetAutoExposureMeteringRectangle_should_reset_aeMeteringRectangle() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + cr.setAutoExposureMeteringRectangleFromPoint(0, 0); + assertNotNull(cr.getAEMeteringRectangle()); + cr.resetAutoExposureMeteringRectangle(); + assertNull(cr.getAEMeteringRectangle()); + } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 97f257536701..adef3eb14675 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,13 +2,13 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.6.4 +version: 0.6.5 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - camera_platform_interface: '>=1.0.4 <1.3.0' + camera_platform_interface: ^1.2.0 pedantic: ^1.8.0 dev_dependencies: diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 0a799dda7e24..5a4a7fc8771b 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -28,7 +28,7 @@ get mockAvailableCameras => [ get mockInitializeCamera => 13; get mockOnCameraInitializedEvent => - CameraInitializedEvent(13, 75, 75, ExposureMode.auto, false); + CameraInitializedEvent(13, 75, 75, ExposureMode.auto, true); get mockOnCameraClosingEvent => null; diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 5392fdc2f001..ae22f8a5f6a4 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,7 +1,11 @@ -## 1.3.0 +## 1.4.0 - Added interface methods to support auto focus. +## 1.3.0 + +- Introduces an option to set the image format when initializing. + ## 1.2.0 - Added interface to support automatic exposure. diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index ba932fb9f34b..d23271e59844 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -7,6 +7,7 @@ import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/types/image_format_group.dart'; import 'package:camera_platform_interface/src/utils/utils.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; @@ -77,7 +78,8 @@ class MethodChannelCamera extends CameraPlatform { } @override - Future initializeCamera(int cameraId) { + Future initializeCamera(int cameraId, + {ImageFormatGroup imageFormatGroup}) { _channels.putIfAbsent(cameraId, () { final channel = MethodChannel('flutter.io/cameraPlugin/camera$cameraId'); channel.setMethodCallHandler( @@ -95,6 +97,7 @@ class MethodChannelCamera extends CameraPlatform { 'initialize', { 'cameraId': cameraId, + 'imageFormatGroup': imageFormatGroup.name(), }, ); diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 7352bd986e50..fcd85436c365 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -9,6 +9,7 @@ import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; import 'package:camera_platform_interface/src/types/exposure_mode.dart'; import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/types/image_format_group.dart'; import 'package:cross_file/cross_file.dart'; import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -55,7 +56,12 @@ abstract class CameraPlatform extends PlatformInterface { } /// Initializes the camera on the device. - Future initializeCamera(int cameraId) { + /// + /// [imageFormatGroup] is used to specify the image formatting used. + /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream. + /// On iOS this defaults to kCVPixelFormatType_32BGRA. + Future initializeCamera(int cameraId, + {ImageFormatGroup imageFormatGroup}) { throw UnimplementedError('initializeCamera() is not implemented.'); } diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart new file mode 100644 index 000000000000..3d2c0180fe65 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart @@ -0,0 +1,50 @@ +/// Group of image formats that are comparable across Android and iOS platforms. +enum ImageFormatGroup { + /// The image format does not fit into any specific group. + unknown, + + /// Multi-plane YUV 420 format. + /// + /// This format is a generic YCbCr format, capable of describing any 4:2:0 + /// chroma-subsampled planar or semiplanar buffer (but not fully interleaved), + /// with 8 bits per color sample. + /// + /// On Android, this is `android.graphics.ImageFormat.YUV_420_888`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888 + /// + /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc + yuv420, + + /// 32-bit BGRA. + /// + /// On iOS, this is `kCVPixelFormatType_32BGRA`. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_32bgra?language=objc + bgra8888, + + /// 32-big RGB image encoded into JPEG bytes. + /// + /// On Android, this is `android.graphics.ImageFormat.JPEG`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat#JPEG + jpeg, +} + +/// Extension on [ImageFormatGroup] to stringify the enum +extension ImageFormatGroupName on ImageFormatGroup { + /// returns a String value for [ImageFormatGroup] + /// returns 'unknown' if platform is not supported + /// or if [ImageFormatGroup] is not supported for the platform + String name() { + switch (this) { + case ImageFormatGroup.bgra8888: + return 'bgra8888'; + case ImageFormatGroup.yuv420: + return 'yuv420'; + case ImageFormatGroup.jpeg: + return 'jpeg'; + case ImageFormatGroup.unknown: + default: + return 'unknown'; + } + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index 753c8e4e9053..256558dff3e7 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -6,5 +6,6 @@ export 'camera_description.dart'; export 'resolution_preset.dart'; export 'camera_exception.dart'; export 'flash_mode.dart'; +export 'image_format_group.dart'; export 'exposure_mode.dart'; export 'focus_mode.dart'; diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 0ba0ee93e95b..a6d0b815e660 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the camera plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.3.0 +version: 1.4.0 dependencies: flutter: @@ -21,5 +21,5 @@ dev_dependencies: pedantic: ^1.8.0 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.7.0 <3.0.0" flutter: ">=1.22.0" diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 35e211531d28..550be358f7c9 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -26,7 +26,10 @@ void main() { MethodChannelMock cameraMockChannel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', methods: { - 'create': {'cameraId': 1} + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + } }); final camera = MethodChannelCamera(); @@ -109,7 +112,10 @@ void main() { MethodChannelMock cameraMockChannel = MethodChannelMock( channelName: 'plugins.flutter.io/camera', methods: { - 'create': {'cameraId': 1}, + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, 'initialize': null }); final camera = MethodChannelCamera(); @@ -139,6 +145,7 @@ void main() { 'initialize', arguments: { 'cameraId': 1, + 'imageFormatGroup': 'unknown', }, ), ]); diff --git a/packages/camera/camera_platform_interface/test/types/image_group_test.dart b/packages/camera/camera_platform_interface/test/types/image_group_test.dart new file mode 100644 index 000000000000..c49b2f03a7a0 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/image_group_test.dart @@ -0,0 +1,13 @@ +import 'package:camera_platform_interface/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$ImageFormatGroup tests', () { + test('ImageFormatGroupName extension returns correct values', () { + expect(ImageFormatGroup.bgra8888.name(), 'bgra8888'); + expect(ImageFormatGroup.yuv420.name(), 'yuv420'); + expect(ImageFormatGroup.jpeg.name(), 'jpeg'); + expect(ImageFormatGroup.unknown.name(), 'unknown'); + }); + }); +} diff --git a/packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..be52383ef49c --- /dev/null +++ b/packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists