Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.2

* Added functions to pause and resume the camera preview.

## 0.9.1+1

* Replace `device_info` reference with `device_info_plus` in the [README.md](README.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ class Camera
private MediaRecorder mediaRecorder;
/** True when recording video. */
private boolean recordingVideo;
/** True when the preview is paused. */
private boolean pausedPreview;

private File captureFile;

Expand Down Expand Up @@ -428,8 +430,10 @@ private void refreshPreviewCaptureSession(
}

try {
captureSession.setRepeatingRequest(
previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
if (!pausedPreview) {
captureSession.setRepeatingRequest(
previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler);
}

if (onSuccessCallback != null) {
onSuccessCallback.run();
Expand Down Expand Up @@ -834,33 +838,36 @@ public void setFocusMode(final Result result, @NonNull FocusMode newMode) {
* For focus mode an extra step of actually locking/unlocking the
* focus has to be done, in order to ensure it goes into the correct state.
*/
switch (newMode) {
case locked:
// Perform a single focus trigger.
lockAutoFocus();
if (captureSession == null) {
Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
return;
}

// Set AF state to idle again.
previewRequestBuilder.set(
CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);

try {
captureSession.setRepeatingRequest(
previewRequestBuilder.build(), null, backgroundHandler);
} catch (CameraAccessException e) {
if (result != null) {
result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
if (!pausedPreview) {
switch (newMode) {
case locked:
// Perform a single focus trigger.
if (captureSession == null) {
Log.i(TAG, "[unlockAutoFocus] captureSession null, returning");
return;
}
lockAutoFocus();

// Set AF state to idle again.
previewRequestBuilder.set(
CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE);

try {
captureSession.setRepeatingRequest(
previewRequestBuilder.build(), null, backgroundHandler);
} catch (CameraAccessException e) {
if (result != null) {
result.error(
"setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null);
}
return;
}
return;
}
break;
case auto:
// Cancel current AF trigger and set AF to idle again.
unlockAutoFocus();
break;
break;
case auto:
// Cancel current AF trigger and set AF to idle again.
unlockAutoFocus();
break;
}
}

if (result != null) {
Expand Down Expand Up @@ -966,6 +973,19 @@ public void unlockCaptureOrientation() {
cameraFeatures.getSensorOrientation().unlockCaptureOrientation();
}

/** Pause the preview from dart. */
public void pausePreview() throws CameraAccessException {
this.pausedPreview = true;
this.captureSession.stopRepeating();
}

/** Resume the preview from dart. */
public void resumePreview() {
this.pausedPreview = false;
this.refreshPreviewCaptureSession(
null, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
}

public void startPreview() throws CameraAccessException {
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
Log.i(TAG, "startPreview");
Expand Down Expand Up @@ -1022,8 +1042,8 @@ public void onError(String errorCode, String errorMessage) {
private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) {
imageStreamReader.setOnImageAvailableListener(
reader -> {
// Use acquireNextImage since image reader is only for one image.
Image img = reader.acquireNextImage();
// Use acquireNextImage since image reader is only for one image.
if (img == null) return;

List<Map<String, Object>> planes = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
break;
}
case "pausePreview":
{
try {
camera.pausePreview();
result.success(null);
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "resumePreview":
{
camera.resumePreview();
result.success(null);
break;
}
case "dispose":
{
if (camera != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,33 @@ public void unlockCaptureOrientation_shouldUnlockCaptureOrientation() {
verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation();
}

@Test
public void pausePreview_shouldPausePreview() throws CameraAccessException {
camera.pausePreview();

assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), true);
verify(mockCaptureSession, times(1)).stopRepeating();
}

@Test
public void resumePreview_shouldResumePreview() throws CameraAccessException {
camera.resumePreview();

assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), false);
verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any());
}

@Test
public void resumePreview_shouldSendErrorEventOnCameraAccessException()
throws CameraAccessException {
when(mockCaptureSession.setRepeatingRequest(any(), any(), any()))
.thenThrow(new CameraAccessException(0));

camera.resumePreview();

verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any());
}

private static class TestCameraFeatureFactory implements CameraFeatureFactory {
private final AutoFocusFeature mockAutoFocusFeature;
private final ExposureLockFeature mockExposureLockFeature;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Activity;
import android.hardware.camera2.CameraAccessException;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.camera.utils.TestUtils;
import io.flutter.view.TextureRegistry;
import org.junit.Before;
import org.junit.Test;

public class MethodCallHandlerImplTest {

MethodChannel.MethodCallHandler handler;
MethodChannel.Result mockResult;
Camera mockCamera;

@Before
public void setUp() {
handler =
new MethodCallHandlerImpl(
mock(Activity.class),
mock(BinaryMessenger.class),
mock(CameraPermissions.class),
mock(CameraPermissions.PermissionsRegistry.class),
mock(TextureRegistry.class),
null);
mockResult = mock(MethodChannel.Result.class);
mockCamera = mock(Camera.class);
TestUtils.setPrivateField(handler, "camera", mockCamera);
}

@Test
public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult()
throws CameraAccessException {
handler.onMethodCall(new MethodCall("pausePreview", null), mockResult);

verify(mockCamera, times(1)).pausePreview();
verify(mockResult, times(1)).success(null);
}

@Test
public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException()
throws CameraAccessException {
doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview();

handler.onMethodCall(new MethodCall("pausePreview", null), mockResult);

verify(mockResult, times(1)).error("CameraAccess", null, null);
}

@Test
public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() {
handler.onMethodCall(new MethodCall("resumePreview", null), mockResult);

verify(mockCamera, times(1)).resumePreview();
verify(mockResult, times(1)).success(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,15 @@ public static <T> void setPrivateField(T instance, String fieldName, Object newV
Assert.fail("Unable to mock private field: " + fieldName);
}
}

public static <T> Object getPrivateField(T instance, String fieldName) {
try {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(instance);
} catch (Exception e) {
Assert.fail("Unable to mock private field: " + fieldName);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; };
D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; };
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -68,6 +69,7 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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>"; };
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>"; };
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -96,6 +98,7 @@
03BB766A2665316900CE5A93 /* CameraFocusTests.m */,
03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */,
03BB766C2665316900CE5A93 /* Info.plist */,
E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */,
);
path = RunnerTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -359,6 +362,7 @@
buildActionMask = 2147483647;
files = (
03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */,
E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */,
334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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.

@import camera;
@import XCTest;
@import AVFoundation;
#import <OCMock/OCMock.h>

@interface FLTCam : NSObject <FlutterTexture,
AVCaptureVideoDataOutputSampleBufferDelegate,
AVCaptureAudioDataOutputSampleBufferDelegate>
@property(assign, nonatomic) BOOL isPreviewPaused;
- (void)pausePreviewWithResult:(FlutterResult)result;
- (void)resumePreviewWithResult:(FlutterResult)result;
@end

@interface CameraPreviewPauseTests : XCTestCase
@property(readonly, nonatomic) FLTCam* camera;
@end

@implementation CameraPreviewPauseTests

- (void)setUp {
_camera = [[FLTCam alloc] init];
}

- (void)testPausePreviewWithResult_shouldPausePreview {
XCTestExpectation* resultExpectation =
[self expectationWithDescription:@"Succeeding result with nil value"];
[_camera pausePreviewWithResult:^void(id _Nullable result) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:2.0 handler:nil];
XCTAssertTrue(_camera.isPreviewPaused);
}

- (void)testResumePreviewWithResult_shouldResumePreview {
XCTestExpectation* resultExpectation =
[self expectationWithDescription:@"Succeeding result with nil value"];
[_camera resumePreviewWithResult:^void(id _Nullable result) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:2.0 handler:nil];
XCTAssertFalse(_camera.isPreviewPaused);
}

@end
28 changes: 27 additions & 1 deletion packages/camera/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,16 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
cameraController.value.isRecordingVideo
? onStopButtonPressed
: null,
)
),
IconButton(
icon: const Icon(Icons.pause_presentation),
color:
cameraController != null && cameraController.value.isPreviewPaused
? Colors.red
: Colors.blue,
onPressed:
cameraController == null ? null : onPausePreviewButtonPressed,
),
],
);
}
Expand Down Expand Up @@ -747,6 +756,23 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
});
}

Future<void> onPausePreviewButtonPressed() async {
final CameraController? cameraController = controller;

if (cameraController == null || !cameraController.value.isInitialized) {
showInSnackBar('Error: select a camera first.');
return;
}

if (cameraController.value.isPreviewPaused) {
await cameraController.resumePreview();
} else {
await cameraController.pausePreview();
}

if (mounted) setState(() {});
}

void onPauseButtonPressed() {
pauseVideoRecording().then((_) {
if (mounted) setState(() {});
Expand Down
Loading