diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 9ade0f3942f..bed72dc89ab 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.9.20+4 + +* 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 * 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..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 @@ -10,6 +10,14 @@ import CoreMotion #endif final class DefaultCamera: FLTCam, Camera { + override var videoFormat: FourCharCode { + didSet { + captureVideoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: videoFormat + ] + } + } + override var deviceOrientation: UIDeviceOrientation { get { super.deviceOrientation } set { @@ -52,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 + ] + 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( @@ -96,6 +132,40 @@ final class DefaultCamera: FLTCam, Camera { isRecordingPaused = false } + func stopVideoRecording(completion: @escaping (String?, FlutterError?) -> Void) { + 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 { [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, + FlutterError( + code: "IOError", + message: "AVAssetWriter could not finish writing!", + details: nil)) + } + } + } + func lockCaptureOrientation(_ pigeonOrientation: FCPPlatformDeviceOrientation) { let orientation = FCPGetUIDeviceOrientationForPigeonDeviceOrientation(pigeonOrientation) if lockedCaptureOrientation != orientation { @@ -341,6 +411,94 @@ 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 + imageStreamHandler = nil + } else { + reportErrorMessage("Images from camera are not streaming!") + } + } + func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, @@ -591,4 +749,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..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 @@ -5,7 +5,6 @@ #import "./include/camera_avfoundation/FLTCam.h" #import "./include/camera_avfoundation/FLTCam_Test.h" -@import CoreMotion; @import Flutter; #import @@ -33,18 +32,13 @@ @interface FLTCam () *captureVideoInput; @property(readonly, nonatomic) CGSize captureSize; @property(strong, nonatomic) NSObject *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. /// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation. @property(strong, nonatomic) dispatch_queue_t photoIOQueue; @@ -52,9 +46,7 @@ @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. @@ -193,12 +185,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 +412,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,90 +463,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) { - 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 @@ -612,15 +510,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..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 @@ -5,6 +5,7 @@ @import AVFoundation; @import Foundation; @import Flutter; +@import CoreMotion; #import "CameraProperties.h" #import "FLTCamConfiguration.h" @@ -19,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); @@ -51,6 +53,13 @@ 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; +@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. @@ -65,15 +74,9 @@ 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