diff --git a/packages/camera/android/build.gradle b/packages/camera/android/build.gradle index fbce7b86f8f7..dd544c084ba7 100644 --- a/packages/camera/android/build.gradle +++ b/packages/camera/android/build.gradle @@ -44,7 +44,12 @@ android { lintOptions { disable 'InvalidPackage' } + compileOptions { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' + } dependencies { implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.core:core:1.0.0' } } diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java new file mode 100644 index 000000000000..bf99f8d561d5 --- /dev/null +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -0,0 +1,513 @@ +package io.flutter.plugins.camera; + +import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +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.params.StreamConfigurationMap; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaRecorder; +import android.util.Size; +import android.view.OrientationEventListener; +import android.view.Surface; +import androidx.annotation.NonNull; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.FlutterView; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Camera { + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final OrientationEventListener orientationEventListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final Size videoSize; + private final boolean enableAudio; + + private CameraDevice cameraDevice; + private CameraCaptureSession cameraCaptureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + private EventChannel.EventSink eventSink; + private CaptureRequest.Builder captureRequestBuilder; + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private int currentOrientation = ORIENTATION_UNKNOWN; + + public Camera( + final Activity activity, + final FlutterView flutterView, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = flutterView.createSurfaceTexture(); + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + orientationEventListener = + new OrientationEventListener(activity.getApplicationContext()) { + @Override + public void onOrientationChanged(int i) { + if (i == ORIENTATION_UNKNOWN) { + return; + } + // Convert the raw deg angle to the nearest multiple of 90. + currentOrientation = (int) Math.round(i / 90.0) * 90; + } + }; + orientationEventListener.enable(); + + int minHeight; + switch (resolutionPreset) { + case "high": + minHeight = 720; + break; + case "medium": + minHeight = 480; + break; + case "low": + minHeight = 240; + break; + default: + throw new IllegalArgumentException("Unknown preset: " + resolutionPreset); + } + + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + StreamConfigurationMap streamConfigurationMap = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + //noinspection ConstantConditions + sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + //noinspection ConstantConditions + isFrontFacing = + characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + captureSize = CameraUtils.computeBestCaptureSize(streamConfigurationMap); + Size[] sizes = + CameraUtils.computeBestPreviewAndRecordingSize( + activity, streamConfigurationMap, minHeight, getMediaOrientation(), captureSize); + videoSize = sizes[0]; + previewSize = sizes[1]; + } + + public void setupCameraEventChannel(EventChannel cameraEventChannel) { + cameraEventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object arguments, EventChannel.EventSink sink) { + eventSink = sink; + } + + @Override + public void onCancel(Object arguments) { + eventSink = null; + } + }); + } + + private void prepareMediaRecorder(String outputFilePath) throws IOException { + if (mediaRecorder != null) { + mediaRecorder.release(); + } + mediaRecorder = new MediaRecorder(); + + // There's a specific order that mediaRecorder expects. Do not change the order + // of these function calls. + if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); + if (enableAudio) mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); + mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); + mediaRecorder.setVideoEncodingBitRate(1024 * 1000); + if (enableAudio) mediaRecorder.setAudioSamplingRate(16000); + mediaRecorder.setVideoFrameRate(27); + mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight()); + mediaRecorder.setOutputFile(outputFilePath); + mediaRecorder.setOrientationHint(getMediaOrientation()); + + mediaRecorder.prepare(); + } + + @SuppressLint("MissingPermission") + public void open(@NonNull final Result result) throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance( + previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + cameraDevice = device; + try { + startPreview(); + } catch (CameraAccessException e) { + result.error("CameraAccess", e.getMessage(), null); + close(); + return; + } + Map reply = new HashMap<>(); + reply.put("textureId", flutterTexture.id()); + reply.put("previewWidth", previewSize.getWidth()); + reply.put("previewHeight", previewSize.getHeight()); + result.success(reply); + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + sendEvent(EventType.CAMERA_CLOSING); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + close(); + sendEvent(EventType.ERROR, "The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + sendEvent(EventType.ERROR, errorDescription); + } + }, + null); + } + + private void writeToFile(ByteBuffer buffer, File file) throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(file)) { + while (0 < buffer.remaining()) { + outputStream.getChannel().write(buffer); + } + } + } + + SurfaceTextureEntry getFlutterTexture() { + return flutterTexture; + } + + public void takePicture(String filePath, @NonNull final Result result) { + final File file = new File(filePath); + + if (file.exists()) { + result.error( + "fileExists", "File at path '" + filePath + "' already exists. Cannot overwrite.", null); + return; + } + + pictureImageReader.setOnImageAvailableListener( + reader -> { + try (Image image = reader.acquireLatestImage()) { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + writeToFile(buffer, file); + result.success(null); + } catch (IOException e) { + result.error("IOError", "Failed saving image", null); + } + }, + null); + + try { + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); + + cameraCaptureSession.capture( + captureBuilder.build(), + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + String reason; + switch (failure.getReason()) { + case CaptureFailure.REASON_ERROR: + reason = "An error happened in the framework"; + break; + case CaptureFailure.REASON_FLUSHED: + reason = "The capture has failed due to an abortCaptures() call"; + break; + default: + reason = "Unknown reason"; + } + result.error("captureFailure", reason, null); + } + }, + null); + } catch (CameraAccessException e) { + result.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) { + sendEvent(EventType.ERROR, "The camera was closed during configuration."); + return; + } + cameraCaptureSession = session; + captureRequestBuilder.set( + CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + sendEvent(EventType.ERROR, e.getMessage()); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + sendEvent(EventType.ERROR, "Failed to configure camera session."); + } + }; + + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + // Start the session + cameraDevice.createCaptureSession(surfaceList, callback, null); + } + + public void startVideoRecording(String filePath, Result result) { + if (new File(filePath).exists()) { + result.error("fileExists", "File at path '" + filePath + "' already exists.", null); + return; + } + try { + prepareMediaRecorder(filePath); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + recordingVideo = false; + mediaRecorder.stop(); + mediaRecorder.reset(); + startPreview(); + result.success(null); + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void startPreview() throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_STILL_CAPTURE, imageStreamReader.getSurface()); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); + } + + private void sendEvent(EventType eventType) { + sendEvent(eventType, null); + } + + private void sendEvent(EventType eventType, String description) { + if (eventSink != null) { + Map event = new HashMap<>(); + event.put("eventType", eventType.toString().toLowerCase()); + // Only errors have description + if (eventType != EventType.ERROR) { + event.put("errorDescription", description); + } + eventSink.success(event); + } + } + + private void closeCaptureSession() { + if (cameraCaptureSession != null) { + cameraCaptureSession.close(); + cameraCaptureSession = null; + } + } + + public void close() { + closeCaptureSession(); + + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; + } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } + } + + public void dispose() { + close(); + flutterTexture.release(); + orientationEventListener.disable(); + } + + private int getMediaOrientation() { + final int sensorOrientationOffset = + (currentOrientation == ORIENTATION_UNKNOWN) + ? 0 + : (isFrontFacing) ? -currentOrientation : currentOrientation; + return (sensorOrientationOffset + sensorOrientation + 360) % 360; + } + + private enum EventType { + ERROR, + CAMERA_CLOSING, + } +} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java new file mode 100644 index 000000000000..d703af819181 --- /dev/null +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -0,0 +1,80 @@ +package io.flutter.plugins.camera; + +import android.Manifest; +import android.Manifest.permission; +import android.app.Activity; +import android.content.pm.PackageManager; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +public class CameraPermissions { + private static final int CAMERA_REQUEST_ID = 513469796; + private boolean ongoing = false; + + public void requestPermissions( + Registrar registrar, boolean enableAudio, ResultCallback callback) { + if (ongoing) { + callback.onResult("cameraPermission", "Camera permission request ongoing"); + } + Activity activity = registrar.activity(); + if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { + registrar.addRequestPermissionsResultListener( + new CameraRequestPermissionsListener( + (String errorCode, String errorDescription) -> { + ongoing = false; + callback.onResult(errorCode, errorDescription); + })); + ongoing = true; + ActivityCompat.requestPermissions( + activity, + enableAudio + ? new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO} + : new String[] {Manifest.permission.CAMERA}, + CAMERA_REQUEST_ID); + } else { + // Permissions already exist. Call the callback with success. + callback.onResult(null, null); + } + } + + private boolean hasCameraPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.CAMERA) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean hasAudioPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED; + } + + private static class CameraRequestPermissionsListener + implements PluginRegistry.RequestPermissionsResultListener { + final ResultCallback callback; + + private CameraRequestPermissionsListener(ResultCallback callback) { + this.callback = callback; + } + + @Override + public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { + if (id == CAMERA_REQUEST_ID) { + if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + callback.onResult("cameraPermission", "MediaRecorderCamera permission not granted"); + } else if (grantResults.length > 1 + && grantResults[1] != PackageManager.PERMISSION_GRANTED) { + callback.onResult("cameraPermission", "MediaRecorderAudio permission not granted"); + } else { + callback.onResult(null, null); + } + return true; + } + return false; + } + } + + interface ResultCallback { + void onResult(String errorCode, String errorDescription); + } +} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 1a6b4c0de984..2d16e0be80ef 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -1,88 +1,33 @@ package io.flutter.plugins.camera; -import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.ImageFormat; -import android.graphics.Point; -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.params.StreamConfigurationMap; -import android.media.Image; -import android.media.ImageReader; -import android.media.MediaRecorder; import android.os.Build; -import android.util.Size; -import android.view.Display; -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.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.view.FlutterView; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; public class CameraPlugin implements MethodCallHandler { - private static final int CAMERA_REQUEST_ID = 513469796; - private static final String TAG = "CameraPlugin"; - - private static CameraManager cameraManager; + private final CameraPermissions cameraPermissions = new CameraPermissions(); private final FlutterView view; + private final Registrar registrar; + private final EventChannel imageStreamChannel; private Camera camera; - private Registrar registrar; - // The code to run after requesting camera permissions. - private Runnable cameraPermissionContinuation; - private final OrientationEventListener orientationEventListener; - private int currentOrientation = ORIENTATION_UNKNOWN; - private CameraPlugin(Registrar registrar, FlutterView view) { + private CameraPlugin(Registrar registrar) { this.registrar = registrar; - this.view = view; - - orientationEventListener = - new OrientationEventListener(registrar.activity().getApplicationContext()) { - @Override - public void onOrientationChanged(int i) { - if (i == ORIENTATION_UNKNOWN) { - return; - } - // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) Math.round(i / 90.0) * 90; - } - }; - - registrar.addRequestPermissionsResultListener(new CameraRequestPermissionsListener()); + this.view = registrar.view(); + this.imageStreamChannel = + new EventChannel(registrar.messenger(), "plugins.flutter.io/camera/imageStream"); } public static void registerWith(Registrar registrar) { - if (registrar.activity() == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // When a background flutter view tries to register the plugin, the registrar has no activity. // We stop the registration process as this plugin is foreground only. Also, if the sdk is // less than 21 (min sdk for Camera2) we don't register the plugin. @@ -92,61 +37,59 @@ public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/camera"); - cameraManager = (CameraManager) registrar.activity().getSystemService(Context.CAMERA_SERVICE); + channel.setMethodCallHandler(new CameraPlugin(registrar)); + } + + private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { + String cameraName = call.argument("cameraName"); + String resolutionPreset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); + camera = new Camera(registrar.activity(), view, cameraName, resolutionPreset, enableAudio); + + EventChannel cameraEventChannel = + new EventChannel( + registrar.messenger(), + "flutter.io/cameraPlugin/cameraEvents" + camera.getFlutterTexture().id()); + camera.setupCameraEventChannel(cameraEventChannel); - channel.setMethodCallHandler(new CameraPlugin(registrar, registrar.view())); + camera.open(result); } @Override - public void onMethodCall(MethodCall call, final Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { switch (call.method) { case "availableCameras": try { - String[] cameraNames = cameraManager.getCameraIdList(); - List> cameras = new ArrayList<>(); - for (String cameraName : cameraNames) { - HashMap details = new HashMap<>(); - CameraCharacteristics characteristics = - cameraManager.getCameraCharacteristics(cameraName); - details.put("name", cameraName); - @SuppressWarnings("ConstantConditions") - int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - details.put("sensorOrientation", sensorOrientation); - - int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); - switch (lensFacing) { - case CameraMetadata.LENS_FACING_FRONT: - details.put("lensFacing", "front"); - break; - case CameraMetadata.LENS_FACING_BACK: - details.put("lensFacing", "back"); - break; - case CameraMetadata.LENS_FACING_EXTERNAL: - details.put("lensFacing", "external"); - break; - } - cameras.add(details); - } - result.success(cameras); + result.success(CameraUtils.getAvailableCameras(registrar.activity())); } catch (Exception e) { handleException(e, result); } break; case "initialize": { - String cameraName = call.argument("cameraName"); - String resolutionPreset = call.argument("resolutionPreset"); - boolean enableAudio = call.argument("enableAudio"); if (camera != null) { camera.close(); } - camera = new Camera(cameraName, resolutionPreset, result, enableAudio); - orientationEventListener.enable(); + cameraPermissions.requestPermissions( + registrar, + call.argument("enableAudio"), + (String errCode, String errDesc) -> { + if (errCode == null) { + try { + instantiateCamera(call, result); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error(errCode, errDesc, null); + } + }); + break; } case "takePicture": { - camera.takePicture((String) call.argument("path"), result); + camera.takePicture(call.argument("path"), result); break; } case "prepareForVideoRecording": @@ -157,8 +100,7 @@ public void onMethodCall(MethodCall call, final Result result) { } case "startVideoRecording": { - final String filePath = call.argument("filePath"); - camera.startVideoRecording(filePath, result); + camera.startVideoRecording(call.argument("filePath"), result); break; } case "stopVideoRecording": @@ -169,7 +111,7 @@ public void onMethodCall(MethodCall call, final Result result) { case "startImageStream": { try { - camera.startPreviewWithImageStream(); + camera.startPreviewWithImageStream(imageStreamChannel); result.success(null); } catch (Exception e) { handleException(e, result); @@ -191,7 +133,6 @@ public void onMethodCall(MethodCall call, final Result result) { if (camera != null) { camera.dispose(); } - orientationEventListener.disable(); result.success(null); break; } @@ -212,689 +153,4 @@ private void handleException(Exception exception, Result result) { throw (RuntimeException) exception; } - - private static class CompareSizesByArea implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); - } - } - - private class CameraRequestPermissionsListener - implements PluginRegistry.RequestPermissionsResultListener { - @Override - public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { - if (id == CAMERA_REQUEST_ID) { - cameraPermissionContinuation.run(); - return true; - } - return false; - } - } - - private class Camera { - private final FlutterView.SurfaceTextureEntry textureEntry; - private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; - private EventChannel.EventSink eventSink; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - private int sensorOrientation; - private boolean isFrontFacing; - private String cameraName; - private Size captureSize; - private Size previewSize; - private CaptureRequest.Builder captureRequestBuilder; - private Size videoSize; - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private boolean enableAudio; - - Camera( - final String cameraName, - final String resolutionPreset, - @NonNull final Result result, - final boolean enableAudio) { - - this.cameraName = cameraName; - this.enableAudio = enableAudio; - textureEntry = view.createSurfaceTexture(); - - registerEventChannel(); - - try { - int minHeight; - switch (resolutionPreset) { - case "high": - minHeight = 720; - break; - case "medium": - minHeight = 480; - break; - case "low": - minHeight = 240; - break; - default: - throw new IllegalArgumentException("Unknown preset: " + resolutionPreset); - } - - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - StreamConfigurationMap streamConfigurationMap = - characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - //noinspection ConstantConditions - sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - //noinspection ConstantConditions - isFrontFacing = - characteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - computeBestCaptureSize(streamConfigurationMap); - computeBestPreviewAndRecordingSize(streamConfigurationMap, minHeight, captureSize); - - if (cameraPermissionContinuation != null) { - result.error("cameraPermission", "Camera permission request ongoing", null); - } - cameraPermissionContinuation = - new Runnable() { - @Override - public void run() { - cameraPermissionContinuation = null; - if (!hasCameraPermission()) { - result.error( - "cameraPermission", "MediaRecorderCamera permission not granted", null); - return; - } - if (enableAudio && !hasAudioPermission()) { - result.error( - "cameraPermission", "MediaRecorderAudio permission not granted", null); - return; - } - open(result); - } - }; - if (hasCameraPermission() && (!enableAudio || hasAudioPermission())) { - cameraPermissionContinuation.run(); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final Activity activity = registrar.activity(); - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - - activity.requestPermissions( - enableAudio - ? new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO} - : new String[] {Manifest.permission.CAMERA}, - CAMERA_REQUEST_ID); - } - } - } catch (CameraAccessException e) { - result.error("CameraAccess", e.getMessage(), null); - } catch (IllegalArgumentException e) { - result.error("IllegalArgumentException", e.getMessage(), null); - } - } - - private void registerEventChannel() { - new EventChannel( - registrar.messenger(), "flutter.io/cameraPlugin/cameraEvents" + textureEntry.id()) - .setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object arguments, EventChannel.EventSink eventSink) { - Camera.this.eventSink = eventSink; - } - - @Override - public void onCancel(Object arguments) { - Camera.this.eventSink = null; - } - }); - } - - private boolean hasCameraPermission() { - final Activity activity = registrar.activity(); - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || activity.checkSelfPermission(Manifest.permission.CAMERA) - == PackageManager.PERMISSION_GRANTED; - } - - private boolean hasAudioPermission() { - final Activity activity = registrar.activity(); - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || activity.checkSelfPermission(Manifest.permission.RECORD_AUDIO) - == PackageManager.PERMISSION_GRANTED; - } - - private void computeBestPreviewAndRecordingSize( - StreamConfigurationMap streamConfigurationMap, int minHeight, Size captureSize) { - Size[] sizes = streamConfigurationMap.getOutputSizes(SurfaceTexture.class); - - // Preview size and video size should not be greater than screen resolution or 1080. - Point screenResolution = new Point(); - - final Activity activity = registrar.activity(); - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - - Display display = activity.getWindowManager().getDefaultDisplay(); - display.getRealSize(screenResolution); - - final boolean swapWH = getMediaOrientation() % 180 == 90; - int screenWidth = swapWH ? screenResolution.y : screenResolution.x; - int screenHeight = swapWH ? screenResolution.x : screenResolution.y; - - List goodEnough = new ArrayList<>(); - for (Size s : sizes) { - if (minHeight <= s.getHeight() - && s.getWidth() <= screenWidth - && s.getHeight() <= screenHeight - && s.getHeight() <= 1080) { - goodEnough.add(s); - } - } - - Collections.sort(goodEnough, new CompareSizesByArea()); - - if (goodEnough.isEmpty()) { - previewSize = sizes[0]; - videoSize = sizes[0]; - } else { - float captureSizeRatio = (float) captureSize.getWidth() / captureSize.getHeight(); - - previewSize = goodEnough.get(0); - for (Size s : goodEnough) { - if ((float) s.getWidth() / s.getHeight() == captureSizeRatio) { - previewSize = s; - break; - } - } - - Collections.reverse(goodEnough); - videoSize = goodEnough.get(0); - for (Size s : goodEnough) { - if ((float) s.getWidth() / s.getHeight() == captureSizeRatio) { - videoSize = s; - break; - } - } - } - } - - private void computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { - // For still image captures, we use the largest available size. - captureSize = - Collections.max( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } - - private void prepareMediaRecorder(String outputFilePath) throws IOException { - if (mediaRecorder != null) { - mediaRecorder.release(); - } - mediaRecorder = new MediaRecorder(); - - if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - if (enableAudio) mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); - mediaRecorder.setVideoEncodingBitRate(1024 * 1000); - if (enableAudio) mediaRecorder.setAudioSamplingRate(16000); - mediaRecorder.setVideoFrameRate(27); - mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight()); - mediaRecorder.setOutputFile(outputFilePath); - mediaRecorder.setOrientationHint(getMediaOrientation()); - - mediaRecorder.prepare(); - } - - private void open(@Nullable final Result result) { - if (!hasCameraPermission()) { - if (result != null) result.error("cameraPermission", "Camera permission not granted", null); - } else { - try { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance( - previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice cameraDevice) { - Camera.this.cameraDevice = cameraDevice; - try { - startPreview(); - } catch (CameraAccessException e) { - if (result != null) result.error("CameraAccess", e.getMessage(), null); - cameraDevice.close(); - Camera.this.cameraDevice = null; - return; - } - - if (result != null) { - Map reply = new HashMap<>(); - reply.put("textureId", textureEntry.id()); - reply.put("previewWidth", previewSize.getWidth()); - reply.put("previewHeight", previewSize.getHeight()); - result.success(reply); - } - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - if (eventSink != null) { - Map event = new HashMap<>(); - event.put("eventType", "cameraClosing"); - eventSink.success(event); - } - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - cameraDevice.close(); - Camera.this.cameraDevice = null; - sendErrorEvent("The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - cameraDevice.close(); - Camera.this.cameraDevice = null; - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = - "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - sendErrorEvent(errorDescription); - } - }, - null); - } catch (CameraAccessException e) { - if (result != null) result.error("cameraAccess", e.getMessage(), null); - } - } - } - - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } - } - } - - private void takePicture(String filePath, @NonNull final Result result) { - final File file = new File(filePath); - - if (file.exists()) { - result.error( - "fileExists", - "File at path '" + filePath + "' already exists. Cannot overwrite.", - null); - return; - } - - pictureImageReader.setOnImageAvailableListener( - new ImageReader.OnImageAvailableListener() { - @Override - public void onImageAvailable(ImageReader reader) { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - result.success(null); - } catch (IOException e) { - result.error("IOError", "Failed saving image", null); - } - } - }, - null); - - try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); - - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - String reason; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - break; - default: - reason = "Unknown reason"; - } - result.error("captureFailure", reason, null); - } - }, - null); - } catch (CameraAccessException e) { - result.error("cameraAccess", e.getMessage(), null); - } - } - - private void startVideoRecording(String filePath, @NonNull final Result result) { - if (cameraDevice == null) { - result.error("configureFailed", "Camera was closed during configuration.", null); - return; - } - if (new File(filePath).exists()) { - result.error( - "fileExists", - "File at path '" + filePath + "' already exists. Cannot overwrite.", - null); - return; - } - try { - closeCaptureSession(); - prepareMediaRecorder(filePath); - - recordingVideo = true; - - SurfaceTexture surfaceTexture = textureEntry.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); - - List surfaces = new ArrayList<>(); - - Surface previewSurface = new Surface(surfaceTexture); - surfaces.add(previewSurface); - captureRequestBuilder.addTarget(previewSurface); - - Surface recorderSurface = mediaRecorder.getSurface(); - surfaces.add(recorderSurface); - captureRequestBuilder.addTarget(recorderSurface); - - cameraDevice.createCaptureSession( - surfaces, - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { - try { - if (cameraDevice == null) { - result.error("configureFailed", "Camera was closed during configuration", null); - return; - } - Camera.this.cameraCaptureSession = cameraCaptureSession; - captureRequestBuilder.set( - CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), null, null); - mediaRecorder.start(); - result.success(null); - } catch (CameraAccessException - | IllegalStateException - | IllegalArgumentException e) { - result.error("cameraException", e.getMessage(), null); - } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - result.error("configureFailed", "Failed to configure camera session", null); - } - }, - null); - } catch (CameraAccessException | IOException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - private void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - recordingVideo = false; - mediaRecorder.stop(); - mediaRecorder.reset(); - startPreview(); - result.success(null); - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - private void startPreview() throws CameraAccessException { - closeCaptureSession(); - - SurfaceTexture surfaceTexture = textureEntry.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); - - List surfaces = new ArrayList<>(); - - Surface previewSurface = new Surface(surfaceTexture); - surfaces.add(previewSurface); - captureRequestBuilder.addTarget(previewSurface); - - surfaces.add(pictureImageReader.getSurface()); - - cameraDevice.createCaptureSession( - surfaces, - new CameraCaptureSession.StateCallback() { - - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - if (cameraDevice == null) { - sendErrorEvent("The camera was closed during configuration."); - return; - } - try { - cameraCaptureSession = session; - captureRequestBuilder.set( - CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - sendErrorEvent(e.getMessage()); - } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - sendErrorEvent("Failed to configure the camera for preview."); - } - }, - null); - } - - private void startPreviewWithImageStream() throws CameraAccessException { - closeCaptureSession(); - - SurfaceTexture surfaceTexture = textureEntry.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - - captureRequestBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - - List surfaces = new ArrayList<>(); - - Surface previewSurface = new Surface(surfaceTexture); - surfaces.add(previewSurface); - captureRequestBuilder.addTarget(previewSurface); - - surfaces.add(imageStreamReader.getSurface()); - captureRequestBuilder.addTarget(imageStreamReader.getSurface()); - - cameraDevice.createCaptureSession( - surfaces, - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - if (cameraDevice == null) { - sendErrorEvent("The camera was closed during configuration."); - return; - } - try { - cameraCaptureSession = session; - captureRequestBuilder.set( - CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - sendErrorEvent(e.getMessage()); - } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - sendErrorEvent("Failed to configure the camera for streaming images."); - } - }, - null); - - registerImageStreamEventChannel(); - } - - private void registerImageStreamEventChannel() { - final EventChannel imageStreamChannel = - new EventChannel(registrar.messenger(), "plugins.flutter.io/camera/imageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink eventSink) { - setImageStreamImageAvailableListener(eventSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink eventSink) { - imageStreamReader.setOnImageAvailableListener( - new ImageReader.OnImageAvailableListener() { - @Override - public void onImageAvailable(final ImageReader reader) { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - eventSink.success(imageBuffer); - img.close(); - } - }, - null); - } - - private void sendErrorEvent(String errorDescription) { - if (eventSink != null) { - Map event = new HashMap<>(); - event.put("eventType", "error"); - event.put("errorDescription", errorDescription); - eventSink.success(event); - } - } - - private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; - } - } - - private void close() { - closeCaptureSession(); - - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; - } - } - - private void dispose() { - close(); - textureEntry.release(); - } - - private int getMediaOrientation() { - final int sensorOrientationOffset = - (currentOrientation == ORIENTATION_UNKNOWN) - ? 0 - : (isFrontFacing) ? -currentOrientation : currentOrientation; - return (sensorOrientationOffset + sensorOrientation + 360) % 360; - } - } } diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java new file mode 100644 index 000000000000..517db1537041 --- /dev/null +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -0,0 +1,129 @@ +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.Point; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.util.Size; +import android.view.Display; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Provides various utilities for camera. */ +public final class CameraUtils { + + private CameraUtils() {} + + static Size[] computeBestPreviewAndRecordingSize( + Activity activity, + StreamConfigurationMap streamConfigurationMap, + int minHeight, + int orientation, + Size captureSize) { + Size previewSize, videoSize; + Size[] sizes = streamConfigurationMap.getOutputSizes(SurfaceTexture.class); + + // Preview size and video size should not be greater than screen resolution or 1080. + Point screenResolution = new Point(); + + Display display = activity.getWindowManager().getDefaultDisplay(); + display.getRealSize(screenResolution); + + final boolean swapWH = orientation % 180 == 90; + int screenWidth = swapWH ? screenResolution.y : screenResolution.x; + int screenHeight = swapWH ? screenResolution.x : screenResolution.y; + + List goodEnough = new ArrayList<>(); + for (Size s : sizes) { + if (minHeight <= s.getHeight() + && s.getWidth() <= screenWidth + && s.getHeight() <= screenHeight + && s.getHeight() <= 1080) { + goodEnough.add(s); + } + } + + Collections.sort(goodEnough, new CompareSizesByArea()); + + if (goodEnough.isEmpty()) { + previewSize = sizes[0]; + videoSize = sizes[0]; + } else { + float captureSizeRatio = (float) captureSize.getWidth() / captureSize.getHeight(); + + previewSize = goodEnough.get(0); + for (Size s : goodEnough) { + if ((float) s.getWidth() / s.getHeight() == captureSizeRatio) { + previewSize = s; + break; + } + } + + Collections.reverse(goodEnough); + videoSize = goodEnough.get(0); + for (Size s : goodEnough) { + if ((float) s.getWidth() / s.getHeight() == captureSizeRatio) { + videoSize = s; + break; + } + } + } + return new Size[] {videoSize, previewSize}; + } + + static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { + // For still image captures, we use the largest available size. + return Collections.max( + Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); + } + + public static List> getAvailableCameras(Activity activity) + throws CameraAccessException { + CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + String[] cameraNames = cameraManager.getCameraIdList(); + List> cameras = new ArrayList<>(); + for (String cameraName : cameraNames) { + HashMap details = new HashMap<>(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + details.put("name", cameraName); + int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + details.put("sensorOrientation", sensorOrientation); + + int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); + switch (lensFacing) { + case CameraMetadata.LENS_FACING_FRONT: + details.put("lensFacing", "front"); + break; + case CameraMetadata.LENS_FACING_BACK: + details.put("lensFacing", "back"); + break; + case CameraMetadata.LENS_FACING_EXTERNAL: + details.put("lensFacing", "external"); + break; + } + cameras.add(details); + } + return cameras; + } + + private static class CompareSizesByArea implements Comparator { + @Override + public int compare(Size lhs, Size rhs) { + // We cast here to ensure the multiplications won't overflow. + return Long.signum( + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } + } +}