From 2a655493f260161109f0a15d044d87b394f17f9c Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Wed, 22 Feb 2023 14:36:08 -0700 Subject: [PATCH 01/13] Recreating PR from flutter/plugins --- packages/camera/camera_android/CHANGELOG.md | 4 + .../io/flutter/plugins/camera/Camera.java | 173 +++++++-- .../plugins/camera/MethodCallHandlerImpl.java | 12 + .../flutter/plugins/camera/VideoRenderer.java | 365 ++++++++++++++++++ .../io/flutter/plugins/camera/CameraTest.java | 114 ++++++ .../example/integration_test/camera_test.dart | 59 +++ .../example/lib/camera_controller.dart | 30 +- .../camera_android/example/lib/main.dart | 23 +- .../camera_android/example/pubspec.yaml | 4 +- .../lib/src/android_camera.dart | 11 + packages/camera/camera_android/pubspec.yaml | 4 +- .../test/android_camera_test.dart | 23 ++ .../camera/camera_avfoundation/CHANGELOG.md | 4 + .../example/integration_test/camera_test.dart | 45 +++ .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../camera_avfoundation/example/ios/Podfile | 4 +- .../ios/Runner.xcodeproj/project.pbxproj | 15 +- .../example/ios/Runner/Info.plist | 2 + .../example/ios/RunnerTests/CameraTestUtils.m | 13 +- .../example/lib/camera_controller.dart | 30 +- .../camera_avfoundation/example/lib/main.dart | 23 +- .../camera_avfoundation/example/pubspec.yaml | 2 +- .../ios/Classes/CameraPlugin.m | 2 + .../camera_avfoundation/ios/Classes/FLTCam.h | 2 + .../camera_avfoundation/ios/Classes/FLTCam.m | 177 ++++++--- .../ios/Classes/FLTCam_Test.h | 3 +- .../lib/src/avfoundation_camera.dart | 11 + .../camera/camera_avfoundation/pubspec.yaml | 4 +- .../test/avfoundation_camera_test.dart | 23 ++ .../test/image_picker_test.mocks.dart | 4 +- 30 files changed, 1052 insertions(+), 136 deletions(-) create mode 100644 packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 4609b402058..f7f0b2a0343 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.5 + +* Allows camera to be switched while video recording. + ## 0.10.4 * Temporarily fixes issue with requested video profiles being null by falling back to deprecated behavior in that case. diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index b02d6864b5b..157619f50e8 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -115,13 +115,28 @@ class Camera * Holds all of the camera features/settings and will be used to update the request builder when * one changes. */ - private final CameraFeatures cameraFeatures; + private CameraFeatures cameraFeatures; + + private String imageFormatGroup; + + /** + * Takes an input/output surface and orients the recording correctly. This is needed because + * switching cameras while recording causes the wrong orientation. + */ + private VideoRenderer videoRenderer; + + /** + * Whether or not the camera aligns with the initial way the camera was facing if the camera was + * flipped. + */ + private int initialCameraFacing; private final SurfaceTextureEntry flutterTexture; + private final ResolutionPreset resolutionPreset; private final boolean enableAudio; private final Context applicationContext; private final DartMessenger dartMessenger; - private final CameraProperties cameraProperties; + private CameraProperties cameraProperties; private final CameraFeatureFactory cameraFeatureFactory; private final Activity activity; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ @@ -211,6 +226,7 @@ public Camera( this.applicationContext = activity.getApplicationContext(); this.cameraProperties = cameraProperties; this.cameraFeatureFactory = cameraFeatureFactory; + this.resolutionPreset = resolutionPreset; this.cameraFeatures = CameraFeatures.init( cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); @@ -251,6 +267,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { if (mediaRecorder != null) { mediaRecorder.release(); } + closeRenderer(); final PlatformChannel.DeviceOrientation lockedOrientation = ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) @@ -279,6 +296,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { + this.imageFormatGroup = imageFormatGroup; final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); if (!resolutionFeature.checkIsSupported()) { @@ -323,14 +341,16 @@ public void onOpened(@NonNull CameraDevice device) { cameraDevice = new DefaultCameraDeviceWrapper(device); try { startPreview(); + if (!recordingVideo) // only send initialization if we werent already recording and switching cameras dartMessenger.sendCameraInitializedEvent( - resolutionFeature.getPreviewSize().getWidth(), - resolutionFeature.getPreviewSize().getHeight(), - cameraFeatures.getExposureLock().getValue(), - cameraFeatures.getAutoFocus().getValue(), - cameraFeatures.getExposurePoint().checkIsSupported(), - cameraFeatures.getFocusPoint().checkIsSupported()); - } catch (CameraAccessException e) { + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + cameraFeatures.getExposureLock().getValue(), + cameraFeatures.getAutoFocus().getValue(), + cameraFeatures.getExposurePoint().checkIsSupported(), + cameraFeatures.getFocusPoint().checkIsSupported()); + + } catch (CameraAccessException | InterruptedException e) { dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); } @@ -340,7 +360,8 @@ public void onOpened(@NonNull CameraDevice device) { public void onClosed(@NonNull CameraDevice camera) { Log.i(TAG, "open | onClosed"); - // Prevents calls to methods that would otherwise result in IllegalStateException exceptions. + // Prevents calls to methods that would otherwise result in IllegalStateException + // exceptions. cameraDevice = null; closeCaptureSession(); dartMessenger.sendCameraClosingEvent(); @@ -756,7 +777,7 @@ public void startVideoRecording( if (imageStreamChannel != null) { setStreamHandler(imageStreamChannel); } - + initialCameraFacing = cameraProperties.getLensFacing(); recordingVideo = true; try { startCapture(true, imageStreamChannel != null); @@ -768,6 +789,13 @@ public void startVideoRecording( } } + private void closeRenderer() { + if (videoRenderer != null) { + videoRenderer.close(); + videoRenderer = null; + } + } + public void stopVideoRecording(@NonNull final Result result) { if (!recordingVideo) { result.success(null); @@ -778,6 +806,7 @@ public void stopVideoRecording(@NonNull final Result result) { cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); recordingVideo = false; try { + closeRenderer(); captureSession.abortCaptures(); mediaRecorder.stop(); } catch (CameraAccessException | IllegalStateException e) { @@ -786,7 +815,7 @@ public void stopVideoRecording(@NonNull final Result result) { mediaRecorder.reset(); try { startPreview(); - } catch (CameraAccessException | IllegalStateException e) { + } catch (CameraAccessException | IllegalStateException | InterruptedException e) { result.error("videoRecordingFailed", e.getMessage(), null); return; } @@ -1070,13 +1099,51 @@ public void resumePreview() { null, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); } - public void startPreview() throws CameraAccessException { + public void startPreview() throws CameraAccessException, InterruptedException { + // If recording is already in progress, the camera is being flipped, so send it through the VideoRenderer to keep the correct orientation. + if (recordingVideo) { + startPreviewWithVideoRendererStream(); + } else { + startRegularPreview(); + } + } + + private void startRegularPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; Log.i(TAG, "startPreview"); - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } + private void startPreviewWithVideoRendererStream() + throws CameraAccessException, InterruptedException { + if (videoRenderer == null) return; + + // get rotation for rendered video + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + DeviceOrientationManager orientationManager = + cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); + + int rotation = 0; + if (orientationManager != null) { + rotation = + lockedOrientation == null + ? orientationManager.getVideoOrientation() + : orientationManager.getVideoOrientation(lockedOrientation); + } + + if (cameraProperties.getLensFacing() != initialCameraFacing) { + + // If the new camera is facing the opposite way than the initial recording, + // the rotation should be flipped 180 degrees. + rotation = (rotation + 180) % 360; + } + videoRenderer.setRotation(rotation); + + createCaptureSession(CameraDevice.TEMPLATE_RECORD, videoRenderer.getInputSurface()); + } + public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { setStreamHandler(imageStreamChannel); @@ -1200,17 +1267,7 @@ private void closeCaptureSession() { public void close() { Log.i(TAG, "close"); - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - - // Closing the CameraDevice without closing the CameraCaptureSession is recommended - // for quickly closing the camera: - // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close() - captureSession = null; - } else { - closeCaptureSession(); - } + stopAndReleaseCamera(); if (pictureImageReader != null) { pictureImageReader.close(); @@ -1229,6 +1286,72 @@ public void close() { stopBackgroundThread(); } + private void stopAndReleaseCamera() { + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + + // Closing the CameraDevice without closing the CameraCaptureSession is recommended + // for quickly closing the camera: + // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close() + captureSession = null; + } else { + closeCaptureSession(); + } + } + + private void prepareVideoRenderer() { + if (videoRenderer != null) return; + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + + // handle videoRenderer errors + Thread.UncaughtExceptionHandler videoRendererUncaughtExceptionHandler = + new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable ex) { + dartMessenger.sendCameraErrorEvent( + "Failed to process frames after camera was flipped."); + } + }; + + videoRenderer = + new VideoRenderer( + mediaRecorder.getSurface(), + resolutionFeature.getCaptureSize().getWidth(), + resolutionFeature.getCaptureSize().getHeight(), + videoRendererUncaughtExceptionHandler); + } + + public void setDescriptionWhileRecording( + @NonNull final Result result, CameraProperties properties) { + + if (!recordingVideo) { + result.error("setDescriptionWhileRecordingFailed", "Device was not recording", null); + return; + } + + // See VideoRenderer.java requires API 26 to switch camera while recording + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { + result.error("setDescriptionWhileRecordingFailed", "Device does not support", null); + return; + } + + stopAndReleaseCamera(); + prepareVideoRenderer(); + cameraProperties = properties; + cameraFeatures = + CameraFeatures.init( + cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + try { + open(imageFormatGroup); + } catch (CameraAccessException e) { + result.error("setDescriptionWhileRecordingFailed", e.getMessage(), null); + } + result.success(null); + } + public void dispose() { Log.i(TAG, "dispose"); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 432344ade8c..aad62bbaba8 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -354,6 +354,18 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) result.success(null); break; } + case "setDescriptionWhileRecording": + { + try { + String cameraName = call.argument("cameraName"); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + camera.setDescriptionWhileRecording(result, cameraProperties); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java new file mode 100644 index 00000000000..b7128373b10 --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java @@ -0,0 +1,365 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static android.os.SystemClock.uptimeMillis; + +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.view.Surface; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Renders video onto texture after performing a matrix rotation on each frame. + * + *

VideoRenderer is needed because when switching between cameras mid recording, the orientation + * of the recording from the new camera usually becomes flipped. MediaRecorder has + * setOrientationHint, but that cannot be called mid recording and therefore isn't useful. Android + * Camera2 has no setDisplayOrientation on the camera itself as it is supposed to 'just work' (see + * https://stackoverflow.com/questions/33479004/what-is-the-camera2-api-equivalent-of-setdisplayorientation). + * Therefore it cannot be used to set the camera's orientation either. + * + *

This leaves the solution to be routing the recording through a surface texture and performing + * a matrix transformation on it manually to get the correct orientation. This only happens when + * setDescription is called mid video recording. + */ +public class VideoRenderer { + + private static String TAG = "VideoRenderer"; + + private static final String vertexShaderCode = + " precision highp float;\n" + + " attribute vec3 vertexPosition;\n" + + " attribute vec2 uvs;\n" + + " varying vec2 varUvs;\n" + + " uniform mat4 texMatrix;\n" + + " uniform mat4 mvp;\n" + + "\n" + + " void main()\n" + + " {\n" + + " varUvs = (texMatrix * vec4(uvs.x, uvs.y, 0, 1.0)).xy;\n" + + " gl_Position = mvp * vec4(vertexPosition, 1.0);\n" + + " }"; + + private static final String fragmentShaderCode = + " #extension GL_OES_EGL_image_external : require\n" + + " precision mediump float;\n" + + "\n" + + " varying vec2 varUvs;\n" + + " uniform samplerExternalOES texSampler;\n" + + "\n" + + " void main()\n" + + " {\n" + + " vec4 c = texture2D(texSampler, varUvs);\n" + + " gl_FragColor = vec4(c.r, c.g, c.b, c.a);\n" + + " }"; + + private final int[] textureHandles = new int[1]; + + private final float[] vertices = + new float[] { + -1.0f, -1.0f, 0.0f, 0f, 0f, -1.0f, 1.0f, 0.0f, 0f, 1f, 1.0f, 1.0f, 0.0f, 1f, 1f, 1.0f, + -1.0f, 0.0f, 1f, 0f + }; + + private final int[] indices = new int[] {2, 1, 0, 0, 3, 2}; + + private int program; + private int vertexHandle = 0; + private final int[] bufferHandles = new int[2]; + private int uvsHandle = 0; + private int texMatrixHandle = 0; + private int mvpHandle = 0; + + EGLDisplay display; + EGLContext context; + EGLSurface surface; + private Thread thread; + private final Surface outputSurface; + private SurfaceTexture inputSurfaceTexture; + private Surface inputSurface; + + private HandlerThread surfaceTextureFrameAvailableHandler; + private final Object surfaceTextureAvailableFrameLock = new Object(); + private Boolean surfaceTextureFrameAvailable = false; + + private final int recordingWidth; + private final int recordingHeight; + private int rotation = 0; + + private final Object lock = new Object(); + + private final Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + + /** Gets surface for input. Blocks until surface is ready. */ + public Surface getInputSurface() throws InterruptedException { + synchronized (lock) { + while (inputSurface == null) { + lock.wait(); + } + } + return inputSurface; + } + + public VideoRenderer( + Surface outputSurface, + int recordingWidth, + int recordingHeight, + Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { + this.outputSurface = outputSurface; + this.recordingHeight = recordingHeight; + this.recordingWidth = recordingWidth; + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + startOpenGL(); + Log.d(TAG, "VideoRenderer setup complete"); + } + + /** Stop rendering and cleanup resources. */ + public void close() { + thread.interrupt(); + surfaceTextureFrameAvailableHandler.quitSafely(); + cleanupOpenGL(); + inputSurfaceTexture.release(); + } + + private void cleanupOpenGL() { + GLES20.glDeleteBuffers(2, bufferHandles, 0); + GLES20.glDeleteTextures(1, textureHandles, 0); + EGL14.eglDestroyContext(display, context); + EGL14.eglDestroySurface(display, surface); + GLES20.glDeleteProgram(program); + } + + /** Configures openGL. Must be called in same thread as draw is called. */ + private void configureOpenGL() { + synchronized (lock) { + display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (display == EGL14.EGL_NO_DISPLAY) + throw new RuntimeException( + "eglDisplay == EGL14.EGL_NO_DISPLAY: " + + GLUtils.getEGLErrorString(EGL14.eglGetError())); + + int[] version = new int[2]; + if (!EGL14.eglInitialize(display, version, 0, version, 1)) + throw new RuntimeException( + "eglInitialize(): " + GLUtils.getEGLErrorString(EGL14.eglGetError())); + + String eglExtensions = EGL14.eglQueryString(display, EGL14.EGL_EXTENSIONS); + if (!eglExtensions.contains("EGL_ANDROID_presentation_time")) + throw new RuntimeException( + "cannot configure OpenGL. missing EGL_ANDROID_presentation_time"); + + int[] attribList = + new int[] { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGLExt.EGL_RECORDABLE_ANDROID, 1, + EGL14.EGL_NONE + }; + + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + if (!EGL14.eglChooseConfig(display, attribList, 0, configs, 0, configs.length, numConfigs, 0)) + throw new RuntimeException(GLUtils.getEGLErrorString(EGL14.eglGetError())); + + int err = EGL14.eglGetError(); + if (err != EGL14.EGL_SUCCESS) throw new RuntimeException(GLUtils.getEGLErrorString(err)); + + int[] ctxAttribs = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + context = EGL14.eglCreateContext(display, configs[0], EGL14.EGL_NO_CONTEXT, ctxAttribs, 0); + + err = EGL14.eglGetError(); + if (err != EGL14.EGL_SUCCESS) throw new RuntimeException(GLUtils.getEGLErrorString(err)); + + int[] surfaceAttribs = new int[] {EGL14.EGL_NONE}; + + surface = EGL14.eglCreateWindowSurface(display, configs[0], outputSurface, surfaceAttribs, 0); + + err = EGL14.eglGetError(); + if (err != EGL14.EGL_SUCCESS) throw new RuntimeException(GLUtils.getEGLErrorString(err)); + + if (!EGL14.eglMakeCurrent(display, surface, surface, context)) + throw new RuntimeException( + "eglMakeCurrent(): " + GLUtils.getEGLErrorString(EGL14.eglGetError())); + + ByteBuffer vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4); + vertexBuffer.order(ByteOrder.nativeOrder()); + vertexBuffer.asFloatBuffer().put(vertices); + vertexBuffer.asFloatBuffer().position(0); + + ByteBuffer indexBuffer = ByteBuffer.allocateDirect(indices.length * 4); + indexBuffer.order(ByteOrder.nativeOrder()); + indexBuffer.asIntBuffer().put(indices); + indexBuffer.position(0); + + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + + program = GLES20.glCreateProgram(); + + GLES20.glAttachShader(program, vertexShader); + GLES20.glAttachShader(program, fragmentShader); + GLES20.glLinkProgram(program); + + deleteShader(vertexShader); + deleteShader(fragmentShader); + + vertexHandle = GLES20.glGetAttribLocation(program, "vertexPosition"); + uvsHandle = GLES20.glGetAttribLocation(program, "uvs"); + texMatrixHandle = GLES20.glGetUniformLocation(program, "texMatrix"); + mvpHandle = GLES20.glGetUniformLocation(program, "mvp"); + + // Initialize buffers + GLES20.glGenBuffers(2, bufferHandles, 0); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferHandles[0]); + GLES20.glBufferData( + GLES20.GL_ARRAY_BUFFER, vertices.length * 4, vertexBuffer, GLES20.GL_DYNAMIC_DRAW); + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferHandles[1]); + GLES20.glBufferData( + GLES20.GL_ELEMENT_ARRAY_BUFFER, indices.length * 4, indexBuffer, GLES20.GL_DYNAMIC_DRAW); + + // Init texture that will receive decoded frames + GLES20.glGenTextures(1, textureHandles, 0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureHandles[0]); + + inputSurfaceTexture = new SurfaceTexture(getTexId()); + inputSurfaceTexture.setDefaultBufferSize(recordingWidth, recordingHeight); + surfaceTextureFrameAvailableHandler = new HandlerThread("FrameHandlerThread"); + surfaceTextureFrameAvailableHandler.start(); + inputSurface = new Surface(inputSurfaceTexture); + + inputSurfaceTexture.setOnFrameAvailableListener( + new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + synchronized (surfaceTextureAvailableFrameLock) { + if (surfaceTextureFrameAvailable) + Log.w(TAG, "Frame available before processing other frames. dropping frames"); + surfaceTextureFrameAvailable = true; + surfaceTextureAvailableFrameLock.notifyAll(); + } + } + }, + new Handler(surfaceTextureFrameAvailableHandler.getLooper())); + lock.notifyAll(); + } + } + + /** Starts and configures Video Renderer. */ + private void startOpenGL() { + Log.d(TAG, "Starting OpenGL Thread"); + thread = + new Thread() { + @Override + public void run() { + + configureOpenGL(); + + try { + // Continuously pull frames from input surface texture and use videoRenderer to modify + // to correct rotation. + while (!Thread.interrupted()) { + + synchronized (surfaceTextureAvailableFrameLock) { + while (!surfaceTextureFrameAvailable) { + surfaceTextureAvailableFrameLock.wait(500); + } + surfaceTextureFrameAvailable = false; + } + + inputSurfaceTexture.updateTexImage(); + + float[] surfaceTextureMatrix = new float[16]; + inputSurfaceTexture.getTransformMatrix(surfaceTextureMatrix); + + draw(recordingWidth, recordingHeight, surfaceTextureMatrix); + } + } catch (InterruptedException e) { + Log.d(TAG, "thread interrupted while waiting for frames"); + } + } + }; + thread.setUncaughtExceptionHandler(uncaughtExceptionHandler); + thread.start(); + } + + public int getTexId() { + return textureHandles[0]; + } + + public float[] moveMatrix() { + float[] m = new float[16]; + Matrix.setIdentityM(m, 0); + Matrix.rotateM(m, 0, rotation, 0, 0, 1); + return m; + } + + public void setRotation(int rotation) { + this.rotation = rotation; + } + + private int loadShader(int type, String code) { + + int shader = GLES20.glCreateShader(type); + + GLES20.glShaderSource(shader, code); + GLES20.glCompileShader(shader); + return shader; + } + + private void deleteShader(int shader) { + GLES20.glDeleteShader(shader); + } + + public void draw(int viewportWidth, int viewportHeight, float[] texMatrix) { + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + GLES20.glClearColor(0f, 0f, 0f, 0f); + + GLES20.glViewport(0, 0, viewportWidth, viewportHeight); + + GLES20.glUseProgram(program); + + // Pass transformations to shader + GLES20.glUniformMatrix4fv(texMatrixHandle, 1, false, texMatrix, 0); + GLES20.glUniformMatrix4fv(mvpHandle, 1, false, moveMatrix(), 0); + + // Prepare buffers with vertices and indices & draw + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferHandles[0]); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferHandles[1]); + + GLES20.glEnableVertexAttribArray(vertexHandle); + GLES20.glVertexAttribPointer(vertexHandle, 3, GLES20.GL_FLOAT, false, 4 * 5, 0); + + GLES20.glEnableVertexAttribArray(uvsHandle); + GLES20.glVertexAttribPointer(uvsHandle, 2, GLES20.GL_FLOAT, false, 4 * 5, 3 * 4); + + GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_INT, 0); + + EGLExt.eglPresentationTimeANDROID(display, surface, uptimeMillis() * 1000000); + if (!EGL14.eglSwapBuffers(display, surface)) { + throw new RuntimeException( + "eglSwapBuffers()" + GLUtils.getEGLErrorString(EGL14.eglGetError())); + } + } +} diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 9a679017ded..1715514fca3 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -602,6 +602,120 @@ public void resumeVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { verify(mockResult, never()).error(any(), any(), any()); } + @Test + public void setDescriptionWhileRecording() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + VideoRenderer mockVideoRenderer = mock(VideoRenderer.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer); + + final CameraProperties newCameraProperties = mock(CameraProperties.class); + camera.setDescriptionWhileRecording(mockResult, newCameraProperties); + + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { + verify(mockResult, times(1)) + .error(eq("setDescriptionWhileRecordingFailed"), eq("Device does not support"), eq(null)); + } else { + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + } + + @Test + public void startPreview_shouldPullStreamFromVideoRenderer() + throws InterruptedException, CameraAccessException { + VideoRenderer mockVideoRenderer = mock(VideoRenderer.class); + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + Size mockSize = mock(Size.class); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + TextureRegistry.SurfaceTextureEntry cameraFlutterTexture = + (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture"); + ResolutionFeature resolutionFeature = + (ResolutionFeature) + TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature"); + + when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(resolutionFeature.getPreviewSize()).thenReturn(mockSize); + + camera.startPreview(); + verify(mockVideoRenderer, times(1)) + .getInputSurface(); // stream pulled from videoRenderer's surface. + } + + @Test + public void startPreview_shouldPullStreamFromImageReader() + throws InterruptedException, CameraAccessException { + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + Size mockSize = mock(Size.class); + ImageReader mockImageReader = mock(ImageReader.class); + TestUtils.setPrivateField(camera, "recordingVideo", false); + TestUtils.setPrivateField(camera, "pictureImageReader", mockImageReader); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + TextureRegistry.SurfaceTextureEntry cameraFlutterTexture = + (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture"); + ResolutionFeature resolutionFeature = + (ResolutionFeature) + TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature"); + + when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(resolutionFeature.getPreviewSize()).thenReturn(mockSize); + + camera.startPreview(); + verify(mockImageReader, times(1)) + .getSurface(); // stream pulled from regular imageReader's surface. + } + + @Test + public void startPreview_shouldFlipRotation() throws InterruptedException, CameraAccessException { + VideoRenderer mockVideoRenderer = mock(VideoRenderer.class); + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + Size mockSize = mock(Size.class); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer); + TestUtils.setPrivateField(camera, "initialCameraFacing", CameraMetadata.LENS_FACING_BACK); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + TextureRegistry.SurfaceTextureEntry cameraFlutterTexture = + (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture"); + ResolutionFeature resolutionFeature = + (ResolutionFeature) + TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature"); + + when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(resolutionFeature.getPreviewSize()).thenReturn(mockSize); + when(mockCameraProperties.getLensFacing()).thenReturn(CameraMetadata.LENS_FACING_FRONT); + + camera.startPreview(); + verify(mockVideoRenderer, times(1)).setRotation(180); + } + + @Test + public void setDescriptionWhileRecording_shouldErrorWhenNotRecording() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + TestUtils.setPrivateField(camera, "recordingVideo", false); + final CameraProperties newCameraProperties = mock(CameraProperties.class); + camera.setDescriptionWhileRecording(mockResult, newCameraProperties); + + verify(mockResult, times(1)) + .error("setDescriptionWhileRecordingFailed", "Device was not recording", null); + verify(mockResult, never()).success(any()); + } + @Test public void resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThanN() { diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index e499872da5f..6f20789ebf5 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -8,7 +8,9 @@ import 'dart:ui'; import 'package:camera_android/camera_android.dart'; import 'package:camera_example/camera_controller.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/painting.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path_provider/path_provider.dart'; @@ -205,6 +207,63 @@ void main() { expect(duration, lessThan(recordingTime - timePaused)); }); + testWidgets('Set description while recording', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 500)); + + // set description while recording requires android >= 26 + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + final int sdk = androidInfo.version.sdkInt; + if (sdk < 26) { + await expectLater(() => controller.setDescription(cameras[1]), + throwsA(isA())); + // old devices don't switch after throwing an error + expect(controller.description, cameras[0]); + } else { + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + } + }); + + testWidgets('Set description', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + testWidgets( 'image streaming', (WidgetTester tester) async { diff --git a/packages/camera/camera_android/example/lib/camera_controller.dart b/packages/camera/camera_android/example/lib/camera_controller.dart index 8139dcdb022..fd4f09a027b 100644 --- a/packages/camera/camera_android/example/lib/camera_controller.dart +++ b/packages/camera/camera_android/example/lib/camera_controller.dart @@ -24,6 +24,7 @@ class CameraValue { required this.exposureMode, required this.focusMode, required this.deviceOrientation, + required this.description, this.lockedCaptureOrientation, this.recordingOrientation, this.isPreviewPaused = false, @@ -31,7 +32,7 @@ class CameraValue { }); /// Creates a new camera controller state for an uninitialized controller. - const CameraValue.uninitialized() + const CameraValue.uninitialized(CameraDescription description) : this( isInitialized: false, isRecordingVideo: false, @@ -43,6 +44,7 @@ class CameraValue { focusMode: FocusMode.auto, deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, + description: description, ); /// True after [CameraController.initialize] has completed successfully. @@ -92,6 +94,9 @@ class CameraValue { /// The orientation of the currently running video recording. final DeviceOrientation? recordingOrientation; + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -112,6 +117,7 @@ class CameraValue { Optional? lockedCaptureOrientation, Optional? recordingOrientation, bool? isPreviewPaused, + CameraDescription? description, Optional? previewPauseOrientation, }) { return CameraValue( @@ -132,6 +138,7 @@ class CameraValue { ? this.recordingOrientation : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + description: description ?? this.description, previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, @@ -165,14 +172,14 @@ class CameraValue { class CameraController extends ValueNotifier { /// Creates a new camera controller in an uninitialized state. CameraController( - this.description, + CameraDescription cameraDescription, this.resolutionPreset, { this.enableAudio = true, this.imageFormatGroup, - }) : super(const CameraValue.uninitialized()); + }) : super(CameraValue.uninitialized(cameraDescription)); /// The properties of the camera device controlled by this controller. - final CameraDescription description; + CameraDescription get description => value.description; /// The resolution this controller is targeting. /// @@ -202,7 +209,9 @@ class CameraController extends ValueNotifier { int get cameraId => _cameraId; /// Initializes the camera on the device. - Future initialize() async { + Future initialize() => _initializeWithDescription(description); + + Future _initializeWithDescription(CameraDescription description) async { final Completer initializeCompleter = Completer(); @@ -234,6 +243,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, + description: description, previewSize: await initializeCompleter.future .then((CameraInitializedEvent event) => Size( event.previewWidth, @@ -274,6 +284,16 @@ class CameraController extends ValueNotifier { previewPauseOrientation: const Optional.absent()); } + /// Sets the description of the camera. + Future setDescription(CameraDescription description) async { + if (value.isRecordingVideo) { + await CameraPlatform.instance.setDescriptionWhileRecording(description); + value = value.copyWith(description: description); + } else { + await _initializeWithDescription(description); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera_android/example/lib/main.dart b/packages/camera/camera_android/example/lib/main.dart index 4d98aed9a4c..3731325a49f 100644 --- a/packages/camera/camera_android/example/lib/main.dart +++ b/packages/camera/camera_android/example/lib/main.dart @@ -123,7 +123,7 @@ class _CameraExampleHomeState extends State if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { - onNewCameraSelected(cameraController.description); + _initializeCameraController(cameraController.description); } } @@ -603,10 +603,7 @@ class _CameraExampleHomeState extends State title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), groupValue: controller?.description, value: cameraDescription, - onChanged: - controller != null && controller!.value.isRecordingVideo - ? null - : onChanged, + onChanged: onChanged, ), ), ); @@ -639,17 +636,15 @@ class _CameraExampleHomeState extends State } Future onNewCameraSelected(CameraDescription cameraDescription) async { - final CameraController? oldController = controller; - if (oldController != null) { - // `controller` needs to be set to null before getting disposed, - // to avoid a race condition when we use the controller that is being - // disposed. This happens when camera permission dialog shows up, - // which triggers `didChangeAppLifecycleState`, which disposes and - // re-creates the controller. - controller = null; - await oldController.dispose(); + if (controller != null) { + return controller!.setDescription(cameraDescription); + } else { + return _initializeCameraController(cameraDescription); } + } + Future _initializeCameraController( + CameraDescription cameraDescription) async { final CameraController cameraController = CameraController( cameraDescription, kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index e23e31a886d..762d1b0703f 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - camera_platform_interface: ^2.3.1 + camera_platform_interface: ^2.4.0 flutter: sdk: flutter path_provider: ^2.0.0 @@ -23,6 +23,7 @@ dependencies: dev_dependencies: build_runner: ^2.1.10 + device_info_plus: ^8.1.0 flutter_driver: sdk: flutter flutter_test: @@ -32,3 +33,4 @@ dev_dependencies: flutter: uses-material-design: true + diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index 9ab9b578616..eca1003247c 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -505,6 +505,17 @@ class AndroidCamera extends CameraPlatform { ); } + @override + Future setDescriptionWhileRecording( + CameraDescription description) async { + await _channel.invokeMethod( + 'setDescriptionWhileRecording', + { + 'cameraName': description.name, + }, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index fb337191291..637658f4e69 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android description: Android implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.4 +version: 0.10.5 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,7 +18,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.3.1 + camera_platform_interface: ^2.4.0 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart index d80bd9cac7a..b56aa4e352a 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -700,6 +700,29 @@ void main() { ]); }); + test('Should set the description while recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setDescriptionWhileRecording': null}, + ); + const CameraDescription camera2Description = CameraDescription( + name: 'Test2', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0); + + // Act + await camera.setDescriptionWhileRecording(camera2Description); + + // Assert + expect(channel.log, [ + isMethodCall('setDescriptionWhileRecording', + arguments: { + 'cameraName': camera2Description.name, + }), + ]); + }); + test('Should set the flash mode', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index f0605b7914c..169596fa647 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.12 + +* Allows camera to be switched while video recording. + ## 0.9.11 * Adds back use of Optional type. diff --git a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart index 34d460d44ec..5a6935a9011 100644 --- a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -198,6 +198,51 @@ void main() { expect(duration, lessThan(recordingTime - timePaused)); }); + testWidgets('Set description while recording', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + + testWidgets('Set description', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + /// Start streaming with specifying the ImageFormatGroup. Future startStreaming(List cameras, ImageFormatGroup? imageFormatGroup) async { diff --git a/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d..9b41e7d8798 100644 --- a/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/camera/camera_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/camera/camera_avfoundation/example/ios/Podfile b/packages/camera/camera_avfoundation/example/ios/Podfile index 5bc7b7e8571..674ea9b53d8 100644 --- a/packages/camera/camera_avfoundation/example/ios/Podfile +++ b/packages/camera/camera_avfoundation/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -31,7 +31,7 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do - platform :ios, '9.0' + platform :ios, '11.0' inherit! :search_paths # Pods for testing pod 'OCMock', '~> 3.8.1' diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 03c80d79c57..bb819079798 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -290,6 +290,7 @@ }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = W4ZTW5E78A; }; }; }; @@ -574,7 +575,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -624,7 +625,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -637,7 +638,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = W4ZTW5E78A; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -649,7 +650,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.cameraExample; + PRODUCT_BUNDLE_IDENTIFIER = com.bradenbagby.test; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -659,7 +660,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = W4ZTW5E78A; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -671,7 +672,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.cameraExample; + PRODUCT_BUNDLE_IDENTIFIER = com.bradenbagby.test; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist index ff2e341a180..8044ea55312 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion en CFBundleExecutable diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m index 0ae4887eb63..b42aa34e2a1 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m @@ -11,15 +11,20 @@ OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) .andReturn(inputMock); - id sessionMock = OCMClassMock([AVCaptureSession class]); - OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op - OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + id videoSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([videoSessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([videoSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + id audioSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([audioSessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([audioSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); return [[FLTCam alloc] initWithCameraName:@"camera" resolutionPreset:@"medium" enableAudio:true orientation:UIDeviceOrientationPortrait - captureSession:sessionMock + videoCaptureSession:videoSessionMock + audioCaptureSession:audioSessionMock captureSessionQueue:captureSessionQueue error:nil]; } diff --git a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart index 524186816aa..6e1804328d5 100644 --- a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart +++ b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart @@ -24,6 +24,7 @@ class CameraValue { required this.exposureMode, required this.focusMode, required this.deviceOrientation, + required this.description, this.lockedCaptureOrientation, this.recordingOrientation, this.isPreviewPaused = false, @@ -31,7 +32,7 @@ class CameraValue { }); /// Creates a new camera controller state for an uninitialized controller. - const CameraValue.uninitialized() + const CameraValue.uninitialized(CameraDescription description) : this( isInitialized: false, isRecordingVideo: false, @@ -43,6 +44,7 @@ class CameraValue { focusMode: FocusMode.auto, deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, + description: description, ); /// True after [CameraController.initialize] has completed successfully. @@ -92,6 +94,9 @@ class CameraValue { /// The orientation of the currently running video recording. final DeviceOrientation? recordingOrientation; + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -112,6 +117,7 @@ class CameraValue { Optional? lockedCaptureOrientation, Optional? recordingOrientation, bool? isPreviewPaused, + CameraDescription? description, Optional? previewPauseOrientation, }) { return CameraValue( @@ -132,6 +138,7 @@ class CameraValue { ? this.recordingOrientation : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + description: description ?? this.description, previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, @@ -165,14 +172,14 @@ class CameraValue { class CameraController extends ValueNotifier { /// Creates a new camera controller in an uninitialized state. CameraController( - this.description, + CameraDescription cameraDescription, this.resolutionPreset, { this.enableAudio = true, this.imageFormatGroup, - }) : super(const CameraValue.uninitialized()); + }) : super(CameraValue.uninitialized(cameraDescription)); /// The properties of the camera device controlled by this controller. - final CameraDescription description; + CameraDescription get description => value.description; /// The resolution this controller is targeting. /// @@ -202,7 +209,9 @@ class CameraController extends ValueNotifier { int get cameraId => _cameraId; /// Initializes the camera on the device. - Future initialize() async { + Future initialize() => _initializeWithDescription(description); + + Future _initializeWithDescription(CameraDescription description) async { final Completer initializeCompleter = Completer(); @@ -234,6 +243,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, + description: description, previewSize: await initializeCompleter.future .then((CameraInitializedEvent event) => Size( event.previewWidth, @@ -274,6 +284,16 @@ class CameraController extends ValueNotifier { previewPauseOrientation: const Optional.absent()); } + /// Sets the description of the camera + Future setDescription(CameraDescription description) async { + if (value.isRecordingVideo) { + await CameraPlatform.instance.setDescriptionWhileRecording(description); + value = value.copyWith(description: description); + } else { + await _initializeWithDescription(description); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera_avfoundation/example/lib/main.dart b/packages/camera/camera_avfoundation/example/lib/main.dart index 4d98aed9a4c..3731325a49f 100644 --- a/packages/camera/camera_avfoundation/example/lib/main.dart +++ b/packages/camera/camera_avfoundation/example/lib/main.dart @@ -123,7 +123,7 @@ class _CameraExampleHomeState extends State if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { - onNewCameraSelected(cameraController.description); + _initializeCameraController(cameraController.description); } } @@ -603,10 +603,7 @@ class _CameraExampleHomeState extends State title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), groupValue: controller?.description, value: cameraDescription, - onChanged: - controller != null && controller!.value.isRecordingVideo - ? null - : onChanged, + onChanged: onChanged, ), ), ); @@ -639,17 +636,15 @@ class _CameraExampleHomeState extends State } Future onNewCameraSelected(CameraDescription cameraDescription) async { - final CameraController? oldController = controller; - if (oldController != null) { - // `controller` needs to be set to null before getting disposed, - // to avoid a race condition when we use the controller that is being - // disposed. This happens when camera permission dialog shows up, - // which triggers `didChangeAppLifecycleState`, which disposes and - // re-creates the controller. - controller = null; - await oldController.dispose(); + if (controller != null) { + return controller!.setDescription(cameraDescription); + } else { + return _initializeCameraController(cameraDescription); } + } + Future _initializeCameraController( + CameraDescription cameraDescription) async { final CameraController cameraController = CameraController( cameraDescription, kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index 7c85ba80719..872a22021d2 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - camera_platform_interface: ^2.2.0 + camera_platform_interface: ^2.4.0 flutter: sdk: flutter path_provider: ^2.0.0 diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m index b85f68d1f95..f9b2a911b67 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m @@ -261,6 +261,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [_camera pausePreviewWithResult:result]; } else if ([@"resumePreview" isEqualToString:call.method]) { [_camera resumePreviewWithResult:result]; + } else if ([@"setDescriptionWhileRecording" isEqualToString:call.method]) { + [_camera setDescriptionWhileRecording:(call.arguments[@"cameraName"]) result:result]; } else { [result sendNotImplemented]; } diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h index 85b8e2ae06f..df2a155855d 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h @@ -95,6 +95,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)applyFocusMode:(FLTFocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice; - (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result; - (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)setDescriptionWhileRecording:(NSString *)cameraName + result:(FLTThreadSafeFlutterResult *)result; - (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y; - (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y; - (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset; diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index a7d6cd24be3..d5247e00382 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -43,7 +43,8 @@ @interface FLTCam () setDescriptionWhileRecording( + CameraDescription description) async { + await _channel.invokeMethod( + 'setDescriptionWhileRecording', + { + 'cameraName': description.name, + }, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index b272a4c5c68..78c9156a7b7 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.11 +version: 0.9.12 environment: sdk: ">=2.14.0 <3.0.0" @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.3.1 + camera_platform_interface: ^2.4.0 flutter: sdk: flutter stream_transform: ^2.0.0 diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index 5d0b74cf0c0..e756f38ff12 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -701,6 +701,29 @@ void main() { ]); }); + test('Should set the description while recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setDescriptionWhileRecording': null}, + ); + const CameraDescription camera2Description = CameraDescription( + name: 'Test2', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0); + + // Act + await camera.setDescriptionWhileRecording(camera2Description); + + // Assert + expect(channel.log, [ + isMethodCall('setDescriptionWhileRecording', + arguments: { + 'cameraName': camera2Description.name, + }), + ]); + }); + test('Should set the flash mode', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart index f749b538665..f6750fb3155 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -22,8 +22,8 @@ import 'package:mockito/mockito.dart' as _i1; class _FakeLostData_0 extends _i1.Fake implements _i2.LostData {} -class _FakeLostDataResponse_1 extends _i1.Fake - implements _i2.LostDataResponse {} +class _FakeLostDataResponse_1 extends _i1.Fake implements _i2.LostDataResponse { +} /// A class which mocks [ImagePickerPlatform]. /// From 3f56c853051da2411ee4f20d90aaa0f60e85303d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 23 Feb 2023 11:22:29 -0500 Subject: [PATCH 02/13] Format fix --- .../image_picker/test/image_picker_test.mocks.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart index f6750fb3155..f749b538665 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -22,8 +22,8 @@ import 'package:mockito/mockito.dart' as _i1; class _FakeLostData_0 extends _i1.Fake implements _i2.LostData {} -class _FakeLostDataResponse_1 extends _i1.Fake implements _i2.LostDataResponse { -} +class _FakeLostDataResponse_1 extends _i1.Fake + implements _i2.LostDataResponse {} /// A class which mocks [ImagePickerPlatform]. /// From ea0637956c660f7fc0ff1a4657c9ae6748d4ea53 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 27 Feb 2023 10:47:33 -0700 Subject: [PATCH 03/13] logs warning instead of throwing a fatal error if eglswapbuffer fails in thread --- .../src/main/java/io/flutter/plugins/camera/VideoRenderer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java index b7128373b10..78ddd5301d1 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java @@ -358,8 +358,7 @@ public void draw(int viewportWidth, int viewportHeight, float[] texMatrix) { EGLExt.eglPresentationTimeANDROID(display, surface, uptimeMillis() * 1000000); if (!EGL14.eglSwapBuffers(display, surface)) { - throw new RuntimeException( - "eglSwapBuffers()" + GLUtils.getEGLErrorString(EGL14.eglGetError())); + Log.w(TAG, "eglSwapBuffers() " + GLUtils.getEGLErrorString(EGL14.eglGetError())); } } } From b8eab7e6232dcef9621065927ce6301559f017ec Mon Sep 17 00:00:00 2001 From: Braden Bagby <33461698+BradenBagby@users.noreply.github.com> Date: Mon, 27 Feb 2023 10:50:11 -0700 Subject: [PATCH 04/13] Update packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 157619f50e8..6d19536192b 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -1332,7 +1332,7 @@ public void setDescriptionWhileRecording( // See VideoRenderer.java requires API 26 to switch camera while recording if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { - result.error("setDescriptionWhileRecordingFailed", "Device does not support", null); + result.error("setDescriptionWhileRecordingFailed", "Device does not support switching the camera while recording", null); return; } From 27dfc9f6451fb76022f1204fcff0249dd4a7df0f Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Feb 2023 09:08:34 -0700 Subject: [PATCH 05/13] fixes test --- .../src/test/java/io/flutter/plugins/camera/CameraTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 1715514fca3..9bb7d7ffec4 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -616,7 +616,7 @@ public void setDescriptionWhileRecording() { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { verify(mockResult, times(1)) - .error(eq("setDescriptionWhileRecordingFailed"), eq("Device does not support"), eq(null)); + .error(eq("setDescriptionWhileRecordingFailed"), eq("Device does not support switching the camera while recording"), eq(null)); } else { verify(mockResult, times(1)).success(null); verify(mockResult, never()).error(any(), any(), any()); From 6e2a42f6f23341af6e20b1f7c966b34f8ea78a25 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Feb 2023 09:32:35 -0700 Subject: [PATCH 06/13] formats --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 5 ++++- .../main/java/io/flutter/plugins/camera/VideoRenderer.java | 2 +- .../src/test/java/io/flutter/plugins/camera/CameraTest.java | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 6d19536192b..e6521ffcbde 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -1332,7 +1332,10 @@ public void setDescriptionWhileRecording( // See VideoRenderer.java requires API 26 to switch camera while recording if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { - result.error("setDescriptionWhileRecordingFailed", "Device does not support switching the camera while recording", null); + result.error( + "setDescriptionWhileRecordingFailed", + "Device does not support switching the camera while recording", + null); return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java index 78ddd5301d1..62a70640961 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java @@ -358,7 +358,7 @@ public void draw(int viewportWidth, int viewportHeight, float[] texMatrix) { EGLExt.eglPresentationTimeANDROID(display, surface, uptimeMillis() * 1000000); if (!EGL14.eglSwapBuffers(display, surface)) { - Log.w(TAG, "eglSwapBuffers() " + GLUtils.getEGLErrorString(EGL14.eglGetError())); + Log.w(TAG, "eglSwapBuffers() " + GLUtils.getEGLErrorString(EGL14.eglGetError())); } } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 9bb7d7ffec4..9de33e3dc7a 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -616,7 +616,10 @@ public void setDescriptionWhileRecording() { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { verify(mockResult, times(1)) - .error(eq("setDescriptionWhileRecordingFailed"), eq("Device does not support switching the camera while recording"), eq(null)); + .error( + eq("setDescriptionWhileRecordingFailed"), + eq("Device does not support switching the camera while recording"), + eq(null)); } else { verify(mockResult, times(1)).success(null); verify(mockResult, never()).error(any(), any(), any()); From 8c88b10dc59f5c9ff3b1fde17a611f0bc3f1be47 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Feb 2023 09:41:34 -0700 Subject: [PATCH 07/13] fixes ios formatting from merge --- packages/camera/camera_avfoundation/ios/Classes/FLTCam.m | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index bd208e34b4b..31bffc91794 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -158,7 +158,6 @@ - (instancetype)initWithCameraName:(NSString *)cameraName _capturePhotoOutput = [AVCapturePhotoOutput new]; [_capturePhotoOutput setHighResolutionCaptureEnabled:YES]; [_videoCaptureSession addOutput:_capturePhotoOutput]; - _motionManager = [[CMMotionManager alloc] init]; [_motionManager startAccelerometerUpdates]; From 63dc77b744eb7c01670e5342d15a2ce7d3709a02 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Feb 2023 10:25:22 -0700 Subject: [PATCH 08/13] fixes test and removes device_info_plus dev dependency --- .../example/integration_test/camera_test.dart | 31 ++++++++++++------- .../camera_android/example/pubspec.yaml | 1 - .../camera/camera_avfoundation/pubspec.yaml | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index 6f20789ebf5..1630b50e2b0 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -8,7 +8,6 @@ import 'dart:ui'; import 'package:camera_android/camera_android.dart'; import 'package:camera_example/camera_controller.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -226,19 +225,27 @@ void main() { await controller.startVideoRecording(); sleep(const Duration(milliseconds: 500)); - // set description while recording requires android >= 26 - final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - final int sdk = androidInfo.version.sdkInt; - if (sdk < 26) { - await expectLater(() => controller.setDescription(cameras[1]), - throwsA(isA())); - // old devices don't switch after throwing an error - expect(controller.description, cameras[0]); - } else { + // SDK < 26 will throw a platform error when trying to switch and keep the same camera + // we accept either outcome here, while the native integration tests check the outcome based on the current Android SDK + bool failed = false; + try { await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); + } catch (err) { + expect(err, isA()); + expect( + (err as PlatformException).message, + equals( + 'Device does not support switching the camera while recording')); + failed = true; + } + sleep(const Duration(milliseconds: 500)); + + if (failed) { + // cameras did not switch + expect(controller.description, cameras[0]); + } else { + // cameras switched expect(controller.description, cameras[1]); } }); diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index 762d1b0703f..08f94ced1f3 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: dev_dependencies: build_runner: ^2.1.10 - device_info_plus: ^8.1.0 flutter_driver: sdk: flutter flutter_test: diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 0b359442f82..a7bc2ab8b14 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.12 +version: 0.9.13 environment: sdk: '>=2.18.0 <3.0.0' From 5d691c09c64e3c0ddfb93c8be17627931a56b8f8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Mar 2023 11:39:59 -0500 Subject: [PATCH 09/13] Warning fix --- .../android/src/main/java/io/flutter/plugins/camera/Camera.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 7b7ad18421a..08bbfd2c078 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -1099,7 +1099,7 @@ private void startPreviewWithVideoRendererStream() // get rotation for rendered video final PlatformChannel.DeviceOrientation lockedOrientation = - ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + cameraFeatures.getSensorOrientation() .getLockedCaptureOrientation(); DeviceOrientationManager orientationManager = cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); From bd6812dab5c94fc56548ee7bf9f71f7834468911 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Mar 2023 12:12:07 -0500 Subject: [PATCH 10/13] Format --- .../src/main/java/io/flutter/plugins/camera/Camera.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 08bbfd2c078..afdc3831f03 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -1099,8 +1099,7 @@ private void startPreviewWithVideoRendererStream() // get rotation for rendered video final PlatformChannel.DeviceOrientation lockedOrientation = - cameraFeatures.getSensorOrientation() - .getLockedCaptureOrientation(); + cameraFeatures.getSensorOrientation().getLockedCaptureOrientation(); DeviceOrientationManager orientationManager = cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); From b4e54a15ddf9c4d39e4139562cf933d39bf02c41 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 7 Mar 2023 11:08:51 -0700 Subject: [PATCH 11/13] removes sleep in setDescription tests --- .../camera_android/example/integration_test/camera_test.dart | 4 ---- .../example/integration_test/camera_test.dart | 4 ---- 2 files changed, 8 deletions(-) diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index 1630b50e2b0..3648900f693 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -223,7 +223,6 @@ void main() { await controller.prepareForVideoRecording(); await controller.startVideoRecording(); - sleep(const Duration(milliseconds: 500)); // SDK < 26 will throw a platform error when trying to switch and keep the same camera // we accept either outcome here, while the native integration tests check the outcome based on the current Android SDK @@ -239,7 +238,6 @@ void main() { failed = true; } - sleep(const Duration(milliseconds: 500)); if (failed) { // cameras did not switch @@ -264,9 +262,7 @@ void main() { ); await controller.initialize(); - sleep(const Duration(milliseconds: 500)); await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); expect(controller.description, cameras[1]); }); diff --git a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart index 5a6935a9011..315a34e9f9b 100644 --- a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -215,9 +215,7 @@ void main() { await controller.prepareForVideoRecording(); await controller.startVideoRecording(); - sleep(const Duration(milliseconds: 500)); await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); expect(controller.description, cameras[1]); }); @@ -236,9 +234,7 @@ void main() { ); await controller.initialize(); - sleep(const Duration(milliseconds: 500)); await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); expect(controller.description, cameras[1]); }); From b5fb5326da1fefb0e354d2e3ecc43ba3b0e99caa Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 7 Mar 2023 11:09:25 -0700 Subject: [PATCH 12/13] fixes comment --- .../camera_android/example/integration_test/camera_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index 3648900f693..fd478d4545e 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -225,7 +225,7 @@ void main() { await controller.startVideoRecording(); // SDK < 26 will throw a platform error when trying to switch and keep the same camera - // we accept either outcome here, while the native integration tests check the outcome based on the current Android SDK + // we accept either outcome here, while the native unit tests check the outcome based on the current Android SDK bool failed = false; try { await controller.setDescription(cameras[1]); From d7ace441f62ccedf3a9ae202ab2f4db84b29697a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 7 Mar 2023 13:29:05 -0500 Subject: [PATCH 13/13] Format fix --- .../camera_android/example/integration_test/camera_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index fd478d4545e..8d663074df7 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -238,7 +238,6 @@ void main() { failed = true; } - if (failed) { // cameras did not switch expect(controller.description, cameras[0]);