Skip to content

Commit 4709ccb

Browse files
BeMacizedfotiDim
authored andcommitted
[camera] Add Android & iOS implementations for pausing the camera preview (flutter#4258)
1 parent 341b849 commit 4709ccb

16 files changed

Lines changed: 498 additions & 52 deletions

File tree

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.2
2+
3+
* Added functions to pause and resume the camera preview.
4+
15
## 0.9.1+1
26

37
* Replace `device_info` reference with `device_info_plus` in the [README.md](README.md)

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

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class Camera
126126
private MediaRecorder mediaRecorder;
127127
/** True when recording video. */
128128
private boolean recordingVideo;
129+
/** True when the preview is paused. */
130+
private boolean pausedPreview;
129131

130132
private File captureFile;
131133

@@ -428,8 +430,10 @@ private void refreshPreviewCaptureSession(
428430
}
429431

430432
try {
431-
captureSession.setRepeatingRequest(
432-
previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
433+
if (!pausedPreview) {
434+
captureSession.setRepeatingRequest(
435+
previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
436+
}
433437

434438
if (onSuccessCallback != null) {
435439
onSuccessCallback.run();
@@ -834,33 +838,36 @@ public void setFocusMode(final Result result, @NonNull FocusMode newMode) {
834838
* For focus mode an extra step of actually locking/unlocking the
835839
* focus has to be done, in order to ensure it goes into the correct state.
836840
*/
837-
switch (newMode) {
838-
case locked:
839-
// Perform a single focus trigger.
840-
lockAutoFocus();
841-
if (captureSession == null) {
842-
Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
843-
return;
844-
}
845-
846-
// Set AF state to idle again.
847-
previewRequestBuilder.set(
848-
CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
849-
850-
try {
851-
captureSession.setRepeatingRequest(
852-
previewRequestBuilder.build(), null, backgroundHandler);
853-
} catch (CameraAccessException e) {
854-
if (result != null) {
855-
result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
841+
if (!pausedPreview) {
842+
switch (newMode) {
843+
case locked:
844+
// Perform a single focus trigger.
845+
if (captureSession == null) {
846+
Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
847+
return;
848+
}
849+
lockAutoFocus();
850+
851+
// Set AF state to idle again.
852+
previewRequestBuilder.set(
853+
CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
854+
855+
try {
856+
captureSession.setRepeatingRequest(
857+
previewRequestBuilder.build(), null, backgroundHandler);
858+
} catch (CameraAccessException e) {
859+
if (result != null) {
860+
result.error(
861+
"setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
862+
}
863+
return;
856864
}
857-
return;
858-
}
859-
break;
860-
case auto:
861-
// Cancel current AF trigger and set AF to idle again.
862-
unlockAutoFocus();
863-
break;
865+
break;
866+
case auto:
867+
// Cancel current AF trigger and set AF to idle again.
868+
unlockAutoFocus();
869+
break;
870+
}
864871
}
865872

866873
if (result != null) {
@@ -966,6 +973,19 @@ public void unlockCaptureOrientation() {
966973
cameraFeatures.getSensorOrientation().unlockCaptureOrientation();
967974
}
968975

976+
/** Pause the preview from dart. */
977+
public void pausePreview() throws CameraAccessException {
978+
this.pausedPreview = true;
979+
this.captureSession.stopRepeating();
980+
}
981+
982+
/** Resume the preview from dart. */
983+
public void resumePreview() {
984+
this.pausedPreview = false;
985+
this.refreshPreviewCaptureSession(
986+
null, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
987+
}
988+
969989
public void startPreview() throws CameraAccessException {
970990
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
971991
Log.i(TAG, "startPreview");
@@ -1022,8 +1042,8 @@ public void onError(String errorCode, String errorMessage) {
10221042
private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) {
10231043
imageStreamReader.setOnImageAvailableListener(
10241044
reader -> {
1025-
// Use acquireNextImage since image reader is only for one image.
10261045
Image img = reader.acquireNextImage();
1046+
// Use acquireNextImage since image reader is only for one image.
10271047
if (img == null) return;
10281048

10291049
List<Map<String, Object>> planes = new ArrayList<>();

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
339339
}
340340
break;
341341
}
342+
case "pausePreview":
343+
{
344+
try {
345+
camera.pausePreview();
346+
result.success(null);
347+
} catch (Exception e) {
348+
handleException(e, result);
349+
}
350+
break;
351+
}
352+
case "resumePreview":
353+
{
354+
camera.resumePreview();
355+
result.success(null);
356+
break;
357+
}
342358
case "dispose":
343359
{
344360
if (camera != null) {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,33 @@ public void unlockCaptureOrientation_shouldUnlockCaptureOrientation() {
744744
verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation();
745745
}
746746

747+
@Test
748+
public void pausePreview_shouldPausePreview() throws CameraAccessException {
749+
camera.pausePreview();
750+
751+
assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), true);
752+
verify(mockCaptureSession, times(1)).stopRepeating();
753+
}
754+
755+
@Test
756+
public void resumePreview_shouldResumePreview() throws CameraAccessException {
757+
camera.resumePreview();
758+
759+
assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), false);
760+
verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any());
761+
}
762+
763+
@Test
764+
public void resumePreview_shouldSendErrorEventOnCameraAccessException()
765+
throws CameraAccessException {
766+
when(mockCaptureSession.setRepeatingRequest(any(), any(), any()))
767+
.thenThrow(new CameraAccessException(0));
768+
769+
camera.resumePreview();
770+
771+
verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any());
772+
}
773+
747774
private static class TestCameraFeatureFactory implements CameraFeatureFactory {
748775
private final AutoFocusFeature mockAutoFocusFeature;
749776
private final ExposureLockFeature mockExposureLockFeature;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camera;
6+
7+
import static org.mockito.Mockito.doThrow;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.times;
10+
import static org.mockito.Mockito.verify;
11+
12+
import android.app.Activity;
13+
import android.hardware.camera2.CameraAccessException;
14+
import io.flutter.plugin.common.BinaryMessenger;
15+
import io.flutter.plugin.common.MethodCall;
16+
import io.flutter.plugin.common.MethodChannel;
17+
import io.flutter.plugins.camera.utils.TestUtils;
18+
import io.flutter.view.TextureRegistry;
19+
import org.junit.Before;
20+
import org.junit.Test;
21+
22+
public class MethodCallHandlerImplTest {
23+
24+
MethodChannel.MethodCallHandler handler;
25+
MethodChannel.Result mockResult;
26+
Camera mockCamera;
27+
28+
@Before
29+
public void setUp() {
30+
handler =
31+
new MethodCallHandlerImpl(
32+
mock(Activity.class),
33+
mock(BinaryMessenger.class),
34+
mock(CameraPermissions.class),
35+
mock(CameraPermissions.PermissionsRegistry.class),
36+
mock(TextureRegistry.class),
37+
null);
38+
mockResult = mock(MethodChannel.Result.class);
39+
mockCamera = mock(Camera.class);
40+
TestUtils.setPrivateField(handler, "camera", mockCamera);
41+
}
42+
43+
@Test
44+
public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult()
45+
throws CameraAccessException {
46+
handler.onMethodCall(new MethodCall("pausePreview", null), mockResult);
47+
48+
verify(mockCamera, times(1)).pausePreview();
49+
verify(mockResult, times(1)).success(null);
50+
}
51+
52+
@Test
53+
public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException()
54+
throws CameraAccessException {
55+
doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview();
56+
57+
handler.onMethodCall(new MethodCall("pausePreview", null), mockResult);
58+
59+
verify(mockResult, times(1)).error("CameraAccess", null, null);
60+
}
61+
62+
@Test
63+
public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() {
64+
handler.onMethodCall(new MethodCall("resumePreview", null), mockResult);
65+
66+
verify(mockCamera, times(1)).resumePreview();
67+
verify(mockResult, times(1)).success(null);
68+
}
69+
}

packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,15 @@ public static <T> void setPrivateField(T instance, String fieldName, Object newV
3333
Assert.fail("Unable to mock private field: " + fieldName);
3434
}
3535
}
36+
37+
public static <T> Object getPrivateField(T instance, String fieldName) {
38+
try {
39+
Field field = instance.getClass().getDeclaredField(fieldName);
40+
field.setAccessible(true);
41+
return field.get(instance);
42+
} catch (Exception e) {
43+
Assert.fail("Unable to mock private field: " + fieldName);
44+
return null;
45+
}
46+
}
3647
}

packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
1919
A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; };
2020
D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; };
21+
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; };
2122
/* End PBXBuildFile section */
2223

2324
/* Begin PBXContainerItemProxy section */
@@ -68,6 +69,7 @@
6869
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6970
A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
7071
D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
72+
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = "<group>"; };
7173
/* End PBXFileReference section */
7274

7375
/* Begin PBXFrameworksBuildPhase section */
@@ -96,6 +98,7 @@
9698
03BB766A2665316900CE5A93 /* CameraFocusTests.m */,
9799
03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */,
98100
03BB766C2665316900CE5A93 /* Info.plist */,
101+
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
99102
);
100103
path = RunnerTests;
101104
sourceTree = "<group>";
@@ -359,6 +362,7 @@
359362
buildActionMask = 2147483647;
360363
files = (
361364
03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */,
365+
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
362366
334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
363367
);
364368
runOnlyForDeploymentPostprocessing = 0;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
@import camera;
6+
@import XCTest;
7+
@import AVFoundation;
8+
#import <OCMock/OCMock.h>
9+
10+
@interface FLTCam : NSObject <FlutterTexture,
11+
AVCaptureVideoDataOutputSampleBufferDelegate,
12+
AVCaptureAudioDataOutputSampleBufferDelegate>
13+
@property(assign, nonatomic) BOOL isPreviewPaused;
14+
- (void)pausePreviewWithResult:(FlutterResult)result;
15+
- (void)resumePreviewWithResult:(FlutterResult)result;
16+
@end
17+
18+
@interface CameraPreviewPauseTests : XCTestCase
19+
@property(readonly, nonatomic) FLTCam* camera;
20+
@end
21+
22+
@implementation CameraPreviewPauseTests
23+
24+
- (void)setUp {
25+
_camera = [[FLTCam alloc] init];
26+
}
27+
28+
- (void)testPausePreviewWithResult_shouldPausePreview {
29+
XCTestExpectation* resultExpectation =
30+
[self expectationWithDescription:@"Succeeding result with nil value"];
31+
[_camera pausePreviewWithResult:^void(id _Nullable result) {
32+
XCTAssertNil(result);
33+
[resultExpectation fulfill];
34+
}];
35+
[self waitForExpectationsWithTimeout:2.0 handler:nil];
36+
XCTAssertTrue(_camera.isPreviewPaused);
37+
}
38+
39+
- (void)testResumePreviewWithResult_shouldResumePreview {
40+
XCTestExpectation* resultExpectation =
41+
[self expectationWithDescription:@"Succeeding result with nil value"];
42+
[_camera resumePreviewWithResult:^void(id _Nullable result) {
43+
XCTAssertNil(result);
44+
[resultExpectation fulfill];
45+
}];
46+
[self waitForExpectationsWithTimeout:2.0 handler:nil];
47+
XCTAssertFalse(_camera.isPreviewPaused);
48+
}
49+
50+
@end

packages/camera/camera/example/lib/main.dart

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,16 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
530530
cameraController.value.isRecordingVideo
531531
? onStopButtonPressed
532532
: null,
533-
)
533+
),
534+
IconButton(
535+
icon: const Icon(Icons.pause_presentation),
536+
color:
537+
cameraController != null && cameraController.value.isPreviewPaused
538+
? Colors.red
539+
: Colors.blue,
540+
onPressed:
541+
cameraController == null ? null : onPausePreviewButtonPressed,
542+
),
534543
],
535544
);
536545
}
@@ -747,6 +756,23 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
747756
});
748757
}
749758

759+
Future<void> onPausePreviewButtonPressed() async {
760+
final CameraController? cameraController = controller;
761+
762+
if (cameraController == null || !cameraController.value.isInitialized) {
763+
showInSnackBar('Error: select a camera first.');
764+
return;
765+
}
766+
767+
if (cameraController.value.isPreviewPaused) {
768+
await cameraController.resumePreview();
769+
} else {
770+
await cameraController.pausePreview();
771+
}
772+
773+
if (mounted) setState(() {});
774+
}
775+
750776
void onPauseButtonPressed() {
751777
pauseVideoRecording().then((_) {
752778
if (mounted) setState(() {});

0 commit comments

Comments
 (0)