Skip to content

Commit 45b9517

Browse files
BradenBagbynploi
authored andcommitted
[camera] Reland android flip/change camera while recording (flutter#3460)
[camera] Reland android flip/change camera while recording
1 parent 14ef88f commit 45b9517

File tree

12 files changed

+780
-47
lines changed

12 files changed

+780
-47
lines changed

packages/camera/camera_android/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 0.10.5
2+
3+
* Allows camera to be switched while video recording.
14
## 0.10.4+3
25

36
* Clarifies explanation of endorsement in README.

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

Lines changed: 151 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,17 @@ 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 (Exception e) {
334+
Log.i(TAG, "open | onOpened error: " + e.getMessage());
314335
dartMessenger.sendCameraErrorEvent(e.getMessage());
315336
close();
316337
}
@@ -320,7 +341,8 @@ public void onOpened(@NonNull CameraDevice device) {
320341
public void onClosed(@NonNull CameraDevice camera) {
321342
Log.i(TAG, "open | onClosed");
322343

323-
// Prevents calls to methods that would otherwise result in IllegalStateException exceptions.
344+
// Prevents calls to methods that would otherwise result in IllegalStateException
345+
// exceptions.
324346
cameraDevice = null;
325347
closeCaptureSession();
326348
dartMessenger.sendCameraClosingEvent();
@@ -735,7 +757,7 @@ public void startVideoRecording(
735757
if (imageStreamChannel != null) {
736758
setStreamHandler(imageStreamChannel);
737759
}
738-
760+
initialCameraFacing = cameraProperties.getLensFacing();
739761
recordingVideo = true;
740762
try {
741763
startCapture(true, imageStreamChannel != null);
@@ -747,6 +769,13 @@ public void startVideoRecording(
747769
}
748770
}
749771

772+
private void closeRenderer() {
773+
if (videoRenderer != null) {
774+
videoRenderer.close();
775+
videoRenderer = null;
776+
}
777+
}
778+
750779
public void stopVideoRecording(@NonNull final Result result) {
751780
if (!recordingVideo) {
752781
result.success(null);
@@ -757,6 +786,7 @@ public void stopVideoRecording(@NonNull final Result result) {
757786
cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false));
758787
recordingVideo = false;
759788
try {
789+
closeRenderer();
760790
captureSession.abortCaptures();
761791
mediaRecorder.stop();
762792
} catch (CameraAccessException | IllegalStateException e) {
@@ -765,7 +795,7 @@ public void stopVideoRecording(@NonNull final Result result) {
765795
mediaRecorder.reset();
766796
try {
767797
startPreview();
768-
} catch (CameraAccessException | IllegalStateException e) {
798+
} catch (CameraAccessException | IllegalStateException | InterruptedException e) {
769799
result.error("videoRecordingFailed", e.getMessage(), null);
770800
return;
771801
}
@@ -1049,13 +1079,50 @@ public void resumePreview() {
10491079
null, (code, message) -> dartMessenger.sendCameraErrorEvent(message));
10501080
}
10511081

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

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

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-
}
1249+
stopAndReleaseCamera();
11931250

11941251
if (pictureImageReader != null) {
11951252
pictureImageReader.close();
@@ -1208,6 +1265,75 @@ public void close() {
12081265
stopBackgroundThread();
12091266
}
12101267

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

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)