Skip to content

Commit d311478

Browse files
authored
[camera] Reland implementations of flip/change camera while recording (#3272)
[camera] Reland implementations of flip/change camera while recording
1 parent 843bd8c commit d311478

File tree

27 files changed

+1042
-128
lines changed

27 files changed

+1042
-128
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.5
2+
3+
* Allows camera to be switched while video recording.
4+
15
## 0.10.4+2
26

37
* Aligns Dart and Flutter SDK constraints.

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

Lines changed: 150 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,28 @@ class Camera
9696
* Holds all of the camera features/settings and will be used to update the request builder when
9797
* one changes.
9898
*/
99-
private final CameraFeatures cameraFeatures;
99+
private CameraFeatures cameraFeatures;
100+
101+
private String imageFormatGroup;
102+
103+
/**
104+
* Takes an input/output surface and orients the recording correctly. This is needed because
105+
* switching cameras while recording causes the wrong orientation.
106+
*/
107+
private VideoRenderer videoRenderer;
108+
109+
/**
110+
* Whether or not the camera aligns with the initial way the camera was facing if the camera was
111+
* flipped.
112+
*/
113+
private int initialCameraFacing;
100114

101115
private final SurfaceTextureEntry flutterTexture;
116+
private final ResolutionPreset resolutionPreset;
102117
private final boolean enableAudio;
103118
private final Context applicationContext;
104119
private final DartMessenger dartMessenger;
105-
private final CameraProperties cameraProperties;
120+
private CameraProperties cameraProperties;
106121
private final CameraFeatureFactory cameraFeatureFactory;
107122
private final Activity activity;
108123
/** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */
@@ -192,6 +207,7 @@ public Camera(
192207
this.applicationContext = activity.getApplicationContext();
193208
this.cameraProperties = cameraProperties;
194209
this.cameraFeatureFactory = cameraFeatureFactory;
210+
this.resolutionPreset = resolutionPreset;
195211
this.cameraFeatures =
196212
CameraFeatures.init(
197213
cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset);
@@ -232,6 +248,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
232248
if (mediaRecorder != null) {
233249
mediaRecorder.release();
234250
}
251+
closeRenderer();
235252

236253
final PlatformChannel.DeviceOrientation lockedOrientation =
237254
cameraFeatures.getSensorOrientation().getLockedCaptureOrientation();
@@ -259,6 +276,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
259276

260277
@SuppressLint("MissingPermission")
261278
public void open(String imageFormatGroup) throws CameraAccessException {
279+
this.imageFormatGroup = imageFormatGroup;
262280
final ResolutionFeature resolutionFeature = cameraFeatures.getResolution();
263281

264282
if (!resolutionFeature.checkIsSupported()) {
@@ -303,14 +321,16 @@ public void onOpened(@NonNull CameraDevice device) {
303321
cameraDevice = new DefaultCameraDeviceWrapper(device);
304322
try {
305323
startPreview();
324+
if (!recordingVideo) // only send initialization if we werent already recording and switching cameras
306325
dartMessenger.sendCameraInitializedEvent(
307-
resolutionFeature.getPreviewSize().getWidth(),
308-
resolutionFeature.getPreviewSize().getHeight(),
309-
cameraFeatures.getExposureLock().getValue(),
310-
cameraFeatures.getAutoFocus().getValue(),
311-
cameraFeatures.getExposurePoint().checkIsSupported(),
312-
cameraFeatures.getFocusPoint().checkIsSupported());
313-
} catch (CameraAccessException e) {
326+
resolutionFeature.getPreviewSize().getWidth(),
327+
resolutionFeature.getPreviewSize().getHeight(),
328+
cameraFeatures.getExposureLock().getValue(),
329+
cameraFeatures.getAutoFocus().getValue(),
330+
cameraFeatures.getExposurePoint().checkIsSupported(),
331+
cameraFeatures.getFocusPoint().checkIsSupported());
332+
333+
} catch (CameraAccessException | InterruptedException e) {
314334
dartMessenger.sendCameraErrorEvent(e.getMessage());
315335
close();
316336
}
@@ -320,7 +340,8 @@ public void onOpened(@NonNull CameraDevice device) {
320340
public void onClosed(@NonNull CameraDevice camera) {
321341
Log.i(TAG, "open | onClosed");
322342

323-
// Prevents calls to methods that would otherwise result in IllegalStateException exceptions.
343+
// Prevents calls to methods that would otherwise result in IllegalStateException
344+
// exceptions.
324345
cameraDevice = null;
325346
closeCaptureSession();
326347
dartMessenger.sendCameraClosingEvent();
@@ -735,7 +756,7 @@ public void startVideoRecording(
735756
if (imageStreamChannel != null) {
736757
setStreamHandler(imageStreamChannel);
737758
}
738-
759+
initialCameraFacing = cameraProperties.getLensFacing();
739760
recordingVideo = true;
740761
try {
741762
startCapture(true, imageStreamChannel != null);
@@ -747,6 +768,13 @@ public void startVideoRecording(
747768
}
748769
}
749770

771+
private void closeRenderer() {
772+
if (videoRenderer != null) {
773+
videoRenderer.close();
774+
videoRenderer = null;
775+
}
776+
}
777+
750778
public void stopVideoRecording(@NonNull final Result result) {
751779
if (!recordingVideo) {
752780
result.success(null);
@@ -757,6 +785,7 @@ public void stopVideoRecording(@NonNull final Result result) {
757785
cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false));
758786
recordingVideo = false;
759787
try {
788+
closeRenderer();
760789
captureSession.abortCaptures();
761790
mediaRecorder.stop();
762791
} catch (CameraAccessException | IllegalStateException e) {
@@ -765,7 +794,7 @@ public void stopVideoRecording(@NonNull final Result result) {
765794
mediaRecorder.reset();
766795
try {
767796
startPreview();
768-
} catch (CameraAccessException | IllegalStateException e) {
797+
} catch (CameraAccessException | IllegalStateException | InterruptedException e) {
769798
result.error("videoRecordingFailed", e.getMessage(), null);
770799
return;
771800
}
@@ -1049,13 +1078,50 @@ public void resumePreview() {
10491078
null, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
10501079
}
10511080

1052-
public void startPreview() throws CameraAccessException {
1081+
public void startPreview() throws CameraAccessException, InterruptedException {
1082+
// If recording is already in progress, the camera is being flipped, so send it through the VideoRenderer to keep the correct orientation.
1083+
if (recordingVideo) {
1084+
startPreviewWithVideoRendererStream();
1085+
} else {
1086+
startRegularPreview();
1087+
}
1088+
}
1089+
1090+
private void startRegularPreview() throws CameraAccessException {
10531091
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
10541092
Log.i(TAG, "startPreview");
1055-
10561093
createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface());
10571094
}
10581095

1096+
private void startPreviewWithVideoRendererStream()
1097+
throws CameraAccessException, InterruptedException {
1098+
if (videoRenderer == null) return;
1099+
1100+
// get rotation for rendered video
1101+
final PlatformChannel.DeviceOrientation lockedOrientation =
1102+
cameraFeatures.getSensorOrientation().getLockedCaptureOrientation();
1103+
DeviceOrientationManager orientationManager =
1104+
cameraFeatures.getSensorOrientation().getDeviceOrientationManager();
1105+
1106+
int rotation = 0;
1107+
if (orientationManager != null) {
1108+
rotation =
1109+
lockedOrientation == null
1110+
? orientationManager.getVideoOrientation()
1111+
: orientationManager.getVideoOrientation(lockedOrientation);
1112+
}
1113+
1114+
if (cameraProperties.getLensFacing() != initialCameraFacing) {
1115+
1116+
// If the new camera is facing the opposite way than the initial recording,
1117+
// the rotation should be flipped 180 degrees.
1118+
rotation = (rotation + 180) % 360;
1119+
}
1120+
videoRenderer.setRotation(rotation);
1121+
1122+
createCaptureSession(CameraDevice.TEMPLATE_RECORD, videoRenderer.getInputSurface());
1123+
}
1124+
10591125
public void startPreviewWithImageStream(EventChannel imageStreamChannel)
10601126
throws CameraAccessException {
10611127
setStreamHandler(imageStreamChannel);
@@ -1179,17 +1245,7 @@ private void closeCaptureSession() {
11791245
public void close() {
11801246
Log.i(TAG, "close");
11811247

1182-
if (cameraDevice != null) {
1183-
cameraDevice.close();
1184-
cameraDevice = null;
1185-
1186-
// Closing the CameraDevice without closing the CameraCaptureSession is recommended
1187-
// for quickly closing the camera:
1188-
// https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close()
1189-
captureSession = null;
1190-
} else {
1191-
closeCaptureSession();
1192-
}
1248+
stopAndReleaseCamera();
11931249

11941250
if (pictureImageReader != null) {
11951251
pictureImageReader.close();
@@ -1208,6 +1264,75 @@ public void close() {
12081264
stopBackgroundThread();
12091265
}
12101266

1267+
private void stopAndReleaseCamera() {
1268+
if (cameraDevice != null) {
1269+
cameraDevice.close();
1270+
cameraDevice = null;
1271+
1272+
// Closing the CameraDevice without closing the CameraCaptureSession is recommended
1273+
// for quickly closing the camera:
1274+
// https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close()
1275+
captureSession = null;
1276+
} else {
1277+
closeCaptureSession();
1278+
}
1279+
}
1280+
1281+
private void prepareVideoRenderer() {
1282+
if (videoRenderer != null) return;
1283+
final ResolutionFeature resolutionFeature = cameraFeatures.getResolution();
1284+
1285+
// handle videoRenderer errors
1286+
Thread.UncaughtExceptionHandler videoRendererUncaughtExceptionHandler =
1287+
new Thread.UncaughtExceptionHandler() {
1288+
@Override
1289+
public void uncaughtException(Thread thread, Throwable ex) {
1290+
dartMessenger.sendCameraErrorEvent(
1291+
"Failed to process frames after camera was flipped.");
1292+
}
1293+
};
1294+
1295+
videoRenderer =
1296+
new VideoRenderer(
1297+
mediaRecorder.getSurface(),
1298+
resolutionFeature.getCaptureSize().getWidth(),
1299+
resolutionFeature.getCaptureSize().getHeight(),
1300+
videoRendererUncaughtExceptionHandler);
1301+
}
1302+
1303+
public void setDescriptionWhileRecording(
1304+
@NonNull final Result result, CameraProperties properties) {
1305+
1306+
if (!recordingVideo) {
1307+
result.error("setDescriptionWhileRecordingFailed", "Device was not recording", null);
1308+
return;
1309+
}
1310+
1311+
// See VideoRenderer.java requires API 26 to switch camera while recording
1312+
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
1313+
result.error(
1314+
"setDescriptionWhileRecordingFailed",
1315+
"Device does not support switching the camera while recording",
1316+
null);
1317+
return;
1318+
}
1319+
1320+
stopAndReleaseCamera();
1321+
prepareVideoRenderer();
1322+
cameraProperties = properties;
1323+
cameraFeatures =
1324+
CameraFeatures.init(
1325+
cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset);
1326+
cameraFeatures.setAutoFocus(
1327+
cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true));
1328+
try {
1329+
open(imageFormatGroup);
1330+
} catch (CameraAccessException e) {
1331+
result.error("setDescriptionWhileRecordingFailed", e.getMessage(), null);
1332+
}
1333+
result.success(null);
1334+
}
1335+
12111336
public void dispose() {
12121337
Log.i(TAG, "dispose");
12131338

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,18 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
354354
result.success(null);
355355
break;
356356
}
357+
case "setDescriptionWhileRecording":
358+
{
359+
try {
360+
String cameraName = call.argument("cameraName");
361+
CameraProperties cameraProperties =
362+
new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
363+
camera.setDescriptionWhileRecording(result, cameraProperties);
364+
} catch (Exception e) {
365+
handleException(e, result);
366+
}
367+
break;
368+
}
357369
case "dispose":
358370
{
359371
if (camera != null) {

0 commit comments

Comments
 (0)