diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index e42f07ac6a0..34e38f055fd 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.20+6 + +* Migrates `captureToFile` and `getTemporaryFilePath` methods to Swift. +* Switches to Swift dispatch queue specific interface. + ## 0.9.20+5 * Migrates `startVideoRecording`, `setUpVideoRecording`, and `setupWriter` methods to Swift. diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift index ad4b99c86b7..6611f514db9 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift @@ -24,7 +24,8 @@ final class PhotoCaptureTests: XCTestCase { let errorExpectation = expectation( description: "Must send error to result if save photo delegate completes with error.") let captureSessionQueue = DispatchQueue(label: "capture_session_queue") - FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) let cam = createCam(with: captureSessionQueue) let error = NSError(domain: "test", code: 0, userInfo: nil) @@ -57,7 +58,8 @@ final class PhotoCaptureTests: XCTestCase { let pathExpectation = expectation( description: "Must send file path to result if save photo delegate completes with file path.") let captureSessionQueue = DispatchQueue(label: "capture_session_queue") - FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) let cam = createCam(with: captureSessionQueue) let filePath = "test" @@ -90,7 +92,8 @@ final class PhotoCaptureTests: XCTestCase { description: "Test must set extension to heif if availablePhotoCodecTypes contains HEVC.") let captureSessionQueue = DispatchQueue(label: "capture_session_queue") - FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) let cam = createCam(with: captureSessionQueue) cam.setImageFileFormat(FCPPlatformImageFileFormat.heif) @@ -125,7 +128,8 @@ final class PhotoCaptureTests: XCTestCase { "Test must set extension to jpg if availablePhotoCodecTypes does not contain HEVC.") let captureSessionQueue = DispatchQueue(label: "capture_session_queue") - FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) let cam = createCam(with: captureSessionQueue) cam.setImageFileFormat(FCPPlatformImageFileFormat.heif) @@ -170,7 +174,8 @@ final class PhotoCaptureTests: XCTestCase { } let captureSessionQueue = DispatchQueue(label: "capture_session_queue") - FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) let configuration = CameraTestUtils.createTestCameraConfiguration() configuration.captureSessionQueue = captureSessionQueue configuration.captureDeviceFactory = { _ in captureDeviceMock } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift index 1297256d9c3..3b3eaaae051 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift @@ -69,7 +69,8 @@ public final class CameraPlugin: NSObject, FlutterPlugin { super.init() - FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) UIDevice.current.beginGeneratingDeviceOrientationNotifications() NotificationCenter.default.addObserver( 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 39c31defd92..3d573b99bca 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 @@ -38,6 +38,10 @@ final class DefaultCamera: FLTCam, Camera { private let pixelBufferSynchronizationQueue = DispatchQueue( label: "io.flutter.camera.pixelBufferSynchronizationQueue") + /// 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. + private let photoIOQueue = DispatchQueue(label: "io.flutter.camera.photoIOQueue") + /// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback. /// Used to deliver the latest pixel buffer to the flutter engine via the `copyPixelBuffer` API. private var latestPixelBuffer: CVPixelBuffer? @@ -313,6 +317,93 @@ final class DefaultCamera: FLTCam, Camera { } } + func captureToFile(completion: @escaping (String?, FlutterError?) -> Void) { + var settings = AVCapturePhotoSettings() + + if mediaSettings.resolutionPreset == .max { + settings.isHighResolutionPhotoEnabled = true + } + + let fileExtension: String + + let isHEVCCodecAvailable = capturePhotoOutput.availablePhotoCodecTypes.contains( + .hevc) + + if fileFormat == .heif, isHEVCCodecAvailable { + settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc]) + fileExtension = "heif" + } else { + fileExtension = "jpg" + } + + if flashMode != .torch { + settings.flashMode = FCPGetAVCaptureFlashModeForPigeonFlashMode(flashMode) + } + + let path: String + do { + path = try getTemporaryFilePath( + withExtension: fileExtension, + subfolder: "pictures", + prefix: "CAP_") + } catch let error as NSError { + completion(nil, DefaultCamera.flutterErrorFromNSError(error)) + return + } + + let savePhotoDelegate = FLTSavePhotoDelegate( + path: path, + ioQueue: photoIOQueue, + completionHandler: { [weak self] path, error in + guard let strongSelf = self else { return } + + strongSelf.captureSessionQueue.async { [weak self] in + self?.inProgressSavePhotoDelegates.removeObject( + forKey: settings.uniqueID) + } + + if let error = error { + completion(nil, DefaultCamera.flutterErrorFromNSError(error as NSError)) + } else { + assert(path != nil, "Path must not be nil if no error.") + completion(path, nil) + } + } + ) + + assert( + DispatchQueue.getSpecific(key: captureSessionQueueSpecificKey) + == captureSessionQueueSpecificValue, + "save photo delegate references must be updated on the capture session queue") + inProgressSavePhotoDelegates[settings.uniqueID] = savePhotoDelegate + capturePhotoOutput.capturePhoto(with: settings, delegate: savePhotoDelegate) + } + + private func getTemporaryFilePath( + withExtension ext: String, + subfolder: String, + prefix: String + ) throws -> String { + let documentDirectory = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask)[0] + + let fileDirectory = documentDirectory.appendingPathComponent("camera").appendingPathComponent( + subfolder) + let fileName = prefix + UUID().uuidString + let file = fileDirectory.appendingPathComponent(fileName).appendingPathExtension(ext).path + + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: fileDirectory.path) { + try fileManager.createDirectory( + at: fileDirectory, + withIntermediateDirectories: true, + attributes: nil) + } + + return file + } + func lockCaptureOrientation(_ pigeonOrientation: FCPPlatformDeviceOrientation) { let orientation = FCPGetUIDeviceOrientationForPigeonDeviceOrientation(pigeonOrientation) if lockedCaptureOrientation != orientation { diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/QueueUtils.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/QueueUtils.swift new file mode 100644 index 00000000000..6968d4d5ce5 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/QueueUtils.swift @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Dispatch + +/// Queue-specific context data to be associated with the capture session queue. +let captureSessionQueueSpecificKey = DispatchSpecificKey() +let captureSessionQueueSpecificValue = "capture_session_queue" 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 ed57a1364ab..4b3f1bb82b1 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 @@ -19,12 +19,6 @@ #import "./include/camera_avfoundation/QueueUtils.h" #import "./include/camera_avfoundation/messages.g.h" -static FlutterError *FlutterErrorFromNSError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; -} - @interface FLTCam () @@ -36,9 +30,6 @@ @interface FLTCam () *)messenger completion:(nonnull void (^)(FlutterError *_Nullable))completion; - (void)setUpCaptureSessionForAudioIfNeeded; // Methods exposed for the Swift DefaultCamera subclass - (void)updateOrientation; -- (nullable NSString *)getTemporaryFilePathWithExtension:(NSString *)extension - subfolder:(NSString *)subfolder - prefix:(NSString *)prefix - error:(NSError **)error; @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h index bc8fc49840d..c77d5e2cd06 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h @@ -6,9 +6,6 @@ NS_ASSUME_NONNULL_BEGIN -/// Queue-specific context data to be associated with the capture session queue. -extern const char* FLTCaptureSessionQueueSpecific; - /// Ensures the given block to be run on the main queue. /// If caller site is already on the main queue, the block will be run /// synchronously. Otherwise, the block will be dispatched asynchronously to the @@ -16,13 +13,4 @@ extern const char* FLTCaptureSessionQueueSpecific; /// @param block the block to be run on the main queue. extern void FLTEnsureToRunOnMainQueue(dispatch_block_t block); -/// Calls `dispatch_queue_set_specific` with a key that is used to identify the -/// queue. This method is needed for compatibility of Swift implementation with -/// Objective-C code. In Swift, the API for setting key-value pairs on a queue -/// is different, so Swift code need to call this method to set the key-value -/// pair on the queue in a way that's compatible with the existing Objective-C -/// code. -extern void FLTDispatchQueueSetSpecific(dispatch_queue_t queue, - const void* key); - NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 362c9166667..81472526975 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+5 +version: 0.9.20+6 environment: sdk: ^3.6.0