Skip to content

Commit c5e8c82

Browse files
[camera_avfoundation] Implementation swift migration - part 9 (#9645)
Migrates camera implementation as part of flutter/flutter#119109 This PR migrates the 6th chunk of `FLTCam` class to Swift: * `startVideoRecording` * `setUpVideoRecording` * `setupWriter` Some properties of the `FLTCam` have to be temporarily made public so that they are accessible in `DefaultCamera`. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^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 63646c6 commit c5e8c82

File tree

5 files changed

+161
-149
lines changed

5 files changed

+161
-149
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.20+5
2+
3+
* Migrates `startVideoRecording`, `setUpVideoRecording`, and `setupWriter` methods to Swift.
4+
15
## 0.9.20+4
26

37
* Migrates `setVideoFormat`,`stopVideoRecording`, and `stopImageStream` methods to Swift.

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

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,153 @@ final class DefaultCamera: FLTCam, Camera {
122122
audioCaptureSession.stopRunning()
123123
}
124124

125+
func startVideoRecording(
126+
completion: @escaping (FlutterError?) -> Void,
127+
messengerForStreaming messenger: FlutterBinaryMessenger?
128+
) {
129+
guard !isRecording else {
130+
completion(
131+
FlutterError(
132+
code: "Error",
133+
message: "Video is already recording",
134+
details: nil))
135+
return
136+
}
137+
138+
if let messenger = messenger {
139+
startImageStream(with: messenger) { [weak self] error in
140+
self?.setUpVideoRecording(completion: completion)
141+
}
142+
return
143+
}
144+
145+
setUpVideoRecording(completion: completion)
146+
}
147+
148+
/// Main logic to setup the video recording.
149+
private func setUpVideoRecording(completion: @escaping (FlutterError?) -> Void) {
150+
let videoRecordingPath: String
151+
do {
152+
videoRecordingPath = try getTemporaryFilePath(
153+
withExtension: "mp4",
154+
subfolder: "videos",
155+
prefix: "REC_")
156+
self.videoRecordingPath = videoRecordingPath
157+
} catch let error as NSError {
158+
completion(DefaultCamera.flutterErrorFromNSError(error))
159+
return
160+
}
161+
162+
guard setupWriter(forPath: videoRecordingPath) else {
163+
completion(
164+
FlutterError(
165+
code: "IOError",
166+
message: "Setup Writer Failed",
167+
details: nil))
168+
return
169+
}
170+
171+
// startWriting should not be called in didOutputSampleBuffer where it can cause state
172+
// in which isRecording is true but videoWriter.status is .unknown
173+
// in stopVideoRecording if it is called after startVideoRecording but before
174+
// didOutputSampleBuffer had chance to call startWriting and lag at start of video
175+
// https://github.com/flutter/flutter/issues/132016
176+
// https://github.com/flutter/flutter/issues/151319
177+
videoWriter?.startWriting()
178+
isFirstVideoSample = true
179+
isRecording = true
180+
isRecordingPaused = false
181+
videoTimeOffset = CMTime.zero
182+
audioTimeOffset = CMTime.zero
183+
videoIsDisconnected = false
184+
audioIsDisconnected = false
185+
completion(nil)
186+
}
187+
188+
private func setupWriter(forPath path: String) -> Bool {
189+
setUpCaptureSessionForAudioIfNeeded()
190+
191+
var error: NSError?
192+
videoWriter = assetWriterFactory(URL(fileURLWithPath: path), AVFileType.mp4, &error)
193+
194+
guard let videoWriter = videoWriter else {
195+
if let error = error {
196+
reportErrorMessage(error.description)
197+
}
198+
return false
199+
}
200+
201+
var videoSettings = mediaSettingsAVWrapper.recommendedVideoSettingsForAssetWriter(
202+
withFileType:
203+
AVFileType.mp4,
204+
for: captureVideoOutput
205+
)
206+
207+
if mediaSettings.videoBitrate != nil || mediaSettings.framesPerSecond != nil {
208+
var compressionProperties: [String: Any] = [:]
209+
210+
if let videoBitrate = mediaSettings.videoBitrate {
211+
compressionProperties[AVVideoAverageBitRateKey] = videoBitrate
212+
}
213+
214+
if let framesPerSecond = mediaSettings.framesPerSecond {
215+
compressionProperties[AVVideoExpectedSourceFrameRateKey] = framesPerSecond
216+
}
217+
218+
videoSettings?[AVVideoCompressionPropertiesKey] = compressionProperties
219+
}
220+
221+
let videoWriterInput = mediaSettingsAVWrapper.assetWriterVideoInput(
222+
withOutputSettings: videoSettings)
223+
self.videoWriterInput = videoWriterInput
224+
225+
let sourcePixelBufferAttributes: [String: Any] = [
226+
kCVPixelBufferPixelFormatTypeKey as String: videoFormat
227+
]
228+
229+
videoAdaptor = inputPixelBufferAdaptorFactory(videoWriterInput, sourcePixelBufferAttributes)
230+
231+
videoWriterInput.expectsMediaDataInRealTime = true
232+
233+
// Add the audio input
234+
if mediaSettings.enableAudio {
235+
var audioChannelLayout = AudioChannelLayout()
236+
audioChannelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono
237+
238+
let audioChannelLayoutData = withUnsafeBytes(of: &audioChannelLayout) { Data($0) }
239+
240+
var audioSettings: [String: Any] = [
241+
AVFormatIDKey: kAudioFormatMPEG4AAC,
242+
AVSampleRateKey: 44100.0,
243+
AVNumberOfChannelsKey: 1,
244+
AVChannelLayoutKey: audioChannelLayoutData,
245+
]
246+
247+
if let audioBitrate = mediaSettings.audioBitrate {
248+
audioSettings[AVEncoderBitRateKey] = audioBitrate
249+
}
250+
251+
let newAudioWriterInput = mediaSettingsAVWrapper.assetWriterAudioInput(
252+
withOutputSettings: audioSettings)
253+
newAudioWriterInput.expectsMediaDataInRealTime = true
254+
mediaSettingsAVWrapper.addInput(newAudioWriterInput, to: videoWriter)
255+
self.audioWriterInput = newAudioWriterInput
256+
audioOutput.setSampleBufferDelegate(self, queue: captureSessionQueue)
257+
}
258+
259+
if flashMode == .torch {
260+
try? captureDevice.lockForConfiguration()
261+
captureDevice.torchMode = .on
262+
captureDevice.unlockForConfiguration()
263+
}
264+
265+
mediaSettingsAVWrapper.addInput(videoWriterInput, to: videoWriter)
266+
267+
captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue)
268+
269+
return true
270+
}
271+
125272
func pauseVideoRecording() {
126273
isRecordingPaused = true
127274
videoIsDisconnected = true

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

Lines changed: 0 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,11 @@ @interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate,
2929
AVCaptureAudioDataOutputSampleBufferDelegate>
3030

3131
@property(readonly, nonatomic) int64_t textureId;
32-
@property(readonly, nonatomic) FCPPlatformMediaSettings *mediaSettings;
33-
@property(readonly, nonatomic) FLTCamMediaSettingsAVWrapper *mediaSettingsAVWrapper;
3432

3533
@property(readonly, nonatomic) CGSize captureSize;
3634
@property(strong, nonatomic)
3735
NSObject<FLTAssetWriterInputPixelBufferAdaptor> *assetWriterPixelBufferAdaptor;
3836
@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
39-
@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput;
4037
@property(assign, nonatomic) BOOL isAudioSetup;
4138

4239
/// The queue on which captured photos (not videos) are written to disk.
@@ -47,8 +44,6 @@ @interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate,
4744
@property(nonatomic, copy) VideoDimensionsForFormat videoDimensionsForFormat;
4845
/// A wrapper for AVCaptureDevice creation to allow for dependency injection in tests.
4946
@property(nonatomic, copy) AudioCaptureDeviceFactory audioCaptureDeviceFactory;
50-
@property(nonatomic, copy) AssetWriterFactory assetWriterFactory;
51-
@property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory;
5247
/// Reports the given error message to the Dart side of the plugin.
5348
///
5449
/// Can be called from any thread.
@@ -412,57 +407,6 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset
412407
return bestFormat;
413408
}
414409

