Skip to content

[camera_android] Support concurrently image capture and image streaming #4332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
dec3d69
Merge remote-tracking branch 'upstream/main' into camx_occ
camsim99 May 1, 2023
0e0333b
Merge remote-tracking branch 'upstream/main'
camsim99 May 2, 2023
bd7ac99
Merge remote-tracking branch 'upstream/main'
camsim99 May 3, 2023
5c3363b
Merge remote-tracking branch 'upstream/main'
camsim99 May 10, 2023
fed9621
Undo changes
camsim99 May 10, 2023
5aabe34
Merge remote-tracking branch 'upstream/main'
camsim99 May 12, 2023
2b9a352
Merge remote-tracking branch 'upstream/main'
camsim99 May 25, 2023
a1173da
Merge remote-tracking branch 'upstream/main'
camsim99 May 30, 2023
cbc3d6b
Merge remote-tracking branch 'upstream/main'
camsim99 May 30, 2023
cae5a4c
Merge remote-tracking branch 'upstream/main'
camsim99 Jun 1, 2023
72283db
Merge remote-tracking branch 'upstream/main'
camsim99 Jun 5, 2023
166a77c
Merge remote-tracking branch 'upstream/main'
camsim99 Jun 5, 2023
399780e
Merge remote-tracking branch 'upstream/main'
camsim99 Jun 14, 2023
8d5d0e7
Merge remote-tracking branch 'upstream/main'
camsim99 Jun 26, 2023
8de6de1
Make fixes
camsim99 Jun 27, 2023
f731d64
Merge remote-tracking branch 'upstream/main' into xi_debug
camsim99 Jun 27, 2023
0710eb8
Initial stab at tests
camsim99 Jun 27, 2023
72365fa
Test attmept 2
camsim99 Jun 27, 2023
4bb3601
some test fixes
camsim99 Jun 27, 2023
7a45621
Test changes, removed test for ease
camsim99 Jun 27, 2023
73168b5
Add fix for invalid use of matchers
camsim99 Jun 28, 2023
69d7ffe
add debug notes
camsim99 Jun 28, 2023
30a8880
Bump version
camsim99 Jun 28, 2023
3abad3b
Mock prepareMediaRecorder
camsim99 Jun 28, 2023
9e44f56
Change camera to spy for mock
camsim99 Jun 28, 2023
c493b9d
Merge remote-tracking branch 'upstream/main' into xi_debug
camsim99 Jun 28, 2023
22ebff0
Mock prepareRecording
camsim99 Jun 28, 2023
7d39d57
Bump times because createCaptureSession
camsim99 Jun 28, 2023
48177de
Bump times because createCaptureSession
camsim99 Jun 28, 2023
6f9e085
Uncomment other tests, add comments
camsim99 Jun 28, 2023
a0af9cc
Add back imports
camsim99 Jun 28, 2023
ce3c25c
Add one last comment
camsim99 Jun 28, 2023
ede8e88
format
camsim99 Jun 28, 2023
bcfa57c
Refactor and check if surface mock needed
camsim99 Jun 28, 2023
6c1ba5e
fix tests
camsim99 Jun 28, 2023
abf390f
Formatting
camsim99 Jun 28, 2023
00ce873
Add mock for test
camsim99 Jun 28, 2023
10331d6
Correct to 2
camsim99 Jun 28, 2023
c29896c
Merge remote-tracking branch 'upstream/main' into xi_debug
camsim99 Jun 28, 2023
d9347f9
Formatting
camsim99 Jun 28, 2023
020a5e2
I'm not sure why I'm getting so confused
camsim99 Jun 28, 2023
ea9c373
format
camsim99 Jun 28, 2023
f784750
Add periods
camsim99 Jun 28, 2023
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
3 changes: 2 additions & 1 deletion packages/camera/camera_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## NEXT
## 0.10.8+3

* Fixes unawaited_futures violations.
* Removes duplicate line in `MediaRecorderBuilder.java`.
* Adds support for concurrently capturing images and image streaming/recording.

## 0.10.8+2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import android.hardware.camera2.params.SessionConfiguration;
import android.media.CamcorderProfile;
import android.media.EncoderProfiles;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaRecorder;
import android.os.Build;
Expand Down Expand Up @@ -414,8 +415,14 @@ private void createCaptureSession(

List<Surface> remainingSurfaces = Arrays.asList(surfaces);
if (templateType != CameraDevice.TEMPLATE_PREVIEW) {
// If it is not preview mode, add all surfaces as targets.
// If it is not preview mode, add all surfaces as targets
// except the surface used for still capture as this should
// not be part of a repeating request.
Surface pictureImageReaderSurface = pictureImageReader.getSurface();
for (Surface surface : remainingSurfaces) {
if (surface == pictureImageReaderSurface) {
continue;
}
previewRequestBuilder.addTarget(surface);
}
}
Expand Down Expand Up @@ -539,6 +546,10 @@ private void startCapture(boolean record, boolean stream) throws CameraAccessExc
surfaces.add(imageStreamReader.getSurface());
}

