@@ -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
0 commit comments