Skip to content

Commit 699fb4d

Browse files
[camera_avfoundation] Implementation swift migration - part 10 (#9664)
Migrates camera implementation as part of flutter/flutter#119109 This PR migrates the 7th chunk of `FLTCam` class to Swift: * `captureToFile` * `getTemporaryFilePath` We can also finally switch to Swift interface for dispatch queue specific because all usages are now in Swift. ## 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 2755aa3 commit 699fb4d

File tree

10 files changed

+118
-131
lines changed

10 files changed

+118
-131
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.9.20+6
2+
3+
* Migrates `captureToFile` and `getTemporaryFilePath` methods to Swift.
4+
* Switches to Swift dispatch queue specific interface.
5+
16
## 0.9.20+5
27

38
* Migrates `startVideoRecording`, `setUpVideoRecording`, and `setupWriter` methods to Swift.

packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ final class PhotoCaptureTests: XCTestCase {
2424
let errorExpectation = expectation(
2525
description: "Must send error to result if save photo delegate completes with error.")
2626
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
27-
FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific)
27+
captureSessionQueue.setSpecific(
28+
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
2829
let cam = createCam(with: captureSessionQueue)
2930
let error = NSError(domain: "test", code: 0, userInfo: nil)
3031

@@ -57,7 +58,8 @@ final class PhotoCaptureTests: XCTestCase {
5758
let pathExpectation = expectation(
5859
description: "Must send file path to result if save photo delegate completes with file path.")
5960
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
60-
FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific)
61+
captureSessionQueue.setSpecific(
62+
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
6163
let cam = createCam(with: captureSessionQueue)
6264
let filePath = "test"
6365

@@ -90,7 +92,8 @@ final class PhotoCaptureTests: XCTestCase {
9092
description: "Test must set extension to heif if availablePhotoCodecTypes contains HEVC.")
9193

9294
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
93-
FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific)
95+
captureSessionQueue.setSpecific(
96+
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
9497
let cam = createCam(with: captureSessionQueue)
9598
cam.setImageFileFormat(FCPPlatformImageFileFormat.heif)
9699

@@ -125,7 +128,8 @@ final class PhotoCaptureTests: XCTestCase {
125128
"Test must set extension to jpg if availablePhotoCodecTypes does not contain HEVC.")
126129

127130
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
128-
FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific)
131+
captureSessionQueue.setSpecific(
132+
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
129133
let cam = createCam(with: captureSessionQueue)
130134
cam.setImageFileFormat(FCPPlatformImageFileFormat.heif)
131135

@@ -170,7 +174,8 @@ final class PhotoCaptureTests: XCTestCase {
170174
}
171175

172176
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
173-
FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific)
177+
captureSessionQueue.setSpecific(
178+
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
174179
let configuration = CameraTestUtils.createTestCameraConfiguration()
175180
configuration.captureSessionQueue = captureSessionQueue
176181
configuration.captureDeviceFactory = { _ in captureDeviceMock }

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ public final class CameraPlugin: NSObject, FlutterPlugin {
6969

7070
super.init()
7171

72-
FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific)
72+
captureSessionQueue.setSpecific(
73+
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
7374

7475
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
7576
NotificationCenter.default.addObserver(

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ final class DefaultCamera: FLTCam, Camera {
3838
private let pixelBufferSynchronizationQueue = DispatchQueue(
3939
label: "io.flutter.camera.pixelBufferSynchronizationQueue")
4040

41+
/// The queue on which captured photos (not videos) are written to disk.
42+
/// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation.
43+
private let photoIOQueue = DispatchQueue(label: "io.flutter.camera.photoIOQueue")
44+
4145
/// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback.
4246
/// Used to deliver the latest pixel buffer to the flutter engine via the `copyPixelBuffer` API.
4347
private var latestPixelBuffer: CVPixelBuffer?
@@ -313,6 +317,93 @@ final class DefaultCamera: FLTCam, Camera {
313317
}
314318
}
315319

320+
func captureToFile(completion: @escaping (String?, FlutterError?) -> Void) {
321+
var settings = AVCapturePhotoSettings()
322+
323+
if mediaSettings.resolutionPreset == .max {
324+
settings.isHighResolutionPhotoEnabled = true
325+
}
326+
327+
let fileExtension: String
328+
329+
let isHEVCCodecAvailable = capturePhotoOutput.availablePhotoCodecTypes.contains(
330+
.hevc)
331+
332+
if fileFormat == .heif, isHEVCCodecAvailable {
333+
settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
334+
fileExtension = "heif"
335+
} else {
336+
fileExtension = "jpg"
337+
}
338+
339+
if flashMode != .torch {
340+
settings.flashMode = FCPGetAVCaptureFlashModeForPigeonFlashMode(flashMode)
341+
}
342+
343+
let path: String
344+
do {
345+
path = try getTemporaryFilePath(
346+
withExtension: fileExtension,
347+
subfolder: "pictures",
348+
prefix: "CAP_")
349+
} catch let error as NSError {
350+
completion(nil, DefaultCamera.flutterErrorFromNSError(error))
351+
return
352+
}
353+
354+
let savePhotoDelegate = FLTSavePhotoDelegate(
355+
path: path,
356+
ioQueue: photoIOQueue,
357+
completionHandler: { [weak self] path, error in
358+
guard let strongSelf = self else { return }
359+
360+
strongSelf.captureSessionQueue.async { [weak self] in
361+
self?.inProgressSavePhotoDelegates.removeObject(
362+
forKey: settings.uniqueID)
363+
}
364+
365+
if let error = error {
366+
completion(nil, DefaultCamera.flutterErrorFromNSError(error as NSError))
367+
} else {
368+
assert(path != nil, "Path must not be nil if no error.")
369+
completion(path, nil)
370+
}
371+
}
372+
)
373+
374+
assert(
375+
DispatchQueue.getSpecific(key: captureSessionQueueSpecificKey)
376+
== captureSessionQueueSpecificValue,
377+
"save photo delegate references must be updated on the capture session queue")
378+
inProgressSavePhotoDelegates[settings.uniqueID] = savePhotoDelegate
379+
capturePhotoOutput.capturePhoto(with: settings, delegate: savePhotoDelegate)
380+
}
381+
382+
private func getTemporaryFilePath(
383+
withExtension ext: String,
384+
subfolder: String,
385+
prefix: String
386+
) throws -> String {
387+
let documentDirectory = FileManager.default.urls(
388+
for: .documentDirectory,
389+
in: .userDomainMask)[0]
390+
391+
let fileDirectory = documentDirectory.appendingPathComponent("camera").appendingPathComponent(
392+
subfolder)
393+
let fileName = prefix + UUID().uuidString
394+
let file = fileDirectory.appendingPathComponent(fileName).appendingPathExtension(ext).path
395+
396+
let fileManager = FileManager.default
397+
if !fileManager.fileExists(atPath: fileDirectory.path) {
398+
try fileManager.createDirectory(
399+
at: fileDirectory,
400+
withIntermediateDirectories: true,
401+
attributes: nil)
402+
}
403+
404+
return file
405+
}
406+
316407
func lockCaptureOrientation(_ pigeonOrientation: FCPPlatformDeviceOrientation) {
317408
let orientation = FCPGetUIDeviceOrientationForPigeonDeviceOrientation(pigeonOrientation)
318409
if lockedCaptureOrientation != orientation {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import Dispatch
6+
7+
/// Queue-specific context data to be associated with the capture session queue.
8+
let captureSessionQueueSpecificKey = DispatchSpecificKey<String>()
9+
let captureSessionQueueSpecificValue = "capture_session_queue"

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

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,6 @@
1919
#import "./include/camera_avfoundation/QueueUtils.h"
2020
#import "./include/camera_avfoundation/messages.g.h"
2121

22-
static FlutterError *FlutterErrorFromNSError(NSError *error) {
23-
return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code]
24-
message:error.localizedDescription
25-
details:error.domain];
26-
}
27-
2822
@interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate,
2923
AVCaptureAudioDataOutputSampleBufferDelegate>
3024

@@ -36,9 +30,6 @@ @interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate,
3630
@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
3731
@property(assign, nonatomic) BOOL isAudioSetup;
3832

39-
/// The queue on which captured photos (not videos) are written to disk.
40-
/// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation.
41-
@property(strong, nonatomic) dispatch_queue_t photoIOQueue;
4233
/// A wrapper for CMVideoFormatDescriptionGetDimensions.
4334
/// Allows for alternate implementations in tests.
4435
@property(nonatomic, copy) VideoDimensionsForFormat videoDimensionsForFormat;
@@ -62,7 +53,6 @@ - (instancetype)initWithConfiguration:(nonnull FLTCamConfiguration *)configurati
6253
_mediaSettingsAVWrapper = configuration.mediaSettingsWrapper;
6354

6455
_captureSessionQueue = configuration.captureSessionQueue;
65-
_photoIOQueue = dispatch_queue_create("io.flutter.camera.photoIOQueue", NULL);
6656
_videoCaptureSession = configuration.videoCaptureSession;
6757
_audioCaptureSession = configuration.audioCaptureSession;
6858
_captureDeviceFactory = configuration.captureDeviceFactory;
@@ -206,69 +196,6 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation
206196
}
207197
}
208198

209-
- (void)captureToFileWithCompletion:(void (^)(NSString *_Nullable,
210-
FlutterError *_Nullable))completion {
211-
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
212-
213-
if (self.mediaSettings.resolutionPreset == FCPPlatformResolutionPresetMax) {
214-
[settings setHighResolutionPhotoEnabled:YES];
215-
}
216-
217-
NSString *extension;
218-
219-
BOOL isHEVCCodecAvailable =
220-
[self.capturePhotoOutput.availablePhotoCodecTypes containsObject:AVVideoCodecTypeHEVC];
221-
222-
if (_fileFormat == FCPPlatformImageFileFormatHeif && isHEVCCodecAvailable) {
223-
settings =
224-
[AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey : AVVideoCodecTypeHEVC}];
225-
extension = @"heif";
226-
} else {
227-
extension = @"jpg";
228-
}
229-
230-
// If the flash is in torch mode, no capture-level flash setting is needed.
231-
if (self.flashMode != FCPPlatformFlashModeTorch) {
232-
[settings setFlashMode:FCPGetAVCaptureFlashModeForPigeonFlashMode(self.flashMode)];
233-
}
234-
NSError *error;
235-
NSString *path = [self getTemporaryFilePathWithExtension:extension
236-
subfolder:@"pictures"
237-
prefix:@"CAP_"
238-
error:&error];
239-
if (error) {
240-
completion(nil, FlutterErrorFromNSError(error));
241-
return;
242-
}
243-
244-
__weak typeof(self) weakSelf = self;
245-
FLTSavePhotoDelegate *savePhotoDelegate = [[FLTSavePhotoDelegate alloc]
246-
initWithPath:path
247-
ioQueue:self.photoIOQueue
248-
completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) {
249-
typeof(self) strongSelf = weakSelf;
250-
if (!strongSelf) return;
251-
dispatch_async(strongSelf.captureSessionQueue, ^{
252-
// cannot use the outter `strongSelf`
253-
typeof(self) strongSelf = weakSelf;
254-
if (!strongSelf) return;
255-
[strongSelf.inProgressSavePhotoDelegates removeObjectForKey:@(settings.uniqueID)];
256-
});
257-
258-
if (error) {
259-
completion(nil, FlutterErrorFromNSError(error));
260-
} else {
261-
NSAssert(path, @"Path must not be nil if no error.");
262-
completion(path, nil);
263-
}
264-
}];
265-
266-
NSAssert(dispatch_get_specific(FLTCaptureSessionQueueSpecific),
267-
@"save photo delegate references must be updated on the capture session queue");
268-
self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = savePhotoDelegate;
269-
[self.capturePhotoOutput capturePhotoWithSettings:settings delegate:savePhotoDelegate];
270-
}
271-
272199
- (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
273200
(UIDeviceOrientation)deviceOrientation {
274201
if (deviceOrientation == UIDeviceOrientationPortrait) {
@@ -288,32 +215,6 @@ - (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
288215
}
289216
}
290217

291-
- (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension
292-
subfolder:(NSString *)subfolder
293-
prefix:(NSString *)prefix
294-
error:(NSError **)error {
295-
NSString *docDir =
296-
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
297-
NSString *fileDir =
298-
[[docDir stringByAppendingPathComponent:@"camera"] stringByAppendingPathComponent:subfolder];
299-
NSString *fileName = [prefix stringByAppendingString:[[NSUUID UUID] UUIDString]];
300-
NSString *file =
301-
[[fileDir stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:extension];
302-
303-
NSFileManager *fm = [NSFileManager defaultManager];
304-
if (![fm fileExistsAtPath:fileDir]) {
305-
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:fileDir
306-
withIntermediateDirectories:true
307-
attributes:nil
308-
error:error];
309-
if (!success) {
310-
return nil;
311-
}
312-
}
313-
314-
return file;
315-
}
316-
317218
- (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset
318219
withError:(NSError **)error {
319220
switch (resolutionPreset) {

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,10 @@
44

55
#import "./include/camera_avfoundation/QueueUtils.h"
66

7-
const char *FLTCaptureSessionQueueSpecific = "capture_session_queue";
8-
97
void FLTEnsureToRunOnMainQueue(dispatch_block_t block) {
108
if (!NSThread.isMainThread) {
119
dispatch_async(dispatch_get_main_queue(), block);
1210
} else {
1311
block();
1412
}
1513
}
16-
17-
void FLTDispatchQueueSetSpecific(dispatch_queue_t queue, const void *key) {
18-
dispatch_queue_set_specific(queue, key, (void *)key, NULL);
19-
}

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,12 @@ NS_ASSUME_NONNULL_BEGIN
7070
/// @param error report to the caller if any error happened creating the camera.
7171
- (instancetype)initWithConfiguration:(FLTCamConfiguration *)configuration error:(NSError **)error;
7272

73-
- (void)captureToFileWithCompletion:(void (^)(NSString *_Nullable,
74-
FlutterError *_Nullable))completion;
75-
7673
- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger
7774
completion:(nonnull void (^)(FlutterError *_Nullable))completion;
7875
- (void)setUpCaptureSessionForAudioIfNeeded;
7976

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

8780
@end
8881

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,11 @@
66

77
NS_ASSUME_NONNULL_BEGIN
88

9-
/// Queue-specific context data to be associated with the capture session queue.
10-
extern const char* FLTCaptureSessionQueueSpecific;
11-
129
/// Ensures the given block to be run on the main queue.
1310
/// If caller site is already on the main queue, the block will be run
1411
/// synchronously. Otherwise, the block will be dispatched asynchronously to the
1512
/// main queue.
1613
/// @param block the block to be run on the main queue.
1714
extern void FLTEnsureToRunOnMainQueue(dispatch_block_t block);
1815

19-
/// Calls `dispatch_queue_set_specific` with a key that is used to identify the
20-
/// queue. This method is needed for compatibility of Swift implementation with
21-
/// Objective-C code. In Swift, the API for setting key-value pairs on a queue
22-
/// is different, so Swift code need to call this method to set the key-value
23-
/// pair on the queue in a way that's compatible with the existing Objective-C
24-
/// code.
25-
extern void FLTDispatchQueueSetSpecific(dispatch_queue_t queue,
26-
const void* key);
27-
2816
NS_ASSUME_NONNULL_END

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+5
5+
version: 0.9.20+6
66

77
environment:
88
sdk: ^3.6.0

0 commit comments

Comments
 (0)