// Add pictureImageReader surface to allow for still capture
// during recording/image streaming.
surfaces.add(pictureImageReader.getSurface());

createCaptureSession(
CameraDevice.TEMPLATE_RECORD, successCallback, surfaces.toArray(new Surface[0]));
}
Expand Down Expand Up @@ -659,7 +670,6 @@ public void onCaptureCompleted(
};

try {
captureSession.stopRepeating();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated fix: This call should not be necessary for still capture to proceed.

Log.i(TAG, "sending capture request");
captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler);
} catch (CameraAccessException e) {
Expand Down Expand Up @@ -1140,10 +1150,15 @@ public void startPreviewWithImageStream(EventChannel imageStreamChannel)
public void onImageAvailable(ImageReader reader) {
Log.i(TAG, "onImageAvailable");

// Use acquireNextImage since image reader is only for one image.
Image image = reader.acquireNextImage();
if (image == null) {
return;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated fix: acquireNextImage may return null, so added null check.

backgroundHandler.post(
new ImageSaver(
// Use acquireNextImage since image reader is only for one image.
reader.acquireNextImage(),
image,
captureFile,
new ImageSaver.Callback() {
@Override
Expand All @@ -1159,7 +1174,8 @@ public void onError(String errorCode, String errorMessage) {
cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW);
}

private void prepareRecording(@NonNull Result result) {
@VisibleForTesting
void prepareRecording(@NonNull Result result) {
final File outputDir = applicationContext.getCacheDir();
try {
captureFile = File.createTempFile("REC", ".mp4", outputDir);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -36,6 +39,7 @@
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleObserver;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.camera.features.CameraFeatureFactory;
import io.flutter.plugins.camera.features.CameraFeatures;
Expand All @@ -56,8 +60,10 @@
import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager;
import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature;
import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
import io.flutter.plugins.camera.media.ImageStreamReader;
import io.flutter.plugins.camera.utils.TestUtils;
import io.flutter.view.TextureRegistry;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
Expand Down Expand Up @@ -638,6 +644,8 @@ public void startPreview_shouldPullStreamFromVideoRenderer()
TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer);
CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders);
TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera);
ImageReader mockPictureImageReader = mock(ImageReader.class);
TestUtils.setPrivateField(camera, "pictureImageReader", mockPictureImageReader);

TextureRegistry.SurfaceTextureEntry cameraFlutterTexture =
(TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture");
Expand Down Expand Up @@ -674,9 +682,10 @@ public void startPreview_shouldPullStreamFromImageReader()

when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture);
when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
when(mockImageReader.getSurface()).thenReturn(mock(Surface.class));

camera.startPreview();
verify(mockImageReader, times(1))
verify(mockImageReader, times(2)) // we expect two calls to start regular preview.
.getSurface(); // stream pulled from regular imageReader's surface.
}

Expand All @@ -692,6 +701,8 @@ public void startPreview_shouldFlipRotation() throws InterruptedException, Camer
TestUtils.setPrivateField(camera, "initialCameraFacing", CameraMetadata.LENS_FACING_BACK);
CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders);
TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera);
ImageReader mockPictureImageReader = mock(ImageReader.class);
TestUtils.setPrivateField(camera, "pictureImageReader", mockPictureImageReader);

TextureRegistry.SurfaceTextureEntry cameraFlutterTexture =
(TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture");
Expand All @@ -707,6 +718,39 @@ public void startPreview_shouldFlipRotation() throws InterruptedException, Camer
verify(mockVideoRenderer, times(1)).setRotation(180);
}

@Test
public void startPreviewWithImageStream_shouldPullStreamsFromImageReaders()
throws InterruptedException, CameraAccessException {
ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
Size mockSize = mock(Size.class);
ImageReader mockPictureImageReader = mock(ImageReader.class);
ImageStreamReader mockImageStreamReader = mock(ImageStreamReader.class);
TestUtils.setPrivateField(camera, "recordingVideo", false);
TestUtils.setPrivateField(camera, "pictureImageReader", mockPictureImageReader);
CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders);
TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera);
camera.imageStreamReader = mockImageStreamReader;

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.startPreviewWithImageStream(mock(EventChannel.class));
verify(mockImageStreamReader, times(1))
.getSurface(); // stream pulled from image streaming imageReader's surface.
verify(
mockPictureImageReader,
times(2)) // we expect one call to start the capture, one to create the capture session.
.getSurface(); // stream pulled from regular imageReader's surface.
}