415-
/// Main logic to setup the video recording.
416-
- (void)setUpVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion {
417-
NSError *error;
418-
_videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4"
419-
subfolder:@"videos"
420-
prefix:@"REC_"
421-
error:&error];
422-
if (error) {
423-
completion(FlutterErrorFromNSError(error));
424-
return;
425-
}
426-
if (![self setupWriterForPath:_videoRecordingPath]) {
427-
completion([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]);
428-
return;
429-
}
430-
// startWriting should not be called in didOutputSampleBuffer where it can cause state
431-
// in which _isRecording is YES but _videoWriter.status is AVAssetWriterStatusUnknown
432-
// in stopVideoRecording if it is called after startVideoRecording but before
433-
// didOutputSampleBuffer had chance to call startWriting and lag at start of video
434-
// https://github.com/flutter/flutter/issues/132016
435-
// https://github.com/flutter/flutter/issues/151319
436-
[_videoWriter startWriting];
437-
_isFirstVideoSample = YES;
438-
_isRecording = YES;
439-
_isRecordingPaused = NO;
440-
_videoTimeOffset = CMTimeMake(0, 1);
441-
_audioTimeOffset = CMTimeMake(0, 1);
442-
_videoIsDisconnected = NO;
443-
_audioIsDisconnected = NO;
444-
completion(nil);
445-
}
446-
447-
- (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion
448-
messengerForStreaming:(nullable NSObject<FlutterBinaryMessenger> *)messenger {
449-
if (!_isRecording) {
450-
if (messenger != nil) {
451-
[self startImageStreamWithMessenger:messenger
452-
completion:^(FlutterError *_Nullable error) {
453-
[self setUpVideoRecordingWithCompletion:completion];
454-
}];
455-
return;
456-
}
457-
458-
[self setUpVideoRecordingWithCompletion:completion];
459-
} else {
460-
completion([FlutterError errorWithCode:@"Error"
461-
message:@"Video is already recording"
462-
details:nil]);
463-
}
464-
}
465-
466410
- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger
467411
completion:(void (^)(FlutterError *))completion {
468412
[self startImageStreamWithMessenger:messenger
@@ -510,91 +454,6 @@ - (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messen
510454
}
511455
}
512456

513-
- (BOOL)setupWriterForPath:(NSString *)path {
514-
NSError *error = nil;
515-
NSURL *outputURL;
516-
if (path != nil) {
517-
outputURL = [NSURL fileURLWithPath:path];
518-
} else {
519-
return NO;
520-
}
521-
522-
[self setUpCaptureSessionForAudioIfNeeded];
523-
524-
_videoWriter = _assetWriterFactory(outputURL, AVFileTypeMPEG4, &error);
525-
526-
NSParameterAssert(_videoWriter);
527-
if (error) {
528-
[self reportErrorMessage:error.description];
529-
return NO;
530-
}
531-
532-
NSMutableDictionary<NSString *, id> *videoSettings = [[_mediaSettingsAVWrapper
533-
recommendedVideoSettingsForAssetWriterWithFileType:AVFileTypeMPEG4
534-
forOutput:_captureVideoOutput] mutableCopy];
535-
536-
if (_mediaSettings.videoBitrate || _mediaSettings.framesPerSecond) {
537-
NSMutableDictionary *compressionProperties = [[NSMutableDictionary alloc] init];
538-
539-
if (_mediaSettings.videoBitrate) {
540-
compressionProperties[AVVideoAverageBitRateKey] = _mediaSettings.videoBitrate;
541-
}
542-
543-
if (_mediaSettings.framesPerSecond) {
544-
compressionProperties[AVVideoExpectedSourceFrameRateKey] = _mediaSettings.framesPerSecond;
545-
}
546-
547-
videoSettings[AVVideoCompressionPropertiesKey] = compressionProperties;
548-
}
549-
550-
_videoWriterInput =
551-
[_mediaSettingsAVWrapper assetWriterVideoInputWithOutputSettings:videoSettings];
552-
553-
_videoAdaptor = _inputPixelBufferAdaptorFactory(
554-
_videoWriterInput, @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat)});
555-
556-
NSParameterAssert(_videoWriterInput);
557-
558-
_videoWriterInput.expectsMediaDataInRealTime = YES;
559-
560-
// Add the audio input
561-
if (_mediaSettings.enableAudio) {
562-
AudioChannelLayout acl;
563-
bzero(&acl, sizeof(acl));
564-
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
565-
NSMutableDictionary *audioOutputSettings = [@{
566-
AVFormatIDKey : [NSNumber numberWithInt:kAudioFormatMPEG4AAC],
567-
AVSampleRateKey : [NSNumber numberWithFloat:44100.0],
568-
AVNumberOfChannelsKey : [NSNumber numberWithInt:1],
569-
AVChannelLayoutKey : [NSData dataWithBytes:&acl length:sizeof(acl)],
570-
} mutableCopy];
571-
572-
if (_mediaSettings.audioBitrate) {
573-
audioOutputSettings[AVEncoderBitRateKey] = _mediaSettings.audioBitrate;
574-
}
575-
576-
_audioWriterInput =
577-
[_mediaSettingsAVWrapper assetWriterAudioInputWithOutputSettings:audioOutputSettings];
578-
579-
_audioWriterInput.expectsMediaDataInRealTime = YES;
580-
581-
[_mediaSettingsAVWrapper addInput:_audioWriterInput toAssetWriter:_videoWriter];
582-
[_audioOutput setSampleBufferDelegate:self queue:_captureSessionQueue];
583-
}
584-
585-
if (self.flashMode == FCPPlatformFlashModeTorch) {
586-
[self.captureDevice lockForConfiguration:nil];
587-
[self.captureDevice setTorchMode:AVCaptureTorchModeOn];
588-
[self.captureDevice unlockForConfiguration];
589-
}
590-
591-
[_mediaSettingsAVWrapper addInput:_videoWriterInput toAssetWriter:_videoWriter];
592-
593-
[_captureVideoOutput setSampleBufferDelegate:self queue:_captureSessionQueue];
594-
595-
return YES;
596-
}
597-
598457
// This function, although slightly modified, is also in video_player_avfoundation.
599458
// Both need to do the same thing and run on the same thread (for example main thread).
600459
// Configure application wide audio session manually to prevent overwriting flag

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,27 +60,29 @@ NS_ASSUME_NONNULL_BEGIN
6060
@property(readonly, nonatomic) NSObject<FLTCaptureDeviceInputFactory> *captureDeviceInputFactory;
6161
/// All FLTCam's state access and capture session related operations should be on run on this queue.
6262
@property(strong, nonatomic) dispatch_queue_t captureSessionQueue;
63+
@property(nonatomic, copy) AssetWriterFactory assetWriterFactory;
64+
@property(readonly, nonatomic) FLTCamMediaSettingsAVWrapper *mediaSettingsAVWrapper;
65+
@property(readonly, nonatomic) FCPPlatformMediaSettings *mediaSettings;
66+
@property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory;
67+
@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput;
6368

