Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>()
let captureSessionQueueSpecificValue = "capture_session_queue"
Original file line number Diff line number Diff line change
Expand Up @@ -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 () <AVCaptureVideoDataOutputSampleBufferDelegate,
AVCaptureAudioDataOutputSampleBufferDelegate>

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

/// 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;
/// A wrapper for CMVideoFormatDescriptionGetDimensions.
/// Allows for alternate implementations in tests.
@property(nonatomic, copy) VideoDimensionsForFormat videoDimensionsForFormat;
Expand All @@ -62,7 +53,6 @@ - (instancetype)initWithConfiguration:(nonnull FLTCamConfiguration *)configurati
_mediaSettingsAVWrapper = configuration.mediaSettingsWrapper;

_captureSessionQueue = configuration.captureSessionQueue;
_photoIOQueue = dispatch_queue_create("io.flutter.camera.photoIOQueue", NULL);
_videoCaptureSession = configuration.videoCaptureSession;
_audioCaptureSession = configuration.audioCaptureSession;
_captureDeviceFactory = configuration.captureDeviceFactory;
Expand Down Expand Up @@ -206,69 +196,6 @@ - (void)updateOrientation:(UIDeviceOrientation)orientation
}
}

- (void)captureToFileWithCompletion:(void (^)(NSString *_Nullable,
FlutterError *_Nullable))completion {
AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];

if (self.mediaSettings.resolutionPreset == FCPPlatformResolutionPresetMax) {
[settings setHighResolutionPhotoEnabled:YES];
}

NSString *extension;

BOOL isHEVCCodecAvailable =
[self.capturePhotoOutput.availablePhotoCodecTypes containsObject:AVVideoCodecTypeHEVC];

if (_fileFormat == FCPPlatformImageFileFormatHeif && isHEVCCodecAvailable) {
settings =
[AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey : AVVideoCodecTypeHEVC}];
extension = @"heif";
} else {
extension = @"jpg";
}

// If the flash is in torch mode, no capture-level flash setting is needed.
if (self.flashMode != FCPPlatformFlashModeTorch) {
[settings setFlashMode:FCPGetAVCaptureFlashModeForPigeonFlashMode(self.flashMode)];
}
NSError *error;
NSString *path = [self getTemporaryFilePathWithExtension:extension
subfolder:@"pictures"
prefix:@"CAP_"
error:&error];
if (error) {
completion(nil, FlutterErrorFromNSError(error));
return;
}

__weak typeof(self) weakSelf = self;
FLTSavePhotoDelegate *savePhotoDelegate = [[FLTSavePhotoDelegate alloc]
initWithPath:path
ioQueue:self.photoIOQueue
completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) {
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
dispatch_async(strongSelf.captureSessionQueue, ^{
// cannot use the outter `strongSelf`
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf.inProgressSavePhotoDelegates removeObjectForKey:@(settings.uniqueID)];
});

if (error) {
completion(nil, FlutterErrorFromNSError(error));
} else {
NSAssert(path, @"Path must not be nil if no error.");
completion(path, nil);
}
}];

NSAssert(dispatch_get_specific(FLTCaptureSessionQueueSpecific),
@"save photo delegate references must be updated on the capture session queue");
self.inProgressSavePhotoDelegates[@(settings.uniqueID)] = savePhotoDelegate;
[self.capturePhotoOutput capturePhotoWithSettings:settings delegate:savePhotoDelegate];
}

- (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
(UIDeviceOrientation)deviceOrientation {
if (deviceOrientation == UIDeviceOrientationPortrait) {
Expand All @@ -288,32 +215,6 @@ - (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
}
}

- (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension
subfolder:(NSString *)subfolder
prefix:(NSString *)prefix
error:(NSError **)error {
NSString *docDir =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *fileDir =
[[docDir stringByAppendingPathComponent:@"camera"] stringByAppendingPathComponent:subfolder];
NSString *fileName = [prefix stringByAppendingString:[[NSUUID UUID] UUIDString]];
NSString *file =
[[fileDir stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:extension];

NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:fileDir]) {
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:fileDir
withIntermediateDirectories:true
attributes:nil
error:error];
if (!success) {
return nil;
}
}

return file;
}

- (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset
withError:(NSError **)error {
switch (resolutionPreset) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,10 @@

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

const char *FLTCaptureSessionQueueSpecific = "capture_session_queue";

void FLTEnsureToRunOnMainQueue(dispatch_block_t block) {
if (!NSThread.isMainThread) {
dispatch_async(dispatch_get_main_queue(), block);
} else {
block();
}
}

void FLTDispatchQueueSetSpecific(dispatch_queue_t queue, const void *key) {
dispatch_queue_set_specific(queue, key, (void *)key, NULL);
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,12 @@ NS_ASSUME_NONNULL_BEGIN
/// @param error report to the caller if any error happened creating the camera.
- (instancetype)initWithConfiguration:(FLTCamConfiguration *)configuration error:(NSError **)error;

- (void)captureToFileWithCompletion:(void (^)(NSString *_Nullable,
FlutterError *_Nullable))completion;

- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,11 @@

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
/// main queue.
/// @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
2 changes: 1 addition & 1 deletion packages/camera/camera_avfoundation/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down