@Test
public void setDescriptionWhileRecording_shouldErrorWhenNotRecording() {
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
Expand Down Expand Up @@ -806,6 +850,43 @@ public void setFocusMode_shouldSendErrorEventOnUnlockAutoFocusCameraAccessExcept
verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any());
}

@Test
public void startVideoRecording_shouldPullStreamsFromMediaRecorderAndImageReader()
throws InterruptedException, IOException, CameraAccessException {
Camera cameraSpy = spy(camera);
ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
Size mockSize = mock(Size.class);
MediaRecorder mockMediaRecorder = mock(MediaRecorder.class);
ImageReader mockPictureImageReader = mock(ImageReader.class);
TestUtils.setPrivateField(cameraSpy, "mediaRecorder", mockMediaRecorder);
TestUtils.setPrivateField(cameraSpy, "recordingVideo", false);
TestUtils.setPrivateField(cameraSpy, "pictureImageReader", mockPictureImageReader);
CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders);
TestUtils.setPrivateField(cameraSpy, "cameraDevice", fakeCamera);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);

TextureRegistry.SurfaceTextureEntry cameraFlutterTexture =
(TextureRegistry.SurfaceTextureEntry)
TestUtils.getPrivateField(cameraSpy, "flutterTexture");
ResolutionFeature resolutionFeature =
(ResolutionFeature)
TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature");

when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture);
when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
doNothing().when(cameraSpy).prepareRecording(mockResult);

cameraSpy.startVideoRecording(mockResult, null);
verify(mockMediaRecorder, times(1))
.getSurface(); // stream pulled from media recorder's surface.
verify(
mockPictureImageReader,
times(2)) // we expect one call to start the capture, one to create the capture session.
.getSurface(); // stream pulled from image streaming imageReader's surface.
}

@Test
public void setFocusMode_shouldLockAutoFocusForLockedMode() throws CameraAccessException {
camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked);
Expand Down Expand Up @@ -1013,6 +1094,45 @@ public void createCaptureSession_doesNotCloseCaptureSession() throws CameraAcces
verify(mockCaptureSession, never()).close();
}

@Test
public void createCaptureSession_shouldNotAddPictureImageSurfaceToPreviewRequest()
throws CameraAccessException {
Surface mockSurface = mock(Surface.class);
Surface mockSecondarySurface = mock(Surface.class);
SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class);
ResolutionFeature mockResolutionFeature = mock(ResolutionFeature.class);
Size mockSize = mock(Size.class);
ArrayList<CaptureRequest.Builder> mockRequestBuilders = new ArrayList<>();
mockRequestBuilders.add(mock(CaptureRequest.Builder.class));
CameraDeviceWrapper fakeCamera = spy(new FakeCameraDeviceWrapper(mockRequestBuilders));
ImageReader mockPictureImageReader = mock(ImageReader.class);
TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera);
TestUtils.setPrivateField(camera, "pictureImageReader", mockPictureImageReader);
CaptureRequest.Builder mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class);

TextureRegistry.SurfaceTextureEntry cameraFlutterTexture =
(TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture");
CameraFeatures cameraFeatures =
(CameraFeatures) TestUtils.getPrivateField(camera, "cameraFeatures");
ResolutionFeature resolutionFeature =
(ResolutionFeature)
TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature");

when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture);
when(resolutionFeature.getPreviewSize()).thenReturn(mockSize);
when(fakeCamera.createCaptureRequest(anyInt())).thenReturn(mockPreviewRequestBuilder);
when(mockPictureImageReader.getSurface()).thenReturn(mockSurface);

// Test with preview template.
camera.createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, mockSurface, mockSecondarySurface);
verify(mockPreviewRequestBuilder, times(0)).addTarget(mockSurface);

// Test with non-preview template.
camera.createCaptureSession(CameraDevice.TEMPLATE_RECORD, mockSurface, mockSecondarySurface);
verify(mockPreviewRequestBuilder, times(0)).addTarget(mockSurface);
verify(mockPreviewRequestBuilder).addTarget(mockSecondarySurface);
}

@Test
public void close_doesCloseCaptureSessionWhenCameraDeviceNull() {
camera.close();
Expand Down
2 changes: 1 addition & 1 deletion packages/camera/camera_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Android implementation of the camera plugin.
repository: https://github.com/flutter/packages/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.8+2
version: 0.10.8+3

environment:
sdk: ">=2.18.0 <4.0.0"
Expand Down