Skip to content

Commit b2ce3b0

Browse files
[camera_android] Wait for creating capture session when initializing (flutter#8894)
As discussed in the linked issue, this is an attempt to palliate one of the thread race conditions. In particular, returning to Dart possibly before creating the session and then let other function like "startImageStream" create a different session in parallel and interleave, causing the `java.lang.IllegalArgumentException: CaptureRequest contains unconfigured Input/Output Surface!` error. Fixes flutter/flutter#165092 cc @camsim99 ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 4e4cddd commit b2ce3b0

File tree

5 files changed

+119
-27
lines changed

5 files changed

+119
-27
lines changed

packages/camera/camera_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.10.10+3
2+
3+
* Waits for the creation of the capture session when initializing the camera to avoid thread race conditions.
4+
15
## 0.10.10+2
26

37
* Don't set the FPS range unless video recording. It can cause dark image previews on some devices becuse the auto exposure algorithm is more constrained after fixing the min/max FPS range to the same value. This change has the side effect that providing the `fps` parameter will not affect the camera preview when not video recording. And if you need a lower frame rate in your image streaming handler, you can skip frames by checking the time it passed since the last frame.

packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -372,16 +372,19 @@ public void open(Integer imageFormatGroup) throws CameraAccessException {
372372
public void onOpened(@NonNull CameraDevice device) {
373373
cameraDevice = new DefaultCameraDeviceWrapper(device);
374374
try {
375-
startPreview();
376-
if (!recordingVideo) { // only send initialization if we werent already recording and switching cameras
377-
dartMessenger.sendCameraInitializedEvent(
378-
resolutionFeature.getPreviewSize().getWidth(),
379-
resolutionFeature.getPreviewSize().getHeight(),
380-
cameraFeatures.getExposureLock().getValue(),
381-
cameraFeatures.getAutoFocus().getValue(),
382-
cameraFeatures.getExposurePoint().checkIsSupported(),
383-
cameraFeatures.getFocusPoint().checkIsSupported());
384-
}
375+
// only send initialization if we werent already recording and switching cameras
376+
Runnable onSuccess =
377+
recordingVideo
378+
? null
379+
: () ->
380+
dartMessenger.sendCameraInitializedEvent(
381+
resolutionFeature.getPreviewSize().getWidth(),
382+
resolutionFeature.getPreviewSize().getHeight(),
383+
cameraFeatures.getExposureLock().getValue(),
384+
cameraFeatures.getAutoFocus().getValue(),
385+
cameraFeatures.getExposurePoint().checkIsSupported(),
386+
cameraFeatures.getFocusPoint().checkIsSupported());
387+
startPreview(onSuccess);
385388
} catch (Exception e) {
386389
String message =
387390
(e.getMessage() == null)
@@ -870,7 +873,8 @@ public String stopVideoRecording() {
870873
}
871874
mediaRecorder.reset();
872875
try {
873-
startPreview();
876+
// Don't wait for start preview
877+
startPreview(null);
874878
} catch (CameraAccessException | IllegalStateException | InterruptedException e) {
875879
throw new Messages.FlutterError("videoRecordingFailed", e.getMessage(), null);
876880
}
@@ -1159,24 +1163,40 @@ public void resumePreview() {
11591163
null, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
11601164
}
11611165

1162-
public void startPreview() throws CameraAccessException, InterruptedException {
1166+
public void startPreview(@Nullable Runnable onSuccessCallback)
1167+
throws CameraAccessException, InterruptedException {
11631168
// If recording is already in progress, the camera is being flipped, so send it through the VideoRenderer to keep the correct orientation.
11641169
if (recordingVideo) {
1165-
startPreviewWithVideoRendererStream();
1170+
startPreviewWithVideoRendererStream(onSuccessCallback);
11661171
} else {
1167-
startRegularPreview();
1172+
startRegularPreview(onSuccessCallback);
11681173
}
11691174
}
11701175

1171-
private void startRegularPreview() throws CameraAccessException {
1172-
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
1176+
private void startRegularPreview(@Nullable Runnable onSuccessCallback)
1177+
throws CameraAccessException {
1178+
if (pictureImageReader == null || pictureImageReader.getSurface() == null) {
1179+
// noop
1180+
if (onSuccessCallback != null) {
1181+
onSuccessCallback.run();
1182+
}
1183+
return;
1184+
}
1185+
11731186
Log.i(TAG, "startPreview");
1174-
createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface());
1187+
createCaptureSession(
1188+
CameraDevice.TEMPLATE_PREVIEW, onSuccessCallback, pictureImageReader.getSurface());
11751189
}
11761190

1177-
private void startPreviewWithVideoRendererStream()
1191+
private void startPreviewWithVideoRendererStream(@Nullable Runnable onSuccessCallback)
11781192
throws CameraAccessException, InterruptedException {
1179-
if (videoRenderer == null) return;
1193+
if (videoRenderer == null) {
1194+
// noop
1195+
if (onSuccessCallback != null) {
1196+
onSuccessCallback.run();
1197+
}
1198+
return;
1199+
}
11801200

11811201
// get rotation for rendered video
11821202
final PlatformChannel.DeviceOrientation lockedOrientation =
@@ -1200,7 +1220,8 @@ private void startPreviewWithVideoRendererStream()
12001220
}
12011221
videoRenderer.setRotation(rotation);
12021222

1203-
createCaptureSession(CameraDevice.TEMPLATE_RECORD, videoRenderer.getInputSurface());
1223+
createCaptureSession(
1224+
CameraDevice.TEMPLATE_RECORD, onSuccessCallback, videoRenderer.getInputSurface());
12041225
}
12051226

12061227
public void startPreviewWithImageStream(EventChannel imageStreamChannel)

packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraApiImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ public void startImageStream() {
282282
@Override
283283
public void stopImageStream() {
284284
try {
285-
camera.startPreview();
285+
// Don't wait for start preview
286+
camera.startPreview(null);
286287
} catch (Exception e) {
287288
throw new Messages.FlutterError(e.getClass().getName(), e.getMessage(), null);
288289
}

packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import android.app.Activity;
1616
import android.content.Context;
17+
import android.graphics.ImageFormat;
1718
import android.graphics.SurfaceTexture;
1819
import android.hardware.camera2.*;
1920
import android.hardware.camera2.params.SessionConfiguration;
@@ -64,9 +65,11 @@
6465
import org.junit.Before;
6566
import org.junit.Test;
6667
import org.mockito.ArgumentMatcher;
68+
import org.mockito.InOrder;
6769
import org.mockito.MockedConstruction;
6870
import org.mockito.MockedStatic;
6971
import org.mockito.Mockito;
72+
import org.mockito.stubbing.Answer;
7073

7174
/**
7275
* As Pigeon-generated class, including FlutterError, do not implement equality, this helper class
@@ -134,6 +137,7 @@ public void close() {}
134137
}
135138

136139
public class CameraTest {
140+
private Activity mockActivity;
137141
private CameraProperties mockCameraProperties;
138142
private TestCameraFeatureFactory mockCameraFeatureFactory;
139143
private DartMessenger mockDartMessenger;
@@ -160,9 +164,10 @@ public void before() {
160164
mockHandlerFactory = mockStatic(Camera.HandlerFactory.class);
161165
mockHandler = mock(Handler.class);
162166

163-
final Activity mockActivity = mock(Activity.class);
164-
final TextureRegistry.SurfaceTextureEntry mockFlutterTexture =
167+
mockActivity = mock(Activity.class);
168+
TextureRegistry.SurfaceTextureEntry mockFlutterTexture =
165169
mock(TextureRegistry.SurfaceTextureEntry.class);
170+
when(mockFlutterTexture.surfaceTexture()).thenReturn(mock(SurfaceTexture.class));
166171
final String cameraName = "1";
167172
final ResolutionPreset resolutionPreset = ResolutionPreset.high;
168173
final boolean enableAudio = false;
@@ -211,6 +216,67 @@ public void after() throws IOException {
211216
mockRangeConstruction.close();
212217
}
213218

219+
@Test
220+
public void shouldSendCameraInitEventAfterCreatingTheSession()
221+
throws CameraAccessException, InterruptedException {
222+
// A spy is used so that we can properly configure the camera class with appropriate mocks
223+
// when camera.startPreview is called
224+
camera = spy(camera);
225+
226+
// Setup ImageReader
227+
final ImageReader mockImageReader = mock(ImageReader.class);
228+
final Surface mockSurface = mock(Surface.class);
229+
when(mockImageReader.getSurface()).thenReturn(mockSurface);
230+
231+
// Setup camera device.
232+
ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
233+
mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
234+
CameraDeviceWrapper fakeCameraDevice =
235+
new FakeCameraDeviceWrapper(mockRequestBuilders, mock(CameraCaptureSession.class));
236+
237+
// When starting the preview, initialize with mock properties
238+
doAnswer(
239+
invocation -> {
240+
camera.cameraDevice = fakeCameraDevice;
241+
camera.pictureImageReader = mockImageReader;
242+
return invocation.callRealMethod();
243+
})
244+
.when(camera)
245+
.startPreview(any());
246+
247+
// Setup CameraManager
248+
final CameraManager mockCameraManager = mock(CameraManager.class);
249+
when(mockActivity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager);
250+
251+
// Trigger the onOpened callback when calling CameraManager.openCamera.
252+
doAnswer(
253+
(Answer<Object>)
254+
invocation -> {
255+
CameraDevice.StateCallback cb = invocation.getArgument(1);
256+
cb.onOpened(mock(CameraDevice.class));
257+
return null;
258+
})
259+
.when(mockCameraManager)
260+
.openCamera(any(), any(), any(Handler.class));
261+
262+
// Setup ResolutionFeature
263+
ResolutionFeature resolutionFeature = mockCameraFeatureFactory.mockResolutionFeature;
264+
when(resolutionFeature.checkIsSupported()).thenReturn(true);
265+
final Size mockSize = mock(Size.class);
266+
when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
267+
when(resolutionFeature.getCaptureSize()).thenReturn(mockSize);
268+
269+
// Actual test that opens the camera, that mimics the camera initialize call.
270+
camera.open(ImageFormat.JPEG);
271+
272+
// Make sure that the initialize event is called after configuring the session
273+
InOrder inOrder = inOrder(camera.captureSession, mockDartMessenger);
274+
inOrder.verify(camera.captureSession, times(1)).setRepeatingRequest(any(), any(), any());
275+
inOrder
276+
.verify(mockDartMessenger, times(1))
277+
.sendCameraInitializedEvent(any(), any(), any(), any(), any(), any());
278+
}
279+
214280
@Test
215281
public void shouldNotImplementLifecycleObserverInterface() {
216282
Class<Camera> cameraClass = Camera.class;
@@ -705,7 +771,7 @@ public void startPreview_shouldPullStreamFromVideoRenderer()
705771
when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture);
706772
when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
707773

708-
camera.startPreview();
774+
camera.startPreview(null);
709775
verify(mockVideoRenderer, times(1))
710776
.getInputSurface(); // stream pulled from videoRenderer's surface.
711777
}
@@ -730,7 +796,7 @@ public void startPreview_shouldPullStreamFromImageReader()
730796
when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
731797
when(mockImageReader.getSurface()).thenReturn(mock(Surface.class));
732798

733-
camera.startPreview();
799+
camera.startPreview(null);
734800
verify(mockImageReader, times(2)) // we expect two calls to start regular preview.
735801
.getSurface(); // stream pulled from regular imageReader's surface.
736802
}
@@ -757,7 +823,7 @@ public void startPreview_shouldFlipRotation() throws InterruptedException, Camer
757823
when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
758824
when(mockCameraProperties.getLensFacing()).thenReturn(CameraMetadata.LENS_FACING_FRONT);
759825

760-
camera.startPreview();
826+
camera.startPreview(null);
761827
verify(mockVideoRenderer, times(1)).setRotation(180);
762828
}
763829

packages/camera/camera_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Android implementation of the camera plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
55

6-
version: 0.10.10+2
6+
version: 0.10.10+3
77

88
environment:
99
sdk: ^3.6.0

0 commit comments

Comments
 (0)