From d52b59b4cb54f1cca66e2e58cd855fa10d074a0c Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Wed, 16 Jul 2025 19:11:09 +0200 Subject: [PATCH 1/5] Migrate setVideoFormat, stopVideoRecording, stopImageStream, and deinit to Swift --- .../camera/camera_avfoundation/CHANGELOG.md | 5 ++ .../camera_avfoundation/DefaultCamera.swift | 52 +++++++++++++++++++ .../Sources/camera_avfoundation_objc/FLTCam.m | 51 ------------------ .../include/camera_avfoundation/FLTCam.h | 6 +-- .../camera/camera_avfoundation/pubspec.yaml | 2 +- 5 files changed, 61 insertions(+), 55 deletions(-) diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 9ade0f3942f..f9f426c152b 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.20+4 + +* Migrates `setVideoFormat`,`stopVideoRecording`, and `stopImageStream` methods to Swift. +* Migrates stopping accelerometer updates to Swift. + ## 0.9.20+3 * Migrates `setZoomLevel` and `setFlashMode` methods to Swift. diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 0ea612ce31d..5bdcf9f870d 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -10,6 +10,14 @@ import CoreMotion #endif final class DefaultCamera: FLTCam, Camera { + override var videoFormat: FourCharCode { + didSet { + captureVideoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: videoFormat) + ] + } + } + override var deviceOrientation: UIDeviceOrientation { get { super.deviceOrientation } set { @@ -96,6 +104,37 @@ final class DefaultCamera: FLTCam, Camera { isRecordingPaused = false } + func stopVideoRecording(completion: @escaping (String?, FlutterError?) -> Void) { + if isRecording { + isRecording = false + + // When `isRecording` is true `startWriting` was already called so `videoWriter.status` + // is always either `.writing` or `.failed` and `finishWriting` does not throw exceptions so + // there is no need to check `videoWriter.status` + videoWriter?.finishWriting { + if self.videoWriter?.status == .completed { + self.updateOrientation() + completion(self.videoRecordingPath, nil) + self.videoRecordingPath = nil + } else { + completion( + nil, + FlutterError( + code: "IOError", + message: "AVAssetWriter could not finish writing!", + details: nil)) + } + } + } else { + let error = NSError( + domain: NSCocoaErrorDomain, + code: URLError.resourceUnavailable.rawValue, + userInfo: [NSLocalizedDescriptionKey: "Video is not recording!"] + ) + completion(nil, DefaultCamera.flutterErrorFromNSError(error)) + } + } + func lockCaptureOrientation(_ pigeonOrientation: FCPPlatformDeviceOrientation) { let orientation = FCPGetUIDeviceOrientationForPigeonDeviceOrientation(pigeonOrientation) if lockedCaptureOrientation != orientation { @@ -341,6 +380,15 @@ final class DefaultCamera: FLTCam, Camera { isPreviewPaused = false } + func stopImageStream() { + if isStreamingImages { + isStreamingImages = false + imageStreamHandler = nil + } else { + reportErrorMessage("Images from camera are not streaming!") + } + } + func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, @@ -591,4 +639,8 @@ final class DefaultCamera: FLTCam, Camera { } } } + + deinit { + motionManager.stopAccelerometerUpdates() + } } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m index e78375ccbad..caf31d39657 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m @@ -5,7 +5,6 @@ #import "./include/camera_avfoundation/FLTCam.h" #import "./include/camera_avfoundation/FLTCam_Test.h" -@import CoreMotion; @import Flutter; #import @@ -39,10 +38,8 @@ @interface FLTCam () *assetWriterPixelBufferAdaptor; @property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput; @property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput; -@property(strong, nonatomic) NSString *videoRecordingPath; @property(assign, nonatomic) BOOL isAudioSetup; -@property(nonatomic) CMMotionManager *motionManager; /// All FLTCam's state access and capture session related operations should be on run on this queue. @property(strong, nonatomic) dispatch_queue_t captureSessionQueue; /// The queue on which captured photos (not videos) are written to disk. @@ -193,12 +190,6 @@ - (AVCaptureConnection *)createConnection:(NSError **)error { return connection; } -- (void)setVideoFormat:(OSType)videoFormat { - _videoFormat = videoFormat; - _captureVideoOutput.videoSettings = - @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)}; -} - - (void)updateOrientation { if (_isRecording) { return; @@ -426,10 +417,6 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset return bestFormat; } -- (void)dealloc { - [_motionManager stopAccelerometerUpdates]; -} - /// Main logic to setup the video recording. - (void)setUpVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion { NSError *error; @@ -481,35 +468,6 @@ - (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))com } } -- (void)stopVideoRecordingWithCompletion:(void (^)(NSString *_Nullable, - FlutterError *_Nullable))completion { - if (_isRecording) { - _isRecording = NO; - - // when _isRecording is YES startWriting was already called so _videoWriter.status - // is always either AVAssetWriterStatusWriting or AVAssetWriterStatusFailed and - // finishWritingWithCompletionHandler does not throw exception so there is no need - // to check _videoWriter.status - [_videoWriter finishWritingWithCompletionHandler:^{ - if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { - [self updateOrientation]; - completion(self->_videoRecordingPath, nil); - self->_videoRecordingPath = nil; - } else { - completion(nil, [FlutterError errorWithCode:@"IOError" - message:@"AVAssetWriter could not finish writing!" - details:nil]); - } - }]; - } else { - NSError *error = - [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorResourceUnavailable - userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; - completion(nil, FlutterErrorFromNSError(error)); - } -} - - (void)setDescriptionWhileRecording:(NSString *)cameraName withCompletion:(void (^)(FlutterError *_Nullable))completion { if (!_isRecording) { @@ -612,15 +570,6 @@ - (void)startImageStreamWithMessenger:(NSObject *)messen } } -- (void)stopImageStream { - if (_isStreamingImages) { - _isStreamingImages = NO; - _imageStreamHandler = nil; - } else { - [self reportErrorMessage:@"Images from camera are not streaming!"]; - } -} - - (BOOL)setupWriterForPath:(NSString *)path { NSError *error = nil; NSURL *outputURL; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h index a74b9951be0..9c7a01ddc95 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h @@ -5,6 +5,7 @@ @import AVFoundation; @import Foundation; @import Flutter; +@import CoreMotion; #import "CameraProperties.h" #import "FLTCamConfiguration.h" @@ -51,6 +52,8 @@ NS_ASSUME_NONNULL_BEGIN @property(assign, nonatomic) UIDeviceOrientation lockedCaptureOrientation; @property(assign, nonatomic) UIDeviceOrientation deviceOrientation; @property(assign, nonatomic) FCPPlatformFlashMode flashMode; +@property(nonatomic) CMMotionManager *motionManager; +@property(strong, nonatomic, nullable) NSString *videoRecordingPath; /// Initializes an `FLTCam` instance with the given configuration. /// @param error report to the caller if any error happened creating the camera. @@ -65,15 +68,12 @@ NS_ASSUME_NONNULL_BEGIN /// @param messenger Nullable messenger for capturing each frame. - (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion messengerForStreaming:(nullable NSObject *)messenger; -- (void)stopVideoRecordingWithCompletion:(void (^)(NSString *_Nullable, - FlutterError *_Nullable))completion; - (void)setDescriptionWhileRecording:(NSString *)cameraName withCompletion:(void (^)(FlutterError *_Nullable))completion; - (void)startImageStreamWithMessenger:(NSObject *)messenger completion:(nonnull void (^)(FlutterError *_Nullable))completion; -- (void)stopImageStream; - (void)setUpCaptureSessionForAudioIfNeeded; // Methods exposed for the Swift DefaultCamera subclass diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index efeeb8dfc36..d6eaa01e673 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.20+3 +version: 0.9.20+4 environment: sdk: ^3.6.0 From 2881a7a0c4cef462d833336c2c19388550c6e1f1 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Wed, 16 Jul 2025 19:13:15 +0200 Subject: [PATCH 2/5] Migrate setDescriptionWhileRecording and add createConnection to Swift --- .../camera/camera_avfoundation/CHANGELOG.md | 2 + .../camera_avfoundation/DefaultCamera.swift | 107 ++++++++++++++++++ .../Sources/camera_avfoundation_objc/FLTCam.m | 60 ---------- .../include/camera_avfoundation/FLTCam.h | 11 +- 4 files changed, 116 insertions(+), 64 deletions(-) diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index f9f426c152b..bed72dc89ab 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -2,6 +2,8 @@ * Migrates `setVideoFormat`,`stopVideoRecording`, and `stopImageStream` methods to Swift. * Migrates stopping accelerometer updates to Swift. +* Migrates `setDescriptionWhileRecording` method to Swift. +* Adds `createConnection` method implementation to Swift. ## 0.9.20+3 diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 5bdcf9f870d..b2e6eda6854 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -60,6 +60,34 @@ final class DefaultCamera: FLTCam, Camera { details: error.domain) } + private static func createConnection( + captureDevice: FLTCaptureDevice, + videoFormat: FourCharCode, + captureDeviceInputFactory: FLTCaptureDeviceInputFactory + ) throws -> (FLTCaptureInput, FLTCaptureVideoDataOutput, AVCaptureConnection) { + // Setup video capture input. + let captureVideoInput = try captureDeviceInputFactory.deviceInput(with: captureDevice) + + // Setup video capture output. + let captureVideoOutput = FLTDefaultCaptureVideoDataOutput( + captureVideoOutput: AVCaptureVideoDataOutput()) + captureVideoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: videoFormat as Any + ] + captureVideoOutput.alwaysDiscardsLateVideoFrames = true + + // Setup video capture connection. + let connection = AVCaptureConnection( + inputPorts: captureVideoInput.ports, + output: captureVideoOutput.avOutput) + + if captureDevice.position == .front { + connection.isVideoMirrored = true + } + + return (captureVideoInput, captureVideoOutput, connection) + } + func reportInitializationState() { // Get all the state on the current thread, not the main thread. let state = FCPPlatformCameraState.make( @@ -380,6 +408,85 @@ final class DefaultCamera: FLTCam, Camera { isPreviewPaused = false } + func setDescriptionWhileRecording( + _ cameraName: String, withCompletion completion: @escaping (FlutterError?) -> Void + ) { + guard isRecording else { + completion( + FlutterError( + code: "setDescriptionWhileRecordingFailed", + message: "Device was not recording", + details: nil)) + return + } + + captureDevice = captureDeviceFactory(cameraName) + + let oldConnection = captureVideoOutput.connection(withMediaType: .video) + + // Stop video capture from the old output. + captureVideoOutput.setSampleBufferDelegate(nil, queue: nil) + + // Remove the old video capture connections. + videoCaptureSession.beginConfiguration() + videoCaptureSession.removeInput(captureVideoInput) + videoCaptureSession.removeOutput(captureVideoOutput.avOutput) + + let newConnection: AVCaptureConnection + + do { + (captureVideoInput, captureVideoOutput, newConnection) = try DefaultCamera.createConnection( + captureDevice: captureDevice, + videoFormat: videoFormat, + captureDeviceInputFactory: captureDeviceInputFactory) + + captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue) + } catch { + completion( + FlutterError( + code: "VideoError", + message: "Unable to create video connection", + details: nil)) + return + } + + // Keep the same orientation the old connections had. + if let oldConnection = oldConnection, newConnection.isVideoOrientationSupported { + newConnection.videoOrientation = oldConnection.videoOrientation + } + + // Add the new connections to the session. + if !videoCaptureSession.canAddInput(captureVideoInput) { + completion( + FlutterError( + code: "VideoError", + message: "Unable to switch video input", + details: nil)) + } + videoCaptureSession.addInputWithNoConnections(captureVideoInput) + + if !videoCaptureSession.canAddOutput(captureVideoOutput.avOutput) { + completion( + FlutterError( + code: "VideoError", + message: "Unable to switch video output", + details: nil)) + } + videoCaptureSession.addOutputWithNoConnections(captureVideoOutput.avOutput) + + if !videoCaptureSession.canAddConnection(newConnection) { + completion( + FlutterError( + code: "VideoError", + message: "Unable to switch video connection", + details: nil)) + } + videoCaptureSession.addConnection(newConnection) + videoCaptureSession.commitConfiguration() + + completion(nil) + } + func stopImageStream() { if isStreamingImages { isStreamingImages = false diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m index caf31d39657..27dbfdddc64 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m @@ -32,7 +32,6 @@ @interface FLTCam () *captureVideoInput; @property(readonly, nonatomic) CGSize captureSize; @property(strong, nonatomic) NSObject *assetWriterPixelBufferAdaptor; @@ -40,8 +39,6 @@ @interface FLTCam () *captureDeviceInputFactory; @property(nonatomic, copy) AssetWriterFactory assetWriterFactory; @property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory; /// Reports the given error message to the Dart side of the plugin. @@ -468,61 +463,6 @@ - (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))com } } -- (void)setDescriptionWhileRecording:(NSString *)cameraName - withCompletion:(void (^)(FlutterError *_Nullable))completion { - if (!_isRecording) { - completion([FlutterError errorWithCode:@"setDescriptionWhileRecordingFailed" - message:@"Device was not recording" - details:nil]); - return; - } - - _captureDevice = self.captureDeviceFactory(cameraName); - - NSObject *oldConnection = - [_captureVideoOutput connectionWithMediaType:AVMediaTypeVideo]; - - // Stop video capture from the old output. - [_captureVideoOutput setSampleBufferDelegate:nil queue:nil]; - - // Remove the old video capture connections. - [_videoCaptureSession beginConfiguration]; - [_videoCaptureSession removeInput:_captureVideoInput]; - [_videoCaptureSession removeOutput:_captureVideoOutput.avOutput]; - - NSError *error = nil; - AVCaptureConnection *newConnection = [self createConnection:&error]; - if (error) { - completion(FlutterErrorFromNSError(error)); - return; - } - - // Keep the same orientation the old connections had. - if (oldConnection && newConnection.isVideoOrientationSupported) { - newConnection.videoOrientation = oldConnection.videoOrientation; - } - - // Add the new connections to the session. - if (![_videoCaptureSession canAddInput:_captureVideoInput]) - completion([FlutterError errorWithCode:@"VideoError" - message:@"Unable switch video input" - details:nil]); - [_videoCaptureSession addInputWithNoConnections:_captureVideoInput]; - if (![_videoCaptureSession canAddOutput:_captureVideoOutput.avOutput]) - completion([FlutterError errorWithCode:@"VideoError" - message:@"Unable switch video output" - details:nil]); - [_videoCaptureSession addOutputWithNoConnections:_captureVideoOutput.avOutput]; - if (![_videoCaptureSession canAddConnection:newConnection]) - completion([FlutterError errorWithCode:@"VideoError" - message:@"Unable switch video connection" - details:nil]); - [_videoCaptureSession addConnection:newConnection]; - [_videoCaptureSession commitConfiguration]; - - completion(nil); -} - - (void)startImageStreamWithMessenger:(NSObject *)messenger completion:(void (^)(FlutterError *))completion { [self startImageStreamWithMessenger:messenger diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h index 9c7a01ddc95..c6465affe85 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h @@ -20,7 +20,8 @@ NS_ASSUME_NONNULL_BEGIN /// A class that manages camera's state and performs camera operations. @interface FLTCam : NSObject -@property(readonly, nonatomic) NSObject *captureDevice; +// captureDevice is assignable for the Swift DefaultCamera subclass +@property(strong, nonatomic) NSObject *captureDevice; @property(readonly, nonatomic) CGSize previewSize; @property(assign, nonatomic) BOOL isPreviewPaused; @property(nonatomic, copy, nullable) void (^onFrameAvailable)(void); @@ -54,6 +55,11 @@ NS_ASSUME_NONNULL_BEGIN @property(assign, nonatomic) FCPPlatformFlashMode flashMode; @property(nonatomic) CMMotionManager *motionManager; @property(strong, nonatomic, nullable) NSString *videoRecordingPath; +@property(nonatomic, copy) CaptureDeviceFactory captureDeviceFactory; +@property(strong, nonatomic) NSObject *captureVideoInput; +@property(readonly, nonatomic) NSObject *captureDeviceInputFactory; +/// All FLTCam's state access and capture session related operations should be on run on this queue. +@property(strong, nonatomic) dispatch_queue_t captureSessionQueue; /// Initializes an `FLTCam` instance with the given configuration. /// @param error report to the caller if any error happened creating the camera. @@ -69,9 +75,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion messengerForStreaming:(nullable NSObject *)messenger; -- (void)setDescriptionWhileRecording:(NSString *)cameraName - withCompletion:(void (^)(FlutterError *_Nullable))completion; - - (void)startImageStreamWithMessenger:(NSObject *)messenger completion:(nonnull void (^)(FlutterError *_Nullable))completion; - (void)setUpCaptureSessionForAudioIfNeeded; From 0b84234668c3361fa99e779100a88a608d8a1626 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 18 Jul 2025 09:20:33 +0200 Subject: [PATCH 3/5] Remove unnecessary videoFormat casting --- .../Sources/camera_avfoundation/DefaultCamera.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index b2e6eda6854..05a95d2730b 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -13,7 +13,7 @@ final class DefaultCamera: FLTCam, Camera { override var videoFormat: FourCharCode { didSet { captureVideoOutput.videoSettings = [ - kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: videoFormat) + kCVPixelBufferPixelFormatTypeKey as String: videoFormat ] } } @@ -72,7 +72,7 @@ final class DefaultCamera: FLTCam, Camera { let captureVideoOutput = FLTDefaultCaptureVideoDataOutput( captureVideoOutput: AVCaptureVideoDataOutput()) captureVideoOutput.videoSettings = [ - kCVPixelBufferPixelFormatTypeKey as String: videoFormat as Any + kCVPixelBufferPixelFormatTypeKey as String: videoFormat ] captureVideoOutput.alwaysDiscardsLateVideoFrames = true From 379872195556c01fa6dfe0651e7a6951143aff54 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 18 Jul 2025 09:24:34 +0200 Subject: [PATCH 4/5] Return early in stopVideoRecording --- .../camera_avfoundation/DefaultCamera.swift | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 05a95d2730b..f74fb1c8b42 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -133,33 +133,34 @@ final class DefaultCamera: FLTCam, Camera { } func stopVideoRecording(completion: @escaping (String?, FlutterError?) -> Void) { - if isRecording { - isRecording = false - - // When `isRecording` is true `startWriting` was already called so `videoWriter.status` - // is always either `.writing` or `.failed` and `finishWriting` does not throw exceptions so - // there is no need to check `videoWriter.status` - videoWriter?.finishWriting { - if self.videoWriter?.status == .completed { - self.updateOrientation() - completion(self.videoRecordingPath, nil) - self.videoRecordingPath = nil - } else { - completion( - nil, - FlutterError( - code: "IOError", - message: "AVAssetWriter could not finish writing!", - details: nil)) - } - } - } else { + guard isRecording else { let error = NSError( domain: NSCocoaErrorDomain, code: URLError.resourceUnavailable.rawValue, userInfo: [NSLocalizedDescriptionKey: "Video is not recording!"] ) completion(nil, DefaultCamera.flutterErrorFromNSError(error)) + return + } + + isRecording = false + + // When `isRecording` is true `startWriting` was already called so `videoWriter.status` + // is always either `.writing` or `.failed` and `finishWriting` does not throw exceptions so + // there is no need to check `videoWriter.status` + videoWriter?.finishWriting { + if self.videoWriter?.status == .completed { + self.updateOrientation() + completion(self.videoRecordingPath, nil) + self.videoRecordingPath = nil + } else { + completion( + nil, + FlutterError( + code: "IOError", + message: "AVAssetWriter could not finish writing!", + details: nil)) + } } } From c39345a93f473750229a3471fb7c73d00744966b Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 18 Jul 2025 09:30:31 +0200 Subject: [PATCH 5/5] Use weak self in finishWriting callback --- .../Sources/camera_avfoundation/DefaultCamera.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index f74fb1c8b42..31f1b087b82 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -148,11 +148,13 @@ final class DefaultCamera: FLTCam, Camera { // When `isRecording` is true `startWriting` was already called so `videoWriter.status` // is always either `.writing` or `.failed` and `finishWriting` does not throw exceptions so // there is no need to check `videoWriter.status` - videoWriter?.finishWriting { - if self.videoWriter?.status == .completed { - self.updateOrientation() - completion(self.videoRecordingPath, nil) - self.videoRecordingPath = nil + videoWriter?.finishWriting { [weak self] in + guard let strongSelf = self else { return } + + if strongSelf.videoWriter?.status == .completed { + strongSelf.updateOrientation() + completion(strongSelf.videoRecordingPath, nil) + strongSelf.videoRecordingPath = nil } else { completion( nil,