[camera]refactor FLTCam outside of CameraPlugin (#4708)
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index dd4042e..ea27720 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,5 +1,6 @@
## NEXT
+* Minor iOS internal code cleanup related to camera class and its delegate.
* Minor iOS internal code cleanup related to resolution preset, video format, focus mode, exposure mode and device orientation.
* Minor iOS internal code cleanup related to flash mode.
diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m
index fdc2be9..e0f5fda 100644
--- a/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m
+++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m
@@ -3,25 +3,11 @@
// found in the LICENSE file.
@import camera;
+@import camera.Test;
@import XCTest;
@import AVFoundation;
#import <OCMock/OCMock.h>
-// Mirrors FocusMode in camera.dart
-typedef enum {
- FocusModeAuto,
- FocusModeLocked,
-} FocusMode;
-
-@interface FLTCam : NSObject <FlutterTexture,
- AVCaptureVideoDataOutputSampleBufferDelegate,
- AVCaptureAudioDataOutputSampleBufferDelegate>
-
-- (void)applyFocusMode;
-- (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice;
-- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y;
-@end
-
@interface CameraFocusTests : XCTestCase
@property(readonly, nonatomic) FLTCam *camera;
@property(readonly, nonatomic) id mockDevice;
@@ -51,7 +37,7 @@
[[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus];
// Run test
- [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice];
+ [_camera applyFocusMode:FLTFocusModeAuto onDevice:_mockDevice];
// Expect setFocusMode:AVCaptureFocusModeContinuousAutoFocus
OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus]);
@@ -68,7 +54,7 @@
[[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
// Run test
- [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice];
+ [_camera applyFocusMode:FLTFocusModeAuto onDevice:_mockDevice];
// Expect setFocusMode:AVCaptureFocusModeAutoFocus
OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeAutoFocus]);
@@ -86,7 +72,7 @@
[[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus];
// Run test
- [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice];
+ [_camera applyFocusMode:FLTFocusModeAuto onDevice:_mockDevice];
}
- (void)testLockedFocusWithModeSupported_ShouldSetModeAutoFocus {
@@ -99,7 +85,7 @@
[[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
// Run test
- [_camera applyFocusMode:FocusModeLocked onDevice:_mockDevice];
+ [_camera applyFocusMode:FLTFocusModeLocked onDevice:_mockDevice];
// Expect setFocusMode:AVCaptureFocusModeAutoFocus
OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeAutoFocus]);
@@ -116,7 +102,7 @@
[[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus];
// Run test
- [_camera applyFocusMode:FocusModeLocked onDevice:_mockDevice];
+ [_camera applyFocusMode:FLTFocusModeLocked onDevice:_mockDevice];
}
- (void)testSetFocusPointWithResult_SetsFocusPointOfInterest {
diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m
index eb6c007..2c1adbe 100644
--- a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m
+++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m
@@ -3,21 +3,12 @@
// found in the LICENSE file.
@import camera;
+@import camera.Test;
@import XCTest;
@import AVFoundation;
#import <OCMock/OCMock.h>
#import "MockFLTThreadSafeFlutterResult.h"
-@interface FLTCam : NSObject <FlutterTexture,
- AVCaptureVideoDataOutputSampleBufferDelegate,
- AVCaptureAudioDataOutputSampleBufferDelegate>
-@property(assign, nonatomic) BOOL isPreviewPaused;
-
-- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result;
-
-- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result;
-@end
-
@interface CameraPreviewPauseTests : XCTestCase
@end
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index ae55188..a0adb70 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -5,1121 +5,15 @@
#import "CameraPlugin.h"
#import "CameraPlugin_Test.h"
-#import <AVFoundation/AVFoundation.h>
-#import <Accelerate/Accelerate.h>
-#import <CoreMotion/CoreMotion.h>
-#import <libkern/OSAtomic.h>
-#import <uuid/uuid.h>
+@import AVFoundation;
+
#import "CameraProperties.h"
+#import "FLTCam.h"
#import "FLTThreadSafeEventChannel.h"
#import "FLTThreadSafeFlutterResult.h"
#import "FLTThreadSafeMethodChannel.h"
#import "FLTThreadSafeTextureRegistry.h"
-@interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate>
-@property(readonly, nonatomic) NSString *path;
-@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result;
-@end
-
-@interface FLTImageStreamHandler : NSObject <FlutterStreamHandler>
-// The queue on which `eventSink` property should be accessed
-@property(nonatomic, strong) dispatch_queue_t captureSessionQueue;
-// `eventSink` property should be accessed on `captureSessionQueue`.
-// The block itself should be invoked on the main queue.
-@property FlutterEventSink eventSink;
-@end
-
-@implementation FLTImageStreamHandler
-
-- (instancetype)initWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue {
- self = [super init];
- NSAssert(self, @"super init cannot be nil");
- _captureSessionQueue = captureSessionQueue;
- return self;
-}
-
-- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments {
- dispatch_async(self.captureSessionQueue, ^{
- self.eventSink = nil;
- });
- return nil;
-}
-
-- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
- eventSink:(nonnull FlutterEventSink)events {
- dispatch_async(self.captureSessionQueue, ^{
- self.eventSink = events;
- });
- return nil;
-}
-@end
-
-@implementation FLTSavePhotoDelegate {
- /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer.
- FLTSavePhotoDelegate *selfReference;
-}
-
-- initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result {
- self = [super init];
- NSAssert(self, @"super init cannot be nil");
- _path = path;
- selfReference = self;
- _result = result;
- return self;
-}
-
-- (void)captureOutput:(AVCapturePhotoOutput *)output
- didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer
- previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer
- resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
- bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings
- error:(NSError *)error API_AVAILABLE(ios(10)) {
- selfReference = nil;
- if (error) {
- [_result sendError:error];
- return;
- }
-
- NSData *data = [AVCapturePhotoOutput
- JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer
- previewPhotoSampleBuffer:previewPhotoSampleBuffer];
-
- // TODO(sigurdm): Consider writing file asynchronously.
- bool success = [data writeToFile:_path atomically:YES];
-
- if (!success) {
- [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil];
- return;
- }
- [_result sendSuccessWithData:_path];
-}
-
-- (void)captureOutput:(AVCapturePhotoOutput *)output
- didFinishProcessingPhoto:(AVCapturePhoto *)photo
- error:(NSError *)error API_AVAILABLE(ios(11.0)) {
- selfReference = nil;
- if (error) {
- [_result sendError:error];
- return;
- }
-
- NSData *photoData = [photo fileDataRepresentation];
-
- bool success = [photoData writeToFile:_path atomically:YES];
- if (!success) {
- [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil];
- return;
- }
- [_result sendSuccessWithData:_path];
-}
-@end
-
-@interface FLTCam : NSObject <FlutterTexture,
- AVCaptureVideoDataOutputSampleBufferDelegate,
- AVCaptureAudioDataOutputSampleBufferDelegate>
-@property(readonly, nonatomic) int64_t textureId;
-@property(nonatomic, copy) void (^onFrameAvailable)(void);
-@property BOOL enableAudio;
-@property(nonatomic) FLTImageStreamHandler *imageStreamHandler;
-@property(nonatomic) FLTThreadSafeMethodChannel *methodChannel;
-@property(readonly, nonatomic) AVCaptureSession *captureSession;
-@property(readonly, nonatomic) AVCaptureDevice *captureDevice;
-@property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10));
-@property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput;
-@property(readonly, nonatomic) AVCaptureInput *captureVideoInput;
-@property(readonly) CVPixelBufferRef volatile latestPixelBuffer;
-@property(readonly, nonatomic) CGSize previewSize;
-@property(readonly, nonatomic) CGSize captureSize;
-@property(strong, nonatomic) AVAssetWriter *videoWriter;
-@property(strong, nonatomic) AVAssetWriterInput *videoWriterInput;
-@property(strong, nonatomic) AVAssetWriterInput *audioWriterInput;
-@property(strong, nonatomic) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor;
-@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
-@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput;
-@property(strong, nonatomic) NSString *videoRecordingPath;
-@property(assign, nonatomic) BOOL isRecording;
-@property(assign, nonatomic) BOOL isRecordingPaused;
-@property(assign, nonatomic) BOOL videoIsDisconnected;
-@property(assign, nonatomic) BOOL audioIsDisconnected;
-@property(assign, nonatomic) BOOL isAudioSetup;
-@property(assign, nonatomic) BOOL isStreamingImages;
-@property(assign, nonatomic) BOOL isPreviewPaused;
-@property(assign, nonatomic) FLTResolutionPreset resolutionPreset;
-@property(assign, nonatomic) FLTExposureMode exposureMode;
-@property(assign, nonatomic) FLTFocusMode focusMode;
-@property(assign, nonatomic) FLTFlashMode flashMode;
-@property(assign, nonatomic) UIDeviceOrientation lockedCaptureOrientation;
-@property(assign, nonatomic) CMTime lastVideoSampleTime;
-@property(assign, nonatomic) CMTime lastAudioSampleTime;
-@property(assign, nonatomic) CMTime videoTimeOffset;
-@property(assign, nonatomic) CMTime audioTimeOffset;
-// Format used for video and image streaming.
-@property(assign, nonatomic) FourCharCode videoFormat;
-@property(nonatomic) CMMotionManager *motionManager;
-@property AVAssetWriterInputPixelBufferAdaptor *videoAdaptor;
-@end
-
-@implementation FLTCam {
- // All FLTCam's state access and capture session related operations should be on run on this
- // queue.
- dispatch_queue_t _captureSessionQueue;
- UIDeviceOrientation _deviceOrientation;
-}
-NSString *const errorMethod = @"error";
-
-- (instancetype)initWithCameraName:(NSString *)cameraName
- resolutionPreset:(NSString *)resolutionPreset
- enableAudio:(BOOL)enableAudio
- orientation:(UIDeviceOrientation)orientation
- captureSessionQueue:(dispatch_queue_t)captureSessionQueue
- error:(NSError **)error {
- self = [super init];
- NSAssert(self, @"super init cannot be nil");
- @try {
- _resolutionPreset = FLTGetFLTResolutionPresetForString(resolutionPreset);
- } @catch (NSError *e) {
- *error = e;
- }
- _enableAudio = enableAudio;
- _captureSessionQueue = captureSessionQueue;
- _captureSession = [[AVCaptureSession alloc] init];
- _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName];
- _flashMode = _captureDevice.hasFlash ? FLTFlashModeAuto : FLTFlashModeOff;
- _exposureMode = FLTExposureModeAuto;
- _focusMode = FLTFocusModeAuto;
- _lockedCaptureOrientation = UIDeviceOrientationUnknown;
- _deviceOrientation = orientation;
- _videoFormat = kCVPixelFormatType_32BGRA;
-
- NSError *localError = nil;
- _captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice
- error:&localError];
-
- if (localError) {
- *error = localError;
- return nil;
- }
-
- _captureVideoOutput = [AVCaptureVideoDataOutput new];
- _captureVideoOutput.videoSettings =
- @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat)};
- [_captureVideoOutput setAlwaysDiscardsLateVideoFrames:YES];
- [_captureVideoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
-
- AVCaptureConnection *connection =
- [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports
- output:_captureVideoOutput];
-
- if ([_captureDevice position] == AVCaptureDevicePositionFront) {
- connection.videoMirrored = YES;
- }
-
- [_captureSession addInputWithNoConnections:_captureVideoInput];
- [_captureSession addOutputWithNoConnections:_captureVideoOutput];
- [_captureSession addConnection:connection];
-
- if (@available(iOS 10.0, *)) {
- _capturePhotoOutput = [AVCapturePhotoOutput new];
- [_capturePhotoOutput setHighResolutionCaptureEnabled:YES];
- [_captureSession addOutput:_capturePhotoOutput];
- }
- _motionManager = [[CMMotionManager alloc] init];
- [_motionManager startAccelerometerUpdates];
-
- [self setCaptureSessionPreset:_resolutionPreset];
- [self updateOrientation];
-
- return self;
-}
-
-- (void)start {
- [_captureSession startRunning];
-}
-
-- (void)stop {
- [_captureSession stopRunning];
-}
-
-- (void)setVideoFormat:(OSType)videoFormat {
- _videoFormat = videoFormat;
- _captureVideoOutput.videoSettings =
- @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)};
-}
-
-- (void)setDeviceOrientation:(UIDeviceOrientation)orientation {
- if (_deviceOrientation == orientation) {
- return;
- }
-
- _deviceOrientation = orientation;
- [self updateOrientation];
-}
-
-- (void)updateOrientation {
- if (_isRecording) {
- return;
- }
-
- UIDeviceOrientation orientation = (_lockedCaptureOrientation != UIDeviceOrientationUnknown)
- ? _lockedCaptureOrientation
- : _deviceOrientation;
-
- [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput];
- [self updateOrientation:orientation forCaptureOutput:_captureVideoOutput];
-}
-
-- (void)updateOrientation:(UIDeviceOrientation)orientation
- forCaptureOutput:(AVCaptureOutput *)captureOutput {
- if (!captureOutput) {
- return;
- }
-
- AVCaptureConnection *connection = [captureOutput connectionWithMediaType:AVMediaTypeVideo];
- if (connection && connection.isVideoOrientationSupported) {
- connection.videoOrientation = [self getVideoOrientationForDeviceOrientation:orientation];
- }
-}
-
-- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) {
- AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
- if (_resolutionPreset == FLTResolutionPresetMax) {
- [settings setHighResolutionPhotoEnabled:YES];
- }
-
- AVCaptureFlashMode avFlashMode = FLTGetAVCaptureFlashModeForFLTFlashMode(_flashMode);
- if (avFlashMode != -1) {
- [settings setFlashMode:avFlashMode];
- }
- NSError *error;
- NSString *path = [self getTemporaryFilePathWithExtension:@"jpg"
- subfolder:@"pictures"
- prefix:@"CAP_"
- error:error];
- if (error) {
- [result sendError:error];
- return;
- }
-
- [_capturePhotoOutput capturePhotoWithSettings:settings
- delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path
- result:result]];
-}
-
-- (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
- (UIDeviceOrientation)deviceOrientation {
- if (deviceOrientation == UIDeviceOrientationPortrait) {
- return AVCaptureVideoOrientationPortrait;
- } else if (deviceOrientation == UIDeviceOrientationLandscapeLeft) {
- // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation
- // is landscape left the video orientation should be landscape right.
- return AVCaptureVideoOrientationLandscapeRight;
- } else if (deviceOrientation == UIDeviceOrientationLandscapeRight) {
- // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation
- // is landscape right the video orientation should be landscape left.
- return AVCaptureVideoOrientationLandscapeLeft;
- } else if (deviceOrientation == UIDeviceOrientationPortraitUpsideDown) {
- return AVCaptureVideoOrientationPortraitUpsideDown;
- } else {
- return AVCaptureVideoOrientationPortrait;
- }
-}
-
-- (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]) {
- [[NSFileManager defaultManager] createDirectoryAtPath:fileDir
- withIntermediateDirectories:true
- attributes:nil
- error:&error];
- if (error) {
- return nil;
- }
- }
-
- return file;
-}
-
-- (void)setCaptureSessionPreset:(FLTResolutionPreset)resolutionPreset {
- switch (resolutionPreset) {
- case FLTResolutionPresetMax:
- case FLTResolutionPresetUltraHigh:
- if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) {
- _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
- _previewSize = CGSizeMake(3840, 2160);
- break;
- }
- if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {
- _captureSession.sessionPreset = AVCaptureSessionPresetHigh;
- _previewSize =
- CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width,
- _captureDevice.activeFormat.highResolutionStillImageDimensions.height);
- break;
- }
- case FLTResolutionPresetVeryHigh:
- if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
- _captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
- _previewSize = CGSizeMake(1920, 1080);
- break;
- }
- case FLTResolutionPresetHigh:
- if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
- _captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
- _previewSize = CGSizeMake(1280, 720);
- break;
- }
- case FLTResolutionPresetMedium:
- if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
- _captureSession.sessionPreset = AVCaptureSessionPreset640x480;
- _previewSize = CGSizeMake(640, 480);
- break;
- }
- case FLTResolutionPresetLow:
- if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) {
- _captureSession.sessionPreset = AVCaptureSessionPreset352x288;
- _previewSize = CGSizeMake(352, 288);
- break;
- }
- default:
- if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetLow]) {
- _captureSession.sessionPreset = AVCaptureSessionPresetLow;
- _previewSize = CGSizeMake(352, 288);
- } else {
- NSError *error =
- [NSError errorWithDomain:NSCocoaErrorDomain
- code:NSURLErrorUnknown
- userInfo:@{
- NSLocalizedDescriptionKey :
- @"No capture session available for current capture session."
- }];
- @throw error;
- }
- }
-}
-
-- (void)captureOutput:(AVCaptureOutput *)output
- didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
- fromConnection:(AVCaptureConnection *)connection {
- if (output == _captureVideoOutput) {
- CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
- CFRetain(newBuffer);
- CVPixelBufferRef old = _latestPixelBuffer;
- while (!OSAtomicCompareAndSwapPtrBarrier(old, newBuffer, (void **)&_latestPixelBuffer)) {
- old = _latestPixelBuffer;
- }
- if (old != nil) {
- CFRelease(old);
- }
- if (_onFrameAvailable) {
- _onFrameAvailable();
- }
- }
- if (!CMSampleBufferDataIsReady(sampleBuffer)) {
- [_methodChannel invokeMethod:errorMethod
- arguments:@"sample buffer is not ready. Skipping sample"];
- return;
- }
- if (_isStreamingImages) {
- FlutterEventSink eventSink = _imageStreamHandler.eventSink;
- if (eventSink) {
- CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
- // Must lock base address before accessing the pixel data
- CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
-
- size_t imageWidth = CVPixelBufferGetWidth(pixelBuffer);
- size_t imageHeight = CVPixelBufferGetHeight(pixelBuffer);
-
- NSMutableArray *planes = [NSMutableArray array];
-
- const Boolean isPlanar = CVPixelBufferIsPlanar(pixelBuffer);
- size_t planeCount;
- if (isPlanar) {
- planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
- } else {
- planeCount = 1;
- }
-
- for (int i = 0; i < planeCount; i++) {
- void *planeAddress;
- size_t bytesPerRow;
- size_t height;
- size_t width;
-
- if (isPlanar) {
- planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i);
- bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i);
- height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i);
- width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i);
- } else {
- planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
- bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
- height = CVPixelBufferGetHeight(pixelBuffer);
- width = CVPixelBufferGetWidth(pixelBuffer);
- }
-
- NSNumber *length = @(bytesPerRow * height);
- NSData *bytes = [NSData dataWithBytes:planeAddress length:length.unsignedIntegerValue];
-
- NSMutableDictionary *planeBuffer = [NSMutableDictionary dictionary];
- planeBuffer[@"bytesPerRow"] = @(bytesPerRow);
- planeBuffer[@"width"] = @(width);
- planeBuffer[@"height"] = @(height);
- planeBuffer[@"bytes"] = [FlutterStandardTypedData typedDataWithBytes:bytes];
-
- [planes addObject:planeBuffer];
- }
- // Before accessing pixel data, we should lock the base address, and unlock it afterwards.
- // Done accessing the `pixelBuffer` at this point.
- CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
-
- NSMutableDictionary *imageBuffer = [NSMutableDictionary dictionary];
- imageBuffer[@"width"] = [NSNumber numberWithUnsignedLong:imageWidth];
- imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight];
- imageBuffer[@"format"] = @(_videoFormat);
- imageBuffer[@"planes"] = planes;
- imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]];
- Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]);
- Float64 nsExposureDuration = 1000000000 * exposureDuration;
- imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration];
- imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]];
-
- dispatch_async(dispatch_get_main_queue(), ^{
- eventSink(imageBuffer);
- });
- }
- }
- if (_isRecording && !_isRecordingPaused) {
- if (_videoWriter.status == AVAssetWriterStatusFailed) {
- [_methodChannel invokeMethod:errorMethod
- arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
- return;
- }
-
- CFRetain(sampleBuffer);
- CMTime currentSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
-
- if (_videoWriter.status != AVAssetWriterStatusWriting) {
- [_videoWriter startWriting];
- [_videoWriter startSessionAtSourceTime:currentSampleTime];
- }
-
- if (output == _captureVideoOutput) {
- if (_videoIsDisconnected) {
- _videoIsDisconnected = NO;
-
- if (_videoTimeOffset.value == 0) {
- _videoTimeOffset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime);
- } else {
- CMTime offset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime);
- _videoTimeOffset = CMTimeAdd(_videoTimeOffset, offset);
- }
-
- return;
- }
-
- _lastVideoSampleTime = currentSampleTime;
-
- CVPixelBufferRef nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
- CMTime nextSampleTime = CMTimeSubtract(_lastVideoSampleTime, _videoTimeOffset);
- [_videoAdaptor appendPixelBuffer:nextBuffer withPresentationTime:nextSampleTime];
- } else {
- CMTime dur = CMSampleBufferGetDuration(sampleBuffer);
-
- if (dur.value > 0) {
- currentSampleTime = CMTimeAdd(currentSampleTime, dur);
- }
-
- if (_audioIsDisconnected) {
- _audioIsDisconnected = NO;
-
- if (_audioTimeOffset.value == 0) {
- _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime);
- } else {
- CMTime offset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime);
- _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset);
- }
-
- return;
- }
-
- _lastAudioSampleTime = currentSampleTime;
-
- if (_audioTimeOffset.value != 0) {
- CFRelease(sampleBuffer);
- sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset];
- }
-
- [self newAudioSample:sampleBuffer];
- }
-
- CFRelease(sampleBuffer);
- }
-}
-
-- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset CF_RETURNS_RETAINED {
- CMItemCount count;
- CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count);
- CMSampleTimingInfo *pInfo = malloc(sizeof(CMSampleTimingInfo) * count);
- CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count);
- for (CMItemCount i = 0; i < count; i++) {
- pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset);
- pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset);
- }
- CMSampleBufferRef sout;
- CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout);
- free(pInfo);
- return sout;
-}
-
-- (void)newVideoSample:(CMSampleBufferRef)sampleBuffer {
- if (_videoWriter.status != AVAssetWriterStatusWriting) {
- if (_videoWriter.status == AVAssetWriterStatusFailed) {
- [_methodChannel invokeMethod:errorMethod
- arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
- }
- return;
- }
- if (_videoWriterInput.readyForMoreMediaData) {
- if (![_videoWriterInput appendSampleBuffer:sampleBuffer]) {
- [_methodChannel
- invokeMethod:errorMethod
- arguments:[NSString stringWithFormat:@"%@", @"Unable to write to video input"]];
- }
- }
-}
-
-- (void)newAudioSample:(CMSampleBufferRef)sampleBuffer {
- if (_videoWriter.status != AVAssetWriterStatusWriting) {
- if (_videoWriter.status == AVAssetWriterStatusFailed) {
- [_methodChannel invokeMethod:errorMethod
- arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
- }
- return;
- }
- if (_audioWriterInput.readyForMoreMediaData) {
- if (![_audioWriterInput appendSampleBuffer:sampleBuffer]) {
- [_methodChannel
- invokeMethod:errorMethod
- arguments:[NSString stringWithFormat:@"%@", @"Unable to write to audio input"]];
- }
- }
-}
-
-- (void)close {
- [_captureSession stopRunning];
- for (AVCaptureInput *input in [_captureSession inputs]) {
- [_captureSession removeInput:input];
- }
- for (AVCaptureOutput *output in [_captureSession outputs]) {
- [_captureSession removeOutput:output];
- }
-}
-
-- (void)dealloc {
- if (_latestPixelBuffer) {
- CFRelease(_latestPixelBuffer);
- }
- [_motionManager stopAccelerometerUpdates];
-}
-
-- (CVPixelBufferRef)copyPixelBuffer {
- CVPixelBufferRef pixelBuffer = _latestPixelBuffer;
- while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, nil, (void **)&_latestPixelBuffer)) {
- pixelBuffer = _latestPixelBuffer;
- }
-
- return pixelBuffer;
-}
-
-- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
- if (!_isRecording) {
- NSError *error;
- _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4"
- subfolder:@"videos"
- prefix:@"REC_"
- error:error];
- if (error) {
- [result sendError:error];
- return;
- }
- if (![self setupWriterForPath:_videoRecordingPath]) {
- [result sendErrorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil];
- return;
- }
- _isRecording = YES;
- _isRecordingPaused = NO;
- _videoTimeOffset = CMTimeMake(0, 1);
- _audioTimeOffset = CMTimeMake(0, 1);
- _videoIsDisconnected = NO;
- _audioIsDisconnected = NO;
- [result sendSuccess];
- } else {
- [result sendErrorWithCode:@"Error" message:@"Video is already recording" details:nil];
- }
-}
-
-- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
- if (_isRecording) {
- _isRecording = NO;
-
- if (_videoWriter.status != AVAssetWriterStatusUnknown) {
- [_videoWriter finishWritingWithCompletionHandler:^{
- if (self->_videoWriter.status == AVAssetWriterStatusCompleted) {
- [self updateOrientation];
- [result sendSuccessWithData:self->_videoRecordingPath];
- self->_videoRecordingPath = nil;
- } else {
- [result sendErrorWithCode:@"IOError"
- message:@"AVAssetWriter could not finish writing!"
- details:nil];
- }
- }];
- }
- } else {
- NSError *error =
- [NSError errorWithDomain:NSCocoaErrorDomain
- code:NSURLErrorResourceUnavailable
- userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}];
- [result sendError:error];
- }
-}
-
-- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
- _isRecordingPaused = YES;
- _videoIsDisconnected = YES;
- _audioIsDisconnected = YES;
- [result sendSuccess];
-}
-
-- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
- _isRecordingPaused = NO;
- [result sendSuccess];
-}
-
-- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result
- orientation:(NSString *)orientationStr {
- UIDeviceOrientation orientation;
- @try {
- orientation = FLTGetUIDeviceOrientationForString(orientationStr);
- } @catch (NSError *e) {
- [result sendError:e];
- return;
- }
-
- if (_lockedCaptureOrientation != orientation) {
- _lockedCaptureOrientation = orientation;
- [self updateOrientation];
- }
-
- [result sendSuccess];
-}
-
-- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result {
- _lockedCaptureOrientation = UIDeviceOrientationUnknown;
- [self updateOrientation];
- [result sendSuccess];
-}
-
-- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
- FLTFlashMode mode;
- @try {
- mode = FLTGetFLTFlashModeForString(modeStr);
- } @catch (NSError *e) {
- [result sendError:e];
- return;
- }
- if (mode == FLTFlashModeTorch) {
- if (!_captureDevice.hasTorch) {
- [result sendErrorWithCode:@"setFlashModeFailed"
- message:@"Device does not support torch mode"
- details:nil];
- return;
- }
- if (!_captureDevice.isTorchAvailable) {
- [result sendErrorWithCode:@"setFlashModeFailed"
- message:@"Torch mode is currently not available"
- details:nil];
- return;
- }
- if (_captureDevice.torchMode != AVCaptureTorchModeOn) {
- [_captureDevice lockForConfiguration:nil];
- [_captureDevice setTorchMode:AVCaptureTorchModeOn];
- [_captureDevice unlockForConfiguration];
- }
- } else {
- if (!_captureDevice.hasFlash) {
- [result sendErrorWithCode:@"setFlashModeFailed"
- message:@"Device does not have flash capabilities"
- details:nil];
- return;
- }
- AVCaptureFlashMode avFlashMode = FLTGetAVCaptureFlashModeForFLTFlashMode(mode);
- if (![_capturePhotoOutput.supportedFlashModes
- containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) {
- [result sendErrorWithCode:@"setFlashModeFailed"
- message:@"Device does not support this specific flash mode"
- details:nil];
- return;
- }
- if (_captureDevice.torchMode != AVCaptureTorchModeOff) {
- [_captureDevice lockForConfiguration:nil];
- [_captureDevice setTorchMode:AVCaptureTorchModeOff];
- [_captureDevice unlockForConfiguration];
- }
- }
- _flashMode = mode;
- [result sendSuccess];
-}
-
-- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
- FLTExposureMode mode;
- @try {
- mode = FLTGetFLTExposureModeForString(modeStr);
- } @catch (NSError *e) {
- [result sendError:e];
- return;
- }
- _exposureMode = mode;
- [self applyExposureMode];
- [result sendSuccess];
-}
-
-- (void)applyExposureMode {
- [_captureDevice lockForConfiguration:nil];
- switch (_exposureMode) {
- case FLTExposureModeLocked:
- [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
- break;
- case FLTExposureModeAuto:
- if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
- [_captureDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
- } else {
- [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
- }
- break;
- }
- [_captureDevice unlockForConfiguration];
-}
-
-- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
- FLTFocusMode mode;
- @try {
- mode = FLTGetFLTFocusModeForString(modeStr);
- } @catch (NSError *e) {
- [result sendError:e];
- return;
- }
- _focusMode = mode;
- [self applyFocusMode];
- [result sendSuccess];
-}
-
-- (void)applyFocusMode {
- [self applyFocusMode:_focusMode onDevice:_captureDevice];
-}
-
-/**
- * Applies FocusMode on the AVCaptureDevice.
- *
- * If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use
- * AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to
- * AVCaptureFocusModeAutoFocus. If neither AVCaptureFocusModeContinuousModeAutoFocus nor
- * AVCaptureFocusModeAutoFocus are supported focus mode will not be set.
- * If @c focusMode is set to FocusModeLocked the AVCaptureDevice is configured to use
- * AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not
- * be set.
- *
- * @param focusMode The focus mode that should be applied to the @captureDevice instance.
- * @param captureDevice The AVCaptureDevice to which the @focusMode will be applied.
- */
-- (void)applyFocusMode:(FLTFocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice {
- [captureDevice lockForConfiguration:nil];
- switch (focusMode) {
- case FLTFocusModeLocked:
- if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
- [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
- }
- break;
- case FLTFocusModeAuto:
- if ([captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
- [captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
- } else if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
- [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
- }
- break;
- }
- [captureDevice unlockForConfiguration];
-}
-
-- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result {
- _isPreviewPaused = true;
- [result sendSuccess];
-}
-
-- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result {
- _isPreviewPaused = false;
- [result sendSuccess];
-}
-
-- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation
- x:(double)x
- y:(double)y {
- double oldX = x, oldY = y;
- switch (orientation) {
- case UIDeviceOrientationPortrait: // 90 ccw
- y = 1 - oldX;
- x = oldY;
- break;
- case UIDeviceOrientationPortraitUpsideDown: // 90 cw
- x = 1 - oldY;
- y = oldX;
- break;
- case UIDeviceOrientationLandscapeRight: // 180
- x = 1 - x;
- y = 1 - y;
- break;
- case UIDeviceOrientationLandscapeLeft:
- default:
- // No rotation required
- break;
- }
- return CGPointMake(x, y);
-}
-
-- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y {
- if (!_captureDevice.isExposurePointOfInterestSupported) {
- [result sendErrorWithCode:@"setExposurePointFailed"
- message:@"Device does not have exposure point capabilities"
- details:nil];
- return;
- }
- UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
- [_captureDevice lockForConfiguration:nil];
- [_captureDevice setExposurePointOfInterest:[self getCGPointForCoordsWithOrientation:orientation
- x:x
- y:y]];
- [_captureDevice unlockForConfiguration];
- // Retrigger auto exposure
- [self applyExposureMode];
- [result sendSuccess];
-}
-
-- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y {
- if (!_captureDevice.isFocusPointOfInterestSupported) {
- [result sendErrorWithCode:@"setFocusPointFailed"
- message:@"Device does not have focus point capabilities"
- details:nil];
- return;
- }
- UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
- [_captureDevice lockForConfiguration:nil];
-
- [_captureDevice setFocusPointOfInterest:[self getCGPointForCoordsWithOrientation:orientation
- x:x
- y:y]];
- [_captureDevice unlockForConfiguration];
- // Retrigger auto focus
- [self applyFocusMode];
- [result sendSuccess];
-}
-
-- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset {
- [_captureDevice lockForConfiguration:nil];
- [_captureDevice setExposureTargetBias:offset completionHandler:nil];
- [_captureDevice unlockForConfiguration];
- [result sendSuccessWithData:@(offset)];
-}
-
-- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger {
- if (!_isStreamingImages) {
- FlutterEventChannel *eventChannel =
- [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream"
- binaryMessenger:messenger];
- FLTThreadSafeEventChannel *threadSafeEventChannel =
- [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel];
-
- _imageStreamHandler =
- [[FLTImageStreamHandler alloc] initWithCaptureSessionQueue:_captureSessionQueue];
- [threadSafeEventChannel setStreamHandler:_imageStreamHandler
- completion:^{
- dispatch_async(self->_captureSessionQueue, ^{
- self.isStreamingImages = YES;
- });
- }];
- } else {
- [_methodChannel invokeMethod:errorMethod
- arguments:@"Images from camera are already streaming!"];
- }
-}
-
-- (void)stopImageStream {
- if (_isStreamingImages) {
- _isStreamingImages = NO;
- _imageStreamHandler = nil;
- } else {
- [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are not streaming!"];
- }
-}
-
-- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result {
- CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor];
-
- [result sendSuccessWithData:[NSNumber numberWithFloat:maxZoomFactor]];
-}
-
-- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result {
- CGFloat minZoomFactor = [self getMinAvailableZoomFactor];
- [result sendSuccessWithData:[NSNumber numberWithFloat:minZoomFactor]];
-}
-
-- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result {
- CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor];
- CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor];
-
- if (maxAvailableZoomFactor < zoom || minAvailableZoomFactor > zoom) {
- NSString *errorMessage = [NSString
- stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).",
- minAvailableZoomFactor, maxAvailableZoomFactor];
-
- [result sendErrorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil];
- return;
- }
-
- NSError *error = nil;
- if (![_captureDevice lockForConfiguration:&error]) {
- [result sendError:error];
- return;
- }
- _captureDevice.videoZoomFactor = zoom;
- [_captureDevice unlockForConfiguration];
-
- [result sendSuccess];
-}
-
-- (CGFloat)getMinAvailableZoomFactor {
- if (@available(iOS 11.0, *)) {
- return _captureDevice.minAvailableVideoZoomFactor;
- } else {
- return 1.0;
- }
-}
-
-- (CGFloat)getMaxAvailableZoomFactor {
- if (@available(iOS 11.0, *)) {
- return _captureDevice.maxAvailableVideoZoomFactor;
- } else {
- return _captureDevice.activeFormat.videoMaxZoomFactor;
- }
-}
-
-- (BOOL)setupWriterForPath:(NSString *)path {
- NSError *error = nil;
- NSURL *outputURL;
- if (path != nil) {
- outputURL = [NSURL fileURLWithPath:path];
- } else {
- return NO;
- }
- if (_enableAudio && !_isAudioSetup) {
- [self setUpCaptureSessionForAudio];
- }
-
- _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL
- fileType:AVFileTypeMPEG4
- error:&error];
- NSParameterAssert(_videoWriter);
- if (error) {
- [_methodChannel invokeMethod:errorMethod arguments:error.description];
- return NO;
- }
-
- NSDictionary *videoSettings = [_captureVideoOutput
- recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4];
- _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
- outputSettings:videoSettings];
-
- _videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor
- assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput
- sourcePixelBufferAttributes:@{
- (NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat)
- }];
-
- NSParameterAssert(_videoWriterInput);
-
- _videoWriterInput.expectsMediaDataInRealTime = YES;
-
- // Add the audio input
- if (_enableAudio) {
- AudioChannelLayout acl;
- bzero(&acl, sizeof(acl));
- acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
- NSDictionary *audioOutputSettings = nil;
- // Both type of audio inputs causes output video file to be corrupted.
- audioOutputSettings = @{
- AVFormatIDKey : [NSNumber numberWithInt:kAudioFormatMPEG4AAC],
- AVSampleRateKey : [NSNumber numberWithFloat:44100.0],
- AVNumberOfChannelsKey : [NSNumber numberWithInt:1],
- AVChannelLayoutKey : [NSData dataWithBytes:&acl length:sizeof(acl)],
- };
- _audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
- outputSettings:audioOutputSettings];
- _audioWriterInput.expectsMediaDataInRealTime = YES;
-
- [_videoWriter addInput:_audioWriterInput];
- [_audioOutput setSampleBufferDelegate:self queue:_captureSessionQueue];
- }
-
- if (_flashMode == FLTFlashModeTorch) {
- [self.captureDevice lockForConfiguration:nil];
- [self.captureDevice setTorchMode:AVCaptureTorchModeOn];
- [self.captureDevice unlockForConfiguration];
- }
-
- [_videoWriter addInput:_videoWriterInput];
-
- [_captureVideoOutput setSampleBufferDelegate:self queue:_captureSessionQueue];
-
- return YES;
-}
-
-- (void)setUpCaptureSessionForAudio {
- NSError *error = nil;
- // Create a device input with the device and add it to the session.
- // Setup the audio input.
- AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
- AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice
- error:&error];
- if (error) {
- [_methodChannel invokeMethod:errorMethod arguments:error.description];
- }
- // Setup the audio output.
- _audioOutput = [[AVCaptureAudioDataOutput alloc] init];
-
- if ([_captureSession canAddInput:audioInput]) {
- [_captureSession addInput:audioInput];
-
- if ([_captureSession canAddOutput:_audioOutput]) {
- [_captureSession addOutput:_audioOutput];
- _isAudioSetup = YES;
- } else {
- [_methodChannel invokeMethod:errorMethod
- arguments:@"Unable to add Audio input/output to session capture"];
- _isAudioSetup = NO;
- }
- }
-}
-@end
-
@interface CameraPlugin ()
@property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry;
@property(readonly, nonatomic) NSObject<FlutterBinaryMessenger> *messenger;
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
index 0593741..1ad3437 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
@@ -7,5 +7,6 @@
explicit module Test {
header "CameraPlugin_Test.h"
header "CameraProperties.h"
+ header "FLTCam.h"
}
}
diff --git a/packages/camera/camera/ios/Classes/FLTCam.h b/packages/camera/camera/ios/Classes/FLTCam.h
new file mode 100644
index 0000000..417a1d7
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTCam.h
@@ -0,0 +1,86 @@
+// 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 AVFoundation;
+@import Foundation;
+@import Flutter;
+
+#import "CameraProperties.h"
+#import "FLTThreadSafeEventChannel.h"
+#import "FLTThreadSafeFlutterResult.h"
+#import "FLTThreadSafeMethodChannel.h"
+#import "FLTThreadSafeTextureRegistry.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A class that manages camera's state and performs camera operations.
+ */
+@interface FLTCam : NSObject <FlutterTexture>
+
+@property(readonly, nonatomic) AVCaptureDevice *captureDevice;
+@property(readonly, nonatomic) CGSize previewSize;
+@property(assign, nonatomic) BOOL isPreviewPaused;
+@property(nonatomic, copy) void (^onFrameAvailable)(void);
+@property(nonatomic) FLTThreadSafeMethodChannel *methodChannel;
+@property(assign, nonatomic) FLTResolutionPreset resolutionPreset;
+@property(assign, nonatomic) FLTExposureMode exposureMode;
+@property(assign, nonatomic) FLTFocusMode focusMode;
+@property(assign, nonatomic) FLTFlashMode flashMode;
+// Format used for video and image streaming.
+@property(assign, nonatomic) FourCharCode videoFormat;
+
+- (instancetype)initWithCameraName:(NSString *)cameraName
+ resolutionPreset:(NSString *)resolutionPreset
+ enableAudio:(BOOL)enableAudio
+ orientation:(UIDeviceOrientation)orientation
+ captureSessionQueue:(dispatch_queue_t)captureSessionQueue
+ error:(NSError **)error;
+- (void)start;
+- (void)stop;
+- (void)setDeviceOrientation:(UIDeviceOrientation)orientation;
+- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10));
+- (void)close;
+- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result
+ orientation:(NSString *)orientationStr;
+- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr;
+- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr;
+- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr;
+- (void)applyFocusMode;
+
+/**
+ * Applies FocusMode on the AVCaptureDevice.
+ *
+ * If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use
+ * AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to
+ * AVCaptureFocusModeAutoFocus. If neither AVCaptureFocusModeContinuousModeAutoFocus nor
+ * AVCaptureFocusModeAutoFocus are supported focus mode will not be set.
+ * If @c focusMode is set to FocusModeLocked the AVCaptureDevice is configured to use
+ * AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not
+ * be set.
+ *
+ * @param focusMode The focus mode that should be applied to the @captureDevice instance.
+ * @param captureDevice The AVCaptureDevice to which the @focusMode will be applied.
+ */
+- (void)applyFocusMode:(FLTFocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice;
+- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y;
+- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y;
+- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset;
+- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger;
+- (void)stopImageStream;
+- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result;
+- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result;
+- (void)setUpCaptureSessionForAudio;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m
new file mode 100644
index 0000000..11be4ad
--- /dev/null
+++ b/packages/camera/camera/ios/Classes/FLTCam.m
@@ -0,0 +1,1087 @@
+// 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 "FLTCam.h"
+
+@import CoreMotion;
+#import <libkern/OSAtomic.h>
+
+@interface FLTImageStreamHandler : NSObject <FlutterStreamHandler>
+// The queue on which `eventSink` property should be accessed
+@property(nonatomic, strong) dispatch_queue_t captureSessionQueue;
+// `eventSink` property should be accessed on `captureSessionQueue`.
+// The block itself should be invoked on the main queue.
+@property FlutterEventSink eventSink;
+@end
+
+@implementation FLTImageStreamHandler
+
+- (instancetype)initWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue {
+ self = [super init];
+ NSAssert(self, @"super init cannot be nil");
+ _captureSessionQueue = captureSessionQueue;
+ return self;
+}
+
+- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments {
+ dispatch_async(self.captureSessionQueue, ^{
+ self.eventSink = nil;
+ });
+ return nil;
+}
+
+- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
+ eventSink:(nonnull FlutterEventSink)events {
+ dispatch_async(self.captureSessionQueue, ^{
+ self.eventSink = events;
+ });
+ return nil;
+}
+@end
+
+@interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate>
+@property(readonly, nonatomic) NSString *path;
+@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result;
+@end
+
+@implementation FLTSavePhotoDelegate {
+ /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer.
+ FLTSavePhotoDelegate *selfReference;
+}
+
+- initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result {
+ self = [super init];
+ NSAssert(self, @"super init cannot be nil");
+ _path = path;
+ selfReference = self;
+ _result = result;
+ return self;
+}
+
+- (void)captureOutput:(AVCapturePhotoOutput *)output
+ didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer
+ previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer
+ resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
+ bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings
+ error:(NSError *)error API_AVAILABLE(ios(10)) {
+ selfReference = nil;
+ if (error) {
+ [_result sendError:error];
+ return;
+ }
+
+ NSData *data = [AVCapturePhotoOutput
+ JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer
+ previewPhotoSampleBuffer:previewPhotoSampleBuffer];
+
+ // TODO(sigurdm): Consider writing file asynchronously.
+ bool success = [data writeToFile:_path atomically:YES];
+
+ if (!success) {
+ [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil];
+ return;
+ }
+ [_result sendSuccessWithData:_path];
+}
+
+- (void)captureOutput:(AVCapturePhotoOutput *)output
+ didFinishProcessingPhoto:(AVCapturePhoto *)photo
+ error:(NSError *)error API_AVAILABLE(ios(11.0)) {
+ selfReference = nil;
+ if (error) {
+ [_result sendError:error];
+ return;
+ }
+
+ NSData *photoData = [photo fileDataRepresentation];
+
+ bool success = [photoData writeToFile:_path atomically:YES];
+ if (!success) {
+ [_result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil];
+ return;
+ }
+ [_result sendSuccessWithData:_path];
+}
+@end
+
+@interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate,
+ AVCaptureAudioDataOutputSampleBufferDelegate>
+
+@property(readonly, nonatomic) int64_t textureId;
+@property BOOL enableAudio;
+@property(nonatomic) FLTImageStreamHandler *imageStreamHandler;
+@property(readonly, nonatomic) AVCaptureSession *captureSession;
+
+@property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10));
+@property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput;
+@property(readonly, nonatomic) AVCaptureInput *captureVideoInput;
+@property(readonly) CVPixelBufferRef volatile latestPixelBuffer;
+@property(readonly, nonatomic) CGSize captureSize;
+@property(strong, nonatomic) AVAssetWriter *videoWriter;
+@property(strong, nonatomic) AVAssetWriterInput *videoWriterInput;
+@property(strong, nonatomic) AVAssetWriterInput *audioWriterInput;
+@property(strong, nonatomic) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor;
+@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
+@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput;
+@property(strong, nonatomic) NSString *videoRecordingPath;
+@property(assign, nonatomic) BOOL isRecording;
+@property(assign, nonatomic) BOOL isRecordingPaused;
+@property(assign, nonatomic) BOOL videoIsDisconnected;
+@property(assign, nonatomic) BOOL audioIsDisconnected;
+@property(assign, nonatomic) BOOL isAudioSetup;
+@property(assign, nonatomic) BOOL isStreamingImages;
+@property(assign, nonatomic) UIDeviceOrientation lockedCaptureOrientation;
+@property(assign, nonatomic) CMTime lastVideoSampleTime;
+@property(assign, nonatomic) CMTime lastAudioSampleTime;
+@property(assign, nonatomic) CMTime videoTimeOffset;
+@property(assign, nonatomic) CMTime audioTimeOffset;
+@property(nonatomic) CMMotionManager *motionManager;
+@property AVAssetWriterInputPixelBufferAdaptor *videoAdaptor;
+// All FLTCam's state access and capture session related operations should be on run on this queue.
+@property(strong, nonatomic) dispatch_queue_t captureSessionQueue;
+@property(assign, nonatomic) UIDeviceOrientation deviceOrientation;
+@end
+
+@implementation FLTCam
+
+NSString *const errorMethod = @"error";
+
+- (instancetype)initWithCameraName:(NSString *)cameraName
+ resolutionPreset:(NSString *)resolutionPreset
+ enableAudio:(BOOL)enableAudio
+ orientation:(UIDeviceOrientation)orientation
+ captureSessionQueue:(dispatch_queue_t)captureSessionQueue
+ error:(NSError **)error {
+ self = [super init];
+ NSAssert(self, @"super init cannot be nil");
+ @try {
+ _resolutionPreset = FLTGetFLTResolutionPresetForString(resolutionPreset);
+ } @catch (NSError *e) {
+ *error = e;
+ }
+ _enableAudio = enableAudio;
+ _captureSessionQueue = captureSessionQueue;
+ _captureSession = [[AVCaptureSession alloc] init];
+ _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName];
+ _flashMode = _captureDevice.hasFlash ? FLTFlashModeAuto : FLTFlashModeOff;
+ _exposureMode = FLTExposureModeAuto;
+ _focusMode = FLTFocusModeAuto;
+ _lockedCaptureOrientation = UIDeviceOrientationUnknown;
+ _deviceOrientation = orientation;
+ _videoFormat = kCVPixelFormatType_32BGRA;
+
+ NSError *localError = nil;
+ _captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice
+ error:&localError];
+
+ if (localError) {
+ *error = localError;
+ return nil;
+ }
+
+ _captureVideoOutput = [AVCaptureVideoDataOutput new];
+ _captureVideoOutput.videoSettings =
+ @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat)};
+ [_captureVideoOutput setAlwaysDiscardsLateVideoFrames:YES];
+ [_captureVideoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
+
+ AVCaptureConnection *connection =
+ [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports
+ output:_captureVideoOutput];
+
+ if ([_captureDevice position] == AVCaptureDevicePositionFront) {
+ connection.videoMirrored = YES;
+ }
+
+ [_captureSession addInputWithNoConnections:_captureVideoInput];
+ [_captureSession addOutputWithNoConnections:_captureVideoOutput];
+ [_captureSession addConnection:connection];
+
+ if (@available(iOS 10.0, *)) {
+ _capturePhotoOutput = [AVCapturePhotoOutput new];
+ [_capturePhotoOutput setHighResolutionCaptureEnabled:YES];
+ [_captureSession addOutput:_capturePhotoOutput];
+ }
+ _motionManager = [[CMMotionManager alloc] init];
+ [_motionManager startAccelerometerUpdates];
+
+ [self setCaptureSessionPreset:_resolutionPreset];
+ [self updateOrientation];
+
+ return self;
+}
+
+- (void)start {
+ [_captureSession startRunning];
+}
+
+- (void)stop {
+ [_captureSession stopRunning];
+}
+
+- (void)setVideoFormat:(OSType)videoFormat {
+ _videoFormat = videoFormat;
+ _captureVideoOutput.videoSettings =
+ @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)};
+}
+
+- (void)setDeviceOrientation:(UIDeviceOrientation)orientation {
+ if (_deviceOrientation == orientation) {
+ return;
+ }
+
+ _deviceOrientation = orientation;
+ [self updateOrientation];
+}
+
+- (void)updateOrientation {
+ if (_isRecording) {
+ return;
+ }
+
+ UIDeviceOrientation orientation = (_lockedCaptureOrientation != UIDeviceOrientationUnknown)
+ ? _lockedCaptureOrientation
+ : _deviceOrientation;
+
+ [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput];
+ [self updateOrientation:orientation forCaptureOutput:_captureVideoOutput];
+}
+
+- (void)updateOrientation:(UIDeviceOrientation)orientation
+ forCaptureOutput:(AVCaptureOutput *)captureOutput {
+ if (!captureOutput) {
+ return;
+ }
+
+ AVCaptureConnection *connection = [captureOutput connectionWithMediaType:AVMediaTypeVideo];
+ if (connection && connection.isVideoOrientationSupported) {
+ connection.videoOrientation = [self getVideoOrientationForDeviceOrientation:orientation];
+ }
+}
+
+- (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)) {
+ AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings];
+ if (_resolutionPreset == FLTResolutionPresetMax) {
+ [settings setHighResolutionPhotoEnabled:YES];
+ }
+
+ AVCaptureFlashMode avFlashMode = FLTGetAVCaptureFlashModeForFLTFlashMode(_flashMode);
+ if (avFlashMode != -1) {
+ [settings setFlashMode:avFlashMode];
+ }
+ NSError *error;
+ NSString *path = [self getTemporaryFilePathWithExtension:@"jpg"
+ subfolder:@"pictures"
+ prefix:@"CAP_"
+ error:error];
+ if (error) {
+ [result sendError:error];
+ return;
+ }
+
+ [_capturePhotoOutput capturePhotoWithSettings:settings
+ delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path
+ result:result]];
+}
+
+- (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation:
+ (UIDeviceOrientation)deviceOrientation {
+ if (deviceOrientation == UIDeviceOrientationPortrait) {
+ return AVCaptureVideoOrientationPortrait;
+ } else if (deviceOrientation == UIDeviceOrientationLandscapeLeft) {
+ // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation
+ // is landscape left the video orientation should be landscape right.
+ return AVCaptureVideoOrientationLandscapeRight;
+ } else if (deviceOrientation == UIDeviceOrientationLandscapeRight) {
+ // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation
+ // is landscape right the video orientation should be landscape left.
+ return AVCaptureVideoOrientationLandscapeLeft;
+ } else if (deviceOrientation == UIDeviceOrientationPortraitUpsideDown) {
+ return AVCaptureVideoOrientationPortraitUpsideDown;
+ } else {
+ return AVCaptureVideoOrientationPortrait;
+ }
+}
+
+- (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]) {
+ [[NSFileManager defaultManager] createDirectoryAtPath:fileDir
+ withIntermediateDirectories:true
+ attributes:nil
+ error:&error];
+ if (error) {
+ return nil;
+ }
+ }
+
+ return file;
+}
+
+- (void)setCaptureSessionPreset:(FLTResolutionPreset)resolutionPreset {
+ switch (resolutionPreset) {
+ case FLTResolutionPresetMax:
+ case FLTResolutionPresetUltraHigh:
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) {
+ _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160;
+ _previewSize = CGSizeMake(3840, 2160);
+ break;
+ }
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {
+ _captureSession.sessionPreset = AVCaptureSessionPresetHigh;
+ _previewSize =
+ CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width,
+ _captureDevice.activeFormat.highResolutionStillImageDimensions.height);
+ break;
+ }
+ case FLTResolutionPresetVeryHigh:
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
+ _captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
+ _previewSize = CGSizeMake(1920, 1080);
+ break;
+ }
+ case FLTResolutionPresetHigh:
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
+ _captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
+ _previewSize = CGSizeMake(1280, 720);
+ break;
+ }
+ case FLTResolutionPresetMedium:
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
+ _captureSession.sessionPreset = AVCaptureSessionPreset640x480;
+ _previewSize = CGSizeMake(640, 480);
+ break;
+ }
+ case FLTResolutionPresetLow:
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) {
+ _captureSession.sessionPreset = AVCaptureSessionPreset352x288;
+ _previewSize = CGSizeMake(352, 288);
+ break;
+ }
+ default:
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetLow]) {
+ _captureSession.sessionPreset = AVCaptureSessionPresetLow;
+ _previewSize = CGSizeMake(352, 288);
+ } else {
+ NSError *error =
+ [NSError errorWithDomain:NSCocoaErrorDomain
+ code:NSURLErrorUnknown
+ userInfo:@{
+ NSLocalizedDescriptionKey :
+ @"No capture session available for current capture session."
+ }];
+ @throw error;
+ }
+ }
+}
+
+- (void)captureOutput:(AVCaptureOutput *)output
+ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
+ fromConnection:(AVCaptureConnection *)connection {
+ if (output == _captureVideoOutput) {
+ CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
+ CFRetain(newBuffer);
+ CVPixelBufferRef old = _latestPixelBuffer;
+ while (!OSAtomicCompareAndSwapPtrBarrier(old, newBuffer, (void **)&_latestPixelBuffer)) {
+ old = _latestPixelBuffer;
+ }
+ if (old != nil) {
+ CFRelease(old);
+ }
+ if (_onFrameAvailable) {
+ _onFrameAvailable();
+ }
+ }
+ if (!CMSampleBufferDataIsReady(sampleBuffer)) {
+ [_methodChannel invokeMethod:errorMethod
+ arguments:@"sample buffer is not ready. Skipping sample"];
+ return;
+ }
+ if (_isStreamingImages) {
+ FlutterEventSink eventSink = _imageStreamHandler.eventSink;
+ if (eventSink) {
+ CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
+ // Must lock base address before accessing the pixel data
+ CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
+
+ size_t imageWidth = CVPixelBufferGetWidth(pixelBuffer);
+ size_t imageHeight = CVPixelBufferGetHeight(pixelBuffer);
+
+ NSMutableArray *planes = [NSMutableArray array];
+
+ const Boolean isPlanar = CVPixelBufferIsPlanar(pixelBuffer);
+ size_t planeCount;
+ if (isPlanar) {
+ planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
+ } else {
+ planeCount = 1;
+ }
+
+ for (int i = 0; i < planeCount; i++) {
+ void *planeAddress;
+ size_t bytesPerRow;
+ size_t height;
+ size_t width;
+
+ if (isPlanar) {
+ planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i);
+ bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i);
+ height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i);
+ width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i);
+ } else {
+ planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer);
+ bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
+ height = CVPixelBufferGetHeight(pixelBuffer);
+ width = CVPixelBufferGetWidth(pixelBuffer);
+ }
+
+ NSNumber *length = @(bytesPerRow * height);
+ NSData *bytes = [NSData dataWithBytes:planeAddress length:length.unsignedIntegerValue];
+
+ NSMutableDictionary *planeBuffer = [NSMutableDictionary dictionary];
+ planeBuffer[@"bytesPerRow"] = @(bytesPerRow);
+ planeBuffer[@"width"] = @(width);
+ planeBuffer[@"height"] = @(height);
+ planeBuffer[@"bytes"] = [FlutterStandardTypedData typedDataWithBytes:bytes];
+
+ [planes addObject:planeBuffer];
+ }
+ // Before accessing pixel data, we should lock the base address, and unlock it afterwards.
+ // Done accessing the `pixelBuffer` at this point.
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
+
+ NSMutableDictionary *imageBuffer = [NSMutableDictionary dictionary];
+ imageBuffer[@"width"] = [NSNumber numberWithUnsignedLong:imageWidth];
+ imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight];
+ imageBuffer[@"format"] = @(_videoFormat);
+ imageBuffer[@"planes"] = planes;
+ imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]];
+ Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]);
+ Float64 nsExposureDuration = 1000000000 * exposureDuration;
+ imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration];
+ imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]];
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ eventSink(imageBuffer);
+ });
+ }
+ }
+ if (_isRecording && !_isRecordingPaused) {
+ if (_videoWriter.status == AVAssetWriterStatusFailed) {
+ [_methodChannel invokeMethod:errorMethod
+ arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
+ return;
+ }
+
+ CFRetain(sampleBuffer);
+ CMTime currentSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
+
+ if (_videoWriter.status != AVAssetWriterStatusWriting) {
+ [_videoWriter startWriting];
+ [_videoWriter startSessionAtSourceTime:currentSampleTime];
+ }
+
+ if (output == _captureVideoOutput) {
+ if (_videoIsDisconnected) {
+ _videoIsDisconnected = NO;
+
+ if (_videoTimeOffset.value == 0) {
+ _videoTimeOffset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime);
+ } else {
+ CMTime offset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime);
+ _videoTimeOffset = CMTimeAdd(_videoTimeOffset, offset);
+ }
+
+ return;
+ }
+
+ _lastVideoSampleTime = currentSampleTime;
+
+ CVPixelBufferRef nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
+ CMTime nextSampleTime = CMTimeSubtract(_lastVideoSampleTime, _videoTimeOffset);
+ [_videoAdaptor appendPixelBuffer:nextBuffer withPresentationTime:nextSampleTime];
+ } else {
+ CMTime dur = CMSampleBufferGetDuration(sampleBuffer);
+
+ if (dur.value > 0) {
+ currentSampleTime = CMTimeAdd(currentSampleTime, dur);
+ }
+
+ if (_audioIsDisconnected) {
+ _audioIsDisconnected = NO;
+
+ if (_audioTimeOffset.value == 0) {
+ _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime);
+ } else {
+ CMTime offset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime);
+ _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset);
+ }
+
+ return;
+ }
+
+ _lastAudioSampleTime = currentSampleTime;
+
+ if (_audioTimeOffset.value != 0) {
+ CFRelease(sampleBuffer);
+ sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset];
+ }
+
+ [self newAudioSample:sampleBuffer];
+ }
+
+ CFRelease(sampleBuffer);
+ }
+}
+
+- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset CF_RETURNS_RETAINED {
+ CMItemCount count;
+ CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count);
+ CMSampleTimingInfo *pInfo = malloc(sizeof(CMSampleTimingInfo) * count);
+ CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count);
+ for (CMItemCount i = 0; i < count; i++) {
+ pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset);
+ pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset);
+ }
+ CMSampleBufferRef sout;
+ CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout);
+ free(pInfo);
+ return sout;
+}
+
+- (void)newVideoSample:(CMSampleBufferRef)sampleBuffer {
+ if (_videoWriter.status != AVAssetWriterStatusWriting) {
+ if (_videoWriter.status == AVAssetWriterStatusFailed) {
+ [_methodChannel invokeMethod:errorMethod
+ arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
+ }
+ return;
+ }
+ if (_videoWriterInput.readyForMoreMediaData) {
+ if (![_videoWriterInput appendSampleBuffer:sampleBuffer]) {
+ [_methodChannel
+ invokeMethod:errorMethod
+ arguments:[NSString stringWithFormat:@"%@", @"Unable to write to video input"]];
+ }
+ }
+}
+
+- (void)newAudioSample:(CMSampleBufferRef)sampleBuffer {
+ if (_videoWriter.status != AVAssetWriterStatusWriting) {
+ if (_videoWriter.status == AVAssetWriterStatusFailed) {
+ [_methodChannel invokeMethod:errorMethod
+ arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]];
+ }
+ return;
+ }
+ if (_audioWriterInput.readyForMoreMediaData) {
+ if (![_audioWriterInput appendSampleBuffer:sampleBuffer]) {
+ [_methodChannel
+ invokeMethod:errorMethod
+ arguments:[NSString stringWithFormat:@"%@", @"Unable to write to audio input"]];
+ }
+ }
+}
+
+- (void)close {
+ [_captureSession stopRunning];
+ for (AVCaptureInput *input in [_captureSession inputs]) {
+ [_captureSession removeInput:input];
+ }
+ for (AVCaptureOutput *output in [_captureSession outputs]) {
+ [_captureSession removeOutput:output];
+ }
+}
+
+- (void)dealloc {
+ if (_latestPixelBuffer) {
+ CFRelease(_latestPixelBuffer);
+ }
+ [_motionManager stopAccelerometerUpdates];
+}
+
+- (CVPixelBufferRef)copyPixelBuffer {
+ CVPixelBufferRef pixelBuffer = _latestPixelBuffer;
+ while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, nil, (void **)&_latestPixelBuffer)) {
+ pixelBuffer = _latestPixelBuffer;
+ }
+
+ return pixelBuffer;
+}
+
+- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
+ if (!_isRecording) {
+ NSError *error;
+ _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4"
+ subfolder:@"videos"
+ prefix:@"REC_"
+ error:error];
+ if (error) {
+ [result sendError:error];
+ return;
+ }
+ if (![self setupWriterForPath:_videoRecordingPath]) {
+ [result sendErrorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil];
+ return;
+ }
+ _isRecording = YES;
+ _isRecordingPaused = NO;
+ _videoTimeOffset = CMTimeMake(0, 1);
+ _audioTimeOffset = CMTimeMake(0, 1);
+ _videoIsDisconnected = NO;
+ _audioIsDisconnected = NO;
+ [result sendSuccess];
+ } else {
+ [result sendErrorWithCode:@"Error" message:@"Video is already recording" details:nil];
+ }
+}
+
+- (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
+ if (_isRecording) {
+ _isRecording = NO;
+
+ if (_videoWriter.status != AVAssetWriterStatusUnknown) {
+ [_videoWriter finishWritingWithCompletionHandler:^{
+ if (self->_videoWriter.status == AVAssetWriterStatusCompleted) {
+ [self updateOrientation];
+ [result sendSuccessWithData:self->_videoRecordingPath];
+ self->_videoRecordingPath = nil;
+ } else {
+ [result sendErrorWithCode:@"IOError"
+ message:@"AVAssetWriter could not finish writing!"
+ details:nil];
+ }
+ }];
+ }
+ } else {
+ NSError *error =
+ [NSError errorWithDomain:NSCocoaErrorDomain
+ code:NSURLErrorResourceUnavailable
+ userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}];
+ [result sendError:error];
+ }
+}
+
+- (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
+ _isRecordingPaused = YES;
+ _videoIsDisconnected = YES;
+ _audioIsDisconnected = YES;
+ [result sendSuccess];
+}
+
+- (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result {
+ _isRecordingPaused = NO;
+ [result sendSuccess];
+}
+
+- (void)lockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result
+ orientation:(NSString *)orientationStr {
+ UIDeviceOrientation orientation;
+ @try {
+ orientation = FLTGetUIDeviceOrientationForString(orientationStr);
+ } @catch (NSError *e) {
+ [result sendError:e];
+ return;
+ }
+
+ if (_lockedCaptureOrientation != orientation) {
+ _lockedCaptureOrientation = orientation;
+ [self updateOrientation];
+ }
+
+ [result sendSuccess];
+}
+
+- (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result {
+ _lockedCaptureOrientation = UIDeviceOrientationUnknown;
+ [self updateOrientation];
+ [result sendSuccess];
+}
+
+- (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
+ FLTFlashMode mode;
+ @try {
+ mode = FLTGetFLTFlashModeForString(modeStr);
+ } @catch (NSError *e) {
+ [result sendError:e];
+ return;
+ }
+ if (mode == FLTFlashModeTorch) {
+ if (!_captureDevice.hasTorch) {
+ [result sendErrorWithCode:@"setFlashModeFailed"
+ message:@"Device does not support torch mode"
+ details:nil];
+ return;
+ }
+ if (!_captureDevice.isTorchAvailable) {
+ [result sendErrorWithCode:@"setFlashModeFailed"
+ message:@"Torch mode is currently not available"
+ details:nil];
+ return;
+ }
+ if (_captureDevice.torchMode != AVCaptureTorchModeOn) {
+ [_captureDevice lockForConfiguration:nil];
+ [_captureDevice setTorchMode:AVCaptureTorchModeOn];
+ [_captureDevice unlockForConfiguration];
+ }
+ } else {
+ if (!_captureDevice.hasFlash) {
+ [result sendErrorWithCode:@"setFlashModeFailed"
+ message:@"Device does not have flash capabilities"
+ details:nil];
+ return;
+ }
+ AVCaptureFlashMode avFlashMode = FLTGetAVCaptureFlashModeForFLTFlashMode(mode);
+ if (![_capturePhotoOutput.supportedFlashModes
+ containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) {
+ [result sendErrorWithCode:@"setFlashModeFailed"
+ message:@"Device does not support this specific flash mode"
+ details:nil];
+ return;
+ }
+ if (_captureDevice.torchMode != AVCaptureTorchModeOff) {
+ [_captureDevice lockForConfiguration:nil];
+ [_captureDevice setTorchMode:AVCaptureTorchModeOff];
+ [_captureDevice unlockForConfiguration];
+ }
+ }
+ _flashMode = mode;
+ [result sendSuccess];
+}
+
+- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
+ FLTExposureMode mode;
+ @try {
+ mode = FLTGetFLTExposureModeForString(modeStr);
+ } @catch (NSError *e) {
+ [result sendError:e];
+ return;
+ }
+ _exposureMode = mode;
+ [self applyExposureMode];
+ [result sendSuccess];
+}
+
+- (void)applyExposureMode {
+ [_captureDevice lockForConfiguration:nil];
+ switch (_exposureMode) {
+ case FLTExposureModeLocked:
+ [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
+ break;
+ case FLTExposureModeAuto:
+ if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
+ [_captureDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
+ } else {
+ [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose];
+ }
+ break;
+ }
+ [_captureDevice unlockForConfiguration];
+}
+
+- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr {
+ FLTFocusMode mode;
+ @try {
+ mode = FLTGetFLTFocusModeForString(modeStr);
+ } @catch (NSError *e) {
+ [result sendError:e];
+ return;
+ }
+ _focusMode = mode;
+ [self applyFocusMode];
+ [result sendSuccess];
+}
+
+- (void)applyFocusMode {
+ [self applyFocusMode:_focusMode onDevice:_captureDevice];
+}
+
+- (void)applyFocusMode:(FLTFocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice {
+ [captureDevice lockForConfiguration:nil];
+ switch (focusMode) {
+ case FLTFocusModeLocked:
+ if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
+ [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
+ }
+ break;
+ case FLTFocusModeAuto:
+ if ([captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
+ [captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
+ } else if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
+ [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus];
+ }
+ break;
+ }
+ [captureDevice unlockForConfiguration];
+}
+
+- (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result {
+ _isPreviewPaused = true;
+ [result sendSuccess];
+}
+
+- (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result {
+ _isPreviewPaused = false;
+ [result sendSuccess];
+}
+
+- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation
+ x:(double)x
+ y:(double)y {
+ double oldX = x, oldY = y;
+ switch (orientation) {
+ case UIDeviceOrientationPortrait: // 90 ccw
+ y = 1 - oldX;
+ x = oldY;
+ break;
+ case UIDeviceOrientationPortraitUpsideDown: // 90 cw
+ x = 1 - oldY;
+ y = oldX;
+ break;
+ case UIDeviceOrientationLandscapeRight: // 180
+ x = 1 - x;
+ y = 1 - y;
+ break;
+ case UIDeviceOrientationLandscapeLeft:
+ default:
+ // No rotation required
+ break;
+ }
+ return CGPointMake(x, y);
+}
+
+- (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y {
+ if (!_captureDevice.isExposurePointOfInterestSupported) {
+ [result sendErrorWithCode:@"setExposurePointFailed"
+ message:@"Device does not have exposure point capabilities"
+ details:nil];
+ return;
+ }
+ UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
+ [_captureDevice lockForConfiguration:nil];
+ [_captureDevice setExposurePointOfInterest:[self getCGPointForCoordsWithOrientation:orientation
+ x:x
+ y:y]];
+ [_captureDevice unlockForConfiguration];
+ // Retrigger auto exposure
+ [self applyExposureMode];
+ [result sendSuccess];
+}
+
+- (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y {
+ if (!_captureDevice.isFocusPointOfInterestSupported) {
+ [result sendErrorWithCode:@"setFocusPointFailed"
+ message:@"Device does not have focus point capabilities"
+ details:nil];
+ return;
+ }
+ UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
+ [_captureDevice lockForConfiguration:nil];
+
+ [_captureDevice setFocusPointOfInterest:[self getCGPointForCoordsWithOrientation:orientation
+ x:x
+ y:y]];
+ [_captureDevice unlockForConfiguration];
+ // Retrigger auto focus
+ [self applyFocusMode];
+ [result sendSuccess];
+}
+
+- (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset {
+ [_captureDevice lockForConfiguration:nil];
+ [_captureDevice setExposureTargetBias:offset completionHandler:nil];
+ [_captureDevice unlockForConfiguration];
+ [result sendSuccessWithData:@(offset)];
+}
+
+- (void)startImageStreamWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger {
+ if (!_isStreamingImages) {
+ FlutterEventChannel *eventChannel =
+ [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream"
+ binaryMessenger:messenger];
+ FLTThreadSafeEventChannel *threadSafeEventChannel =
+ [[FLTThreadSafeEventChannel alloc] initWithEventChannel:eventChannel];
+
+ _imageStreamHandler =
+ [[FLTImageStreamHandler alloc] initWithCaptureSessionQueue:_captureSessionQueue];
+ [threadSafeEventChannel setStreamHandler:_imageStreamHandler
+ completion:^{
+ dispatch_async(self->_captureSessionQueue, ^{
+ self.isStreamingImages = YES;
+ });
+ }];
+ } else {
+ [_methodChannel invokeMethod:errorMethod
+ arguments:@"Images from camera are already streaming!"];
+ }
+}
+
+- (void)stopImageStream {
+ if (_isStreamingImages) {
+ _isStreamingImages = NO;
+ _imageStreamHandler = nil;
+ } else {
+ [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are not streaming!"];
+ }
+}
+
+- (void)getMaxZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result {
+ CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor];
+
+ [result sendSuccessWithData:[NSNumber numberWithFloat:maxZoomFactor]];
+}
+
+- (void)getMinZoomLevelWithResult:(FLTThreadSafeFlutterResult *)result {
+ CGFloat minZoomFactor = [self getMinAvailableZoomFactor];
+ [result sendSuccessWithData:[NSNumber numberWithFloat:minZoomFactor]];
+}
+
+- (void)setZoomLevel:(CGFloat)zoom Result:(FLTThreadSafeFlutterResult *)result {
+ CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor];
+ CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor];
+
+ if (maxAvailableZoomFactor < zoom || minAvailableZoomFactor > zoom) {
+ NSString *errorMessage = [NSString
+ stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).",
+ minAvailableZoomFactor, maxAvailableZoomFactor];
+
+ [result sendErrorWithCode:@"ZOOM_ERROR" message:errorMessage details:nil];
+ return;
+ }
+
+ NSError *error = nil;
+ if (![_captureDevice lockForConfiguration:&error]) {
+ [result sendError:error];
+ return;
+ }
+ _captureDevice.videoZoomFactor = zoom;
+ [_captureDevice unlockForConfiguration];
+
+ [result sendSuccess];
+}
+
+- (CGFloat)getMinAvailableZoomFactor {
+ if (@available(iOS 11.0, *)) {
+ return _captureDevice.minAvailableVideoZoomFactor;
+ } else {
+ return 1.0;
+ }
+}
+
+- (CGFloat)getMaxAvailableZoomFactor {
+ if (@available(iOS 11.0, *)) {
+ return _captureDevice.maxAvailableVideoZoomFactor;
+ } else {
+ return _captureDevice.activeFormat.videoMaxZoomFactor;
+ }
+}
+
+- (BOOL)setupWriterForPath:(NSString *)path {
+ NSError *error = nil;
+ NSURL *outputURL;
+ if (path != nil) {
+ outputURL = [NSURL fileURLWithPath:path];
+ } else {
+ return NO;
+ }
+ if (_enableAudio && !_isAudioSetup) {
+ [self setUpCaptureSessionForAudio];
+ }
+
+ _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL
+ fileType:AVFileTypeMPEG4
+ error:&error];
+ NSParameterAssert(_videoWriter);
+ if (error) {
+ [_methodChannel invokeMethod:errorMethod arguments:error.description];
+ return NO;
+ }
+
+ NSDictionary *videoSettings = [_captureVideoOutput
+ recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4];
+ _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
+ outputSettings:videoSettings];
+
+ _videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor
+ assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput
+ sourcePixelBufferAttributes:@{
+ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat)
+ }];
+
+ NSParameterAssert(_videoWriterInput);
+
+ _videoWriterInput.expectsMediaDataInRealTime = YES;
+
+ // Add the audio input
+ if (_enableAudio) {
+ AudioChannelLayout acl;
+ bzero(&acl, sizeof(acl));
+ acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
+ NSDictionary *audioOutputSettings = nil;
+ // Both type of audio inputs causes output video file to be corrupted.
+ audioOutputSettings = @{
+ AVFormatIDKey : [NSNumber numberWithInt:kAudioFormatMPEG4AAC],
+ AVSampleRateKey : [NSNumber numberWithFloat:44100.0],
+ AVNumberOfChannelsKey : [NSNumber numberWithInt:1],
+ AVChannelLayoutKey : [NSData dataWithBytes:&acl length:sizeof(acl)],
+ };
+ _audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
+ outputSettings:audioOutputSettings];
+ _audioWriterInput.expectsMediaDataInRealTime = YES;
+
+ [_videoWriter addInput:_audioWriterInput];
+ [_audioOutput setSampleBufferDelegate:self queue:_captureSessionQueue];
+ }
+
+ if (_flashMode == FLTFlashModeTorch) {
+ [self.captureDevice lockForConfiguration:nil];
+ [self.captureDevice setTorchMode:AVCaptureTorchModeOn];
+ [self.captureDevice unlockForConfiguration];
+ }
+
+ [_videoWriter addInput:_videoWriterInput];
+
+ [_captureVideoOutput setSampleBufferDelegate:self queue:_captureSessionQueue];
+
+ return YES;
+}
+
+- (void)setUpCaptureSessionForAudio {
+ NSError *error = nil;
+ // Create a device input with the device and add it to the session.
+ // Setup the audio input.
+ AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
+ AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice
+ error:&error];
+ if (error) {
+ [_methodChannel invokeMethod:errorMethod arguments:error.description];
+ }
+ // Setup the audio output.
+ _audioOutput = [[AVCaptureAudioDataOutput alloc] init];
+
+ if ([_captureSession canAddInput:audioInput]) {
+ [_captureSession addInput:audioInput];
+
+ if ([_captureSession canAddOutput:_audioOutput]) {
+ [_captureSession addOutput:_audioOutput];
+ _isAudioSetup = YES;
+ } else {
+ [_methodChannel invokeMethod:errorMethod
+ arguments:@"Unable to add Audio input/output to session capture"];
+ _isAudioSetup = NO;
+ }
+ }
+}
+@end