Skip to content

Commit ec0ff4d

Browse files
[camera_avfoundation] Implementation swift migration - part 8 (#9635)
Migrates camera implementation as part of flutter/flutter#119109 This PR migrates the 5th chunk of `FLTCam` class to Swift: * `setVideoFormat` * `stopVideoRecording` * `stopImageStream` * stopping accelerometer updates (`deinit`) * `setDescriptionWhileRecording` * `createConnection` (adds Swift implementation, FLTCam still depends on private non-static implementation) NOTE: `setDescriptionWhileRecording` is migrated close to verbatim, the [issue](flutter/flutter#168134) affecting it remains. Some properties of the `FLTCam` have to be temporarily made public so that they are accessible in `DefaultCamera`. ## Pre-Review Checklist [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 65b4fba commit ec0ff4d

File tree

5 files changed

+180
-119
lines changed

5 files changed

+180
-119
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.9.20+4
2+
3+
* Migrates `setVideoFormat`,`stopVideoRecording`, and `stopImageStream` methods to Swift.
4+
* Migrates stopping accelerometer updates to Swift.
5+
* Migrates `setDescriptionWhileRecording` method to Swift.
6+
* Adds `createConnection` method implementation to Swift.
7+
18
## 0.9.20+3
29

310
* Migrates `setZoomLevel` and `setFlashMode` methods to Swift.

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import CoreMotion
1010
#endif
1111

1212
final class DefaultCamera: FLTCam, Camera {
13+
override var videoFormat: FourCharCode {
14+
didSet {
15+
captureVideoOutput.videoSettings = [
16+
kCVPixelBufferPixelFormatTypeKey as String: videoFormat
17+
]
18+
}
19+
}
20+
1321
override var deviceOrientation: UIDeviceOrientation {
1422
get { super.deviceOrientation }
1523
set {
@@ -52,6 +60,34 @@ final class DefaultCamera: FLTCam, Camera {
5260
details: error.domain)
5361
}
5462

63+
private static func createConnection(
64+
captureDevice: FLTCaptureDevice,
65+
videoFormat: FourCharCode,
66+
captureDeviceInputFactory: FLTCaptureDeviceInputFactory
67+
) throws -> (FLTCaptureInput, FLTCaptureVideoDataOutput, AVCaptureConnection) {
68+
// Setup video capture input.
69+
let captureVideoInput = try captureDeviceInputFactory.deviceInput(with: captureDevice)
70+
71+
// Setup video capture output.
72+
let captureVideoOutput = FLTDefaultCaptureVideoDataOutput(
73+
captureVideoOutput: AVCaptureVideoDataOutput())
74+
captureVideoOutput.videoSettings = [
75+
kCVPixelBufferPixelFormatTypeKey as String: videoFormat
76+
]
77+
captureVideoOutput.alwaysDiscardsLateVideoFrames = true
78+
79+
// Setup video capture connection.
80+
let connection = AVCaptureConnection(
81+
inputPorts: captureVideoInput.ports,
82+
output: captureVideoOutput.avOutput)
83+
84+
if captureDevice.position == .front {
85+
connection.isVideoMirrored = true
86+
}
87+
88+
return (captureVideoInput, captureVideoOutput, connection)
89+
}
90+
5591
func reportInitializationState() {
5692
// Get all the state on the current thread, not the main thread.
5793
let state = FCPPlatformCameraState.make(
@@ -96,6 +132,40 @@ final class DefaultCamera: FLTCam, Camera {
96132
isRecordingPaused = false
97133
}
98134

135+
func stopVideoRecording(completion: @escaping (String?, FlutterError?) -> Void) {
136+
guard isRecording else {
137+
let error = NSError(
138+
domain: NSCocoaErrorDomain,
139+
code: URLError.resourceUnavailable.rawValue,
140+
userInfo: [NSLocalizedDescriptionKey: "Video is not recording!"]
141+
)
142+
completion(nil, DefaultCamera.flutterErrorFromNSError(error))
143+
return
144+
}
145+
146+
isRecording = false
147+
148+
// When `isRecording` is true `startWriting` was already called so `videoWriter.status`
149+
// is always either `.writing` or `.failed` and `finishWriting` does not throw exceptions so
150+
// there is no need to check `videoWriter.status`
151+
videoWriter?.finishWriting { [weak self] in
152+
guard let strongSelf = self else { return }
153+
154+
if strongSelf.videoWriter?.status == .completed {
155+
strongSelf.updateOrientation()
156+
completion(strongSelf.videoRecordingPath, nil)
157+
strongSelf.videoRecordingPath = nil
158+
} else {
159+
completion(
160+
nil,
161+
FlutterError(
162+
code: "IOError",
163+
message: "AVAssetWriter could not finish writing!",
164+
details: nil))
165+
}
166+
}
167+
}
168+
99169
func lockCaptureOrientation(_ pigeonOrientation: FCPPlatformDeviceOrientation) {
100170
let orientation = FCPGetUIDeviceOrientationForPigeonDeviceOrientation(pigeonOrientation)
101171
if lockedCaptureOrientation != orientation {
@@ -341,6 +411,94 @@ final class DefaultCamera: FLTCam, Camera {
341411
isPreviewPaused = false
342412
}
343413

414+
func setDescriptionWhileRecording(
415+
_ cameraName: String, withCompletion completion: @escaping (FlutterError?) -> Void
416+
) {
417+
guard isRecording else {
418+
completion(
419+
FlutterError(
420+
code: "setDescriptionWhileRecordingFailed",
421+
message: "Device was not recording",
422+
details: nil))
423+
return
424+
}
425+
426+
captureDevice = captureDeviceFactory(cameraName)
427+
428+
let oldConnection = captureVideoOutput.connection(withMediaType: .video)
429+
430+
// Stop video capture from the old output.
431+
captureVideoOutput.setSampleBufferDelegate(nil, queue: nil)
432+
433+
// Remove the old video capture connections.
434+
videoCaptureSession.beginConfiguration()
435+
videoCaptureSession.removeInput(captureVideoInput)
436+
videoCaptureSession.removeOutput(captureVideoOutput.avOutput)
437+
438+
let newConnection: AVCaptureConnection
439+
440+
do {
441+
(captureVideoInput, captureVideoOutput, newConnection) = try DefaultCamera.createConnection(
442+
captureDevice: captureDevice,
443+
videoFormat: videoFormat,
444+
captureDeviceInputFactory: captureDeviceInputFactory)
445+
446+
captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue)
447+
} catch {
448+
completion(
449+
FlutterError(
450+
code: "VideoError",
451+
message: "Unable to create video connection",
452+
details: nil))
453+
return
454+
}
455+
456+
// Keep the same orientation the old connections had.
457+
if let oldConnection = oldConnection, newConnection.isVideoOrientationSupported {
458+
newConnection.videoOrientation = oldConnection.videoOrientation
459+
}
460+
461+
// Add the new connections to the session.
462+
if !videoCaptureSession.canAddInput(captureVideoInput) {
463+
completion(
464+
FlutterError(
465+
code: "VideoError",
466+
message: "Unable to switch video input",
467+
details: nil))
468+
}
469+
videoCaptureSession.addInputWithNoConnections(captureVideoInput)
470+
471+
if !videoCaptureSession.canAddOutput(captureVideoOutput.avOutput) {
472+
completion(
473+
FlutterError(
474+
code: "VideoError",
475+
message: "Unable to switch video output",
476+
details: nil))
477+
}
478+
videoCaptureSession.addOutputWithNoConnections(captureVideoOutput.avOutput)
479+
480+
if !videoCaptureSession.canAddConnection(newConnection) {
481+
completion(
482+
FlutterError(
483+
code: "VideoError",
484+
message: "Unable to switch video connection",
485+
details: nil))
486+
}
487+
videoCaptureSession.addConnection(newConnection)
488+
videoCaptureSession.commitConfiguration()
489+
490+
completion(nil)
491+
}
492+
493+
func stopImageStream() {
494+
if isStreamingImages {
495+
isStreamingImages = false
496+
imageStreamHandler = nil
497+
} else {
498+
reportErrorMessage("Images from camera are not streaming!")
499+
}
500+
}
501+
344502
func captureOutput(
345503
_ output: AVCaptureOutput,
346504
didOutput sampleBuffer: CMSampleBuffer,
@@ -591,4 +749,8 @@ final class DefaultCamera: FLTCam, Camera {
591749
}
592750
}
593751
}
752+
753+
deinit {
754+
motionManager.stopAccelerometerUpdates()
755+
}
594756
}

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m

Lines changed: 0 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
#import "./include/camera_avfoundation/FLTCam.h"
66
#import "./include/camera_avfoundation/FLTCam_Test.h"
77

8-
@import CoreMotion;
98
@import Flutter;
109
#import <libkern/OSAtomic.h>
1110

@@ -33,28 +32,21 @@ @interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate,
3332
@property(readonly, nonatomic) FCPPlatformMediaSettings *mediaSettings;
3433
@property(readonly, nonatomic) FLTCamMediaSettingsAVWrapper *mediaSettingsAVWrapper;
3534

36-
@property(readonly, nonatomic) NSObject<FLTCaptureInput> *captureVideoInput;
3735
@property(readonly, nonatomic) CGSize captureSize;
3836
@property(strong, nonatomic)
3937
NSObject<FLTAssetWriterInputPixelBufferAdaptor> *assetWriterPixelBufferAdaptor;
4038
@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
4139
@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput;
42-
@property(strong, nonatomic) NSString *videoRecordingPath;
4340
@property(assign, nonatomic) BOOL isAudioSetup;
4441

45-
@property(nonatomic) CMMotionManager *motionManager;
46-
/// All FLTCam's state access and capture session related operations should be on run on this queue.
47-
@property(strong, nonatomic) dispatch_queue_t captureSessionQueue;
4842
/// The queue on which captured photos (not videos) are written to disk.
4943
/// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation.
5044
@property(strong, nonatomic) dispatch_queue_t photoIOQueue;
5145
/// A wrapper for CMVideoFormatDescriptionGetDimensions.
5246
/// Allows for alternate implementations in tests.
5347
@property(nonatomic, copy) VideoDimensionsForFormat videoDimensionsForFormat;
5448
/// A wrapper for AVCaptureDevice creation to allow for dependency injection in tests.
55-
@property(nonatomic, copy) CaptureDeviceFactory captureDeviceFactory;
5649
@property(nonatomic, copy) AudioCaptureDeviceFactory audioCaptureDeviceFactory;
57-
@property(readonly, nonatomic) NSObject<FLTCaptureDeviceInputFactory> *captureDeviceInputFactory;
5850
@property(nonatomic, copy) AssetWriterFactory assetWriterFactory;
5951
@property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory;
6052
/// Reports the given error message to the Dart side of the plugin.
@@ -193,12 +185,6 @@ - (AVCaptureConnection *)createConnection:(NSError **)error {
193185
return connection;
194186
}
195187

196-
- (void)setVideoFormat:(OSType)videoFormat {
197-
_videoFormat = videoFormat;
198-
_captureVideoOutput.videoSettings =
199-
@{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)};
200-
}
201-
202188
- (void)updateOrientation {
203189
if (_isRecording) {
204190
return;
@@ -426,10 +412,6 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset
426412
return bestFormat;
427413
}
428414

429-
- (void)dealloc {
430-
[_motionManager stopAccelerometerUpdates];
431-
}
432-
433415
/// Main logic to setup the video recording.
434416
- (void)setUpVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion {
435417
NSError *error;
@@ -481,90 +463,6 @@ - (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))com
481463
}
482464
}
483465

484-
- (void)stopVideoRecordingWithCompletion:(void (^)(NSString *_Nullable,
485-
FlutterError *_Nullable))completion {
486-
if (_isRecording) {
487-
_isRecording = NO;
488-
489-
// when _isRecording is YES startWriting was already called so _videoWriter.status
490-
// is always either AVAssetWriterStatusWriting or AVAssetWriterStatusFailed and
491-
// finishWritingWithCompletionHandler does not throw exception so there is no need
492-
// to check _videoWriter.status
493-
[_videoWriter finishWritingWithCompletionHandler:^{
494-
if (self->_videoWriter.status == AVAssetWriterStatusCompleted) {
495-
[self updateOrientation];
496-
completion(self->_videoRecordingPath, nil);
497-
self->_videoRecordingPath = nil;
498-
} else {
499-
completion(nil, [FlutterError errorWithCode:@"IOError"
500-
message:@"AVAssetWriter could not finish writing!"
501-
details:nil]);
502-
}
503-
}];
504-
} else {
505-
NSError *error =
506-
[NSError errorWithDomain:NSCocoaErrorDomain
507-
code:NSURLErrorResourceUnavailable
508-
userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}];
509-
completion(nil, FlutterErrorFromNSError(error));
510-
}
511-
}
512-
513-
- (void)setDescriptionWhileRecording:(NSString *)cameraName
514-
withCompletion:(void (^)(FlutterError *_Nullable))completion {
515-
if (!_isRecording) {
516-
completion([FlutterError errorWithCode:@"setDescriptionWhileRecordingFailed"
517-
message:@"Device was not recording"
518-
details:nil]);
519-
return;
520-
}
521-
522-
_captureDevice = self.captureDeviceFactory(cameraName);
523-
524-
NSObject<FLTCaptureConnection> *oldConnection =
525-
[_captureVideoOutput connectionWithMediaType:AVMediaTypeVideo];
526-
527-
// Stop video capture from the old output.
528-
[_captureVideoOutput setSampleBufferDelegate:nil queue:nil];
529-
530-
// Remove the old video capture connections.
531-
[_videoCaptureSession beginConfiguration];
532-
[_videoCaptureSession removeInput:_captureVideoInput];
533-
[_videoCaptureSession removeOutput:_captureVideoOutput.avOutput];
534-
535-
NSError *error = nil;
536-
AVCaptureConnection *newConnection = [self createConnection:&error];
537-
if (error) {
538-
completion(FlutterErrorFromNSError(error));
539-
return;
540-
}
541-
542-
// Keep the same orientation the old connections had.
543-
if (oldConnection && newConnection.isVideoOrientationSupported) {
544-
newConnection.videoOrientation = oldConnection.videoOrientation;
545-
}
546-
547-
// Add the new connections to the session.
548-
if (![_videoCaptureSession canAddInput:_captureVideoInput])
549-
completion([FlutterError errorWithCode:@"VideoError"
550-
message:@"Unable switch video input"
551-
details:nil]);
552-
[_videoCaptureSession addInputWithNoConnections:_captureVideoInput];
553-
if (![_videoCaptureSession canAddOutput:_captureVideoOutput.avOutput])
554-
completion([FlutterError errorWithCode:@"VideoError"
555-
message:@"Unable switch video output"
556-
details:nil]);
557-
[_videoCaptureSession addOutputWithNoConnections:_captureVideoOutput.avOutput];
558-
if (![_videoCaptureSession canAddConnection:newConnection])
559-
completion([FlutterError errorWithCode:@"VideoError"
560-
message:@"Unable switch video connection"
561-
details:nil]);
562-
[_videoCaptureSession addConnection:newConnection];
563-
[_videoCaptureSession commitConfiguration];
564-
565-
completion(nil);
566-
}
567-
568466
- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger
569467
completion:(void (^)(FlutterError *))completion {
570468
[self startImageStreamWithMessenger:messenger
@@ -612,15 +510,6 @@ - (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messen
612510
}
613511
}
614512

615-
- (void)stopImageStream {
616-
if (_isStreamingImages) {
617-
_isStreamingImages = NO;
618-
_imageStreamHandler = nil;
619-
} else {
620-
[self reportErrorMessage:@"Images from camera are not streaming!"];
621-
}
622-
}
623-
624513
- (BOOL)setupWriterForPath:(NSString *)path {
625514
NSError *error = nil;
626515
NSURL *outputURL;

0 commit comments

Comments
 (0)