Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 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