diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 4f4f160cf85a..d919a3c13541 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.4.4 + +* Added 2 new quality presets (veryHigh and veryLow). +* Now quality presets match on Android and iOS +* Now quality presets can be used to control image capture quality. +** NOTE: ** Existing presets have been updated, this will affect the quality of pictures and videos in existing apps. + ## 0.4.3+2 * Bump the minimum Flutter version to 1.2.0. 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 2fafb7d5c3a1..2778a9edc778 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 @@ -8,7 +8,6 @@ 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; @@ -19,13 +18,13 @@ import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.CamcorderProfile; import android.media.Image; import android.media.ImageReader; import android.media.MediaRecorder; import android.os.Build; import android.os.Bundle; import android.util.Size; -import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; import androidx.annotation.NonNull; @@ -97,9 +96,7 @@ public void onActivityStarted(Activity activity) {} @Override public void onActivityResumed(Activity activity) { boolean wasRequestingPermission = requestingPermission; - if (requestingPermission) { - requestingPermission = false; - } + requestingPermission = false; if (activity != CameraPlugin.this.activity) { return; } @@ -295,9 +292,9 @@ private class Camera { private Size captureSize; private Size previewSize; private CaptureRequest.Builder captureRequestBuilder; - private Size videoSize; private MediaRecorder mediaRecorder; private boolean recordingVideo; + private CamcorderProfile recordingProfile; Camera(final String cameraName, final String resolutionPreset, @NonNull final Result result) { @@ -307,32 +304,21 @@ private class Camera { 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); + + recordingProfile = getBestAvailableCamcorderProfileForResolutionPreset(resolutionPreset); + computeBestPreviewSize(recordingProfile); + computeBestCaptureSize(streamConfigurationMap, recordingProfile, resolutionPreset); if (cameraPermissionContinuation != null) { result.error("cameraPermission", "Camera permission request ongoing", null); @@ -404,62 +390,31 @@ private boolean hasAudioPermission() { == 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(); - 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]; + // The preview should never be bigger than 720p (1280 x 720) or it will mess up the recording. + private void computeBestPreviewSize(CamcorderProfile profile) { + float ratio = (float) profile.videoFrameWidth / profile.videoFrameHeight; + if (profile.videoFrameWidth > 1280) { + previewSize = new Size(1280, Math.round(1280 / ratio)); + } else if (profile.videoFrameHeight > 1280) { + previewSize = new Size(Math.round(1280 * ratio), 1280); } 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; - } - } + previewSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); } } - 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 computeBestCaptureSize( + StreamConfigurationMap streamConfigurationMap, + CamcorderProfile profile, + String resolutionPreset) { + // For still image captures, use the largest image size if resolutionPreset is veryHigh + if ("veryHigh".equals(resolutionPreset)) { + captureSize = + Collections.max( + Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); + } else { + captureSize = new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } } private void prepareMediaRecorder(String outputFilePath) throws IOException { @@ -469,19 +424,69 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); - mediaRecorder.setVideoEncodingBitRate(1024 * 1000); - mediaRecorder.setAudioSamplingRate(16000); - mediaRecorder.setVideoFrameRate(27); - mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight()); + mediaRecorder.setProfile(recordingProfile); mediaRecorder.setOutputFile(outputFilePath); mediaRecorder.setOrientationHint(getMediaOrientation()); mediaRecorder.prepare(); } + private CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + String resolutionPreset) { + int camcorderProfileIndex; + switch (resolutionPreset) { + case "veryHigh": + camcorderProfileIndex = 0; + break; + case "high": + camcorderProfileIndex = 1; + break; + case "medium": + camcorderProfileIndex = 2; + break; + case "low": + camcorderProfileIndex = 3; + break; + case "veryLow": + camcorderProfileIndex = 4; + break; + default: + throw new IllegalArgumentException("Unknown resolution preset: " + resolutionPreset); + } + + int cameraId = Integer.parseInt(cameraName); + switch (camcorderProfileIndex) { + case 0: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(CamcorderProfile.QUALITY_2160P); + } + case 1: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(CamcorderProfile.QUALITY_1080P); + } + case 2: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(CamcorderProfile.QUALITY_720P); + } + case 3: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(CamcorderProfile.QUALITY_480P); + } + case 4: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_CIF)) { + return CamcorderProfile.get(CamcorderProfile.QUALITY_CIF); + } + default: + if (CamcorderProfile.hasProfile( + Integer.parseInt(cameraName), CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.get(CamcorderProfile.QUALITY_LOW); + } else { + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + } + private void open(@Nullable final Result result) { if (!hasCameraPermission()) { if (result != null) result.error("cameraPermission", "Camera permission not granted", null); @@ -494,7 +499,10 @@ private void open(@Nullable final Result result) { // Used to steam image byte data to dart side. imageStreamReader = ImageReader.newInstance( - previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + recordingProfile.videoFrameWidth, + recordingProfile.videoFrameHeight, + ImageFormat.YUV_420_888, + 2); cameraManager.openCamera( cameraName, diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart index ca01a7ac0f57..b10113fedb00 100644 --- a/packages/camera/example/lib/main.dart +++ b/packages/camera/example/lib/main.dart @@ -203,7 +203,7 @@ class _CameraExampleHomeState extends State { if (controller != null) { await controller.dispose(); } - controller = CameraController(cameraDescription, ResolutionPreset.high); + controller = CameraController(cameraDescription, ResolutionPreset.medium); // If the controller is updated then update the UI. controller.addListener(() { diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 7d5d28d3a3a7..92124fdfaabe 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -6,8 +6,8 @@ static FlutterError *getFlutterError(NSError *error) { return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.domain - details:error.localizedDescription]; + message:error.localizedDescription + details:error.domain]; } @interface FLTSavePhotoDelegate : NSObject @@ -140,6 +140,7 @@ @interface FLTCam : NSObject - Luigi Agosti