6469
/// Initializes an `FLTCam` instance with the given configuration.
6570
/// @param error report to the caller if any error happened creating the camera.
6671
- (instancetype)initWithConfiguration:(FLTCamConfiguration *)configuration error:(NSError **)error;
6772

6873
- (void)captureToFileWithCompletion:(void (^)(NSString *_Nullable,
6974
FlutterError *_Nullable))completion;
70-
/// Starts recording a video with an optional streaming messenger.
71-
/// If the messenger is non-nil then it will be called for each
72-
/// captured frame, allowing streaming concurrently with recording.
73-
///
74-
/// @param messenger Nullable messenger for capturing each frame.
75-
- (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion
76-
messengerForStreaming:(nullable NSObject<FlutterBinaryMessenger> *)messenger;
7775

7876
- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger
7977
completion:(nonnull void (^)(FlutterError *_Nullable))completion;
8078
- (void)setUpCaptureSessionForAudioIfNeeded;
8179

8280
// Methods exposed for the Swift DefaultCamera subclass
8381
- (void)updateOrientation;
82+
- (nullable NSString *)getTemporaryFilePathWithExtension:(NSString *)extension
83+
subfolder:(NSString *)subfolder
84+
prefix:(NSString *)prefix
85+
error:(NSError **)error;
8486

8587
@end
8688

packages/camera/camera_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_avfoundation
22
description: iOS implementation of the camera plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.9.20+4
5+
version: 0.9.20+5
66

77
environment:
88
sdk: ^3.6.0

0 commit comments

Comments
 (0)