-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[camera]writing file on background queue #4721
Changes from all commits
2939d95
429381a
57f3c84
7911ecd
b0e1139
87ecfb3
4174a54
86569d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // 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 camera; | ||
| @import camera.Test; | ||
| @import AVFoundation; | ||
| @import XCTest; | ||
| #import <OCMock/OCMock.h> | ||
|
|
||
| @interface FLTSavePhotoDelegateTests : XCTestCase | ||
|
|
||
| @end | ||
|
|
||
| @implementation FLTSavePhotoDelegateTests | ||
|
|
||
| - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToCapture { | ||
| NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; | ||
| dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); | ||
| id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); | ||
| FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" | ||
| result:mockResult | ||
| ioQueue:ioQueue]; | ||
|
|
||
| [delegate handlePhotoCaptureResultWithError:error | ||
| photoDataProvider:^NSData * { | ||
| return nil; | ||
| }]; | ||
| OCMVerify([mockResult sendError:error]); | ||
| } | ||
|
|
||
| - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { | ||
| XCTestExpectation *resultExpectation = | ||
| [self expectationWithDescription:@"Must send IOError to the result if failed to write file."]; | ||
| dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); | ||
| id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); | ||
|
|
||
| NSError *ioError = [NSError errorWithDomain:@"IOError" | ||
| code:0 | ||
| userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}]; | ||
|
|
||
| OCMStub([mockResult sendErrorWithCode:@"IOError" | ||
| message:@"Unable to write file" | ||
| details:ioError.localizedDescription]) | ||
| .andDo(^(NSInvocation *invocation) { | ||
| [resultExpectation fulfill]; | ||
| }); | ||
| FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" | ||
| result:mockResult | ||
| ioQueue:ioQueue]; | ||
|
|
||
| // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. | ||
| // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. | ||
| id mockData = OCMPartialMock([NSData data]); | ||
| OCMStub([mockData writeToFile:OCMOCK_ANY | ||
| options:NSDataWritingAtomic | ||
| error:[OCMArg setTo:ioError]]) | ||
| .andReturn(NO); | ||
| [delegate handlePhotoCaptureResultWithError:nil | ||
| photoDataProvider:^NSData * { | ||
| return mockData; | ||
| }]; | ||
| [self waitForExpectationsWithTimeout:1 handler:nil]; | ||
| } | ||
|
|
||
| - (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite { | ||
| XCTestExpectation *resultExpectation = [self | ||
| expectationWithDescription:@"Must send file path to the result if success to write file."]; | ||
|
|
||
| dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); | ||
| id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); | ||
| FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" | ||
| result:mockResult | ||
| ioQueue:ioQueue]; | ||
| OCMStub([mockResult sendSuccessWithData:delegate.path]).andDo(^(NSInvocation *invocation) { | ||
| [resultExpectation fulfill]; | ||
| }); | ||
|
|
||
| // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. | ||
| // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. | ||
| id mockData = OCMPartialMock([NSData data]); | ||
| OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) | ||
| .andReturn(YES); | ||
|
|
||
| [delegate handlePhotoCaptureResultWithError:nil | ||
| photoDataProvider:^NSData * { | ||
| return mockData; | ||
| }]; | ||
| [self waitForExpectationsWithTimeout:1 handler:nil]; | ||
| } | ||
|
|
||
| - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue { | ||
| XCTestExpectation *dataProviderQueueExpectation = | ||
| [self expectationWithDescription:@"Data provider must run on io queue."]; | ||
| XCTestExpectation *writeFileQueueExpectation = | ||
| [self expectationWithDescription:@"File writing must run on io queue"]; | ||
| XCTestExpectation *resultExpectation = [self | ||
| expectationWithDescription:@"Must send file path to the result if success to write file."]; | ||
|
|
||
| dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); | ||
| const char *ioQueueSpecific = "io_queue_specific"; | ||
| dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); | ||
| id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); | ||
| OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { | ||
| [resultExpectation fulfill]; | ||
| }); | ||
|
|
||
| // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. | ||
| // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. | ||
| id mockData = OCMPartialMock([NSData data]); | ||
| OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) | ||
| .andDo(^(NSInvocation *invocation) { | ||
| if (dispatch_get_specific(ioQueueSpecific)) { | ||
| [writeFileQueueExpectation fulfill]; | ||
| } | ||
| }) | ||
| .andReturn(YES); | ||
|
|
||
| FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" | ||
| result:mockResult | ||
| ioQueue:ioQueue]; | ||
| [delegate handlePhotoCaptureResultWithError:nil | ||
| photoDataProvider:^NSData * { | ||
| if (dispatch_get_specific(ioQueueSpecific)) { | ||
| [dataProviderQueueExpectation fulfill]; | ||
| } | ||
| return mockData; | ||
| }]; | ||
|
|
||
| [self waitForExpectationsWithTimeout:1 handler:nil]; | ||
| } | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
|
|
||
| #import "FLTCam.h" | ||
| #import "FLTCam_Test.h" | ||
| #import "FLTSavePhotoDelegate.h" | ||
|
|
||
| @import CoreMotion; | ||
| #import <libkern/OSAtomic.h> | ||
|
|
@@ -41,71 +42,6 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments | |
| } | ||
| @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> | ||
|
|
||
|
|
@@ -138,8 +74,11 @@ @interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate, | |
| @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. | ||
| /// All FLTCam's state access and capture session related operations should be on run on this queue. | ||
| @property(strong, nonatomic) dispatch_queue_t captureSessionQueue; | ||
| /// The queue on which captured photos (not videos) are wrote to disk. | ||
| /// Videos are wrote to disk by `videoAdaptor` on an internal queue managed by AVFoundation. | ||
| @property(strong, nonatomic) dispatch_queue_t photoIOQueue; | ||
| @property(assign, nonatomic) UIDeviceOrientation deviceOrientation; | ||
| @end | ||
|
|
||
|
|
@@ -162,6 +101,7 @@ - (instancetype)initWithCameraName:(NSString *)cameraName | |
| } | ||
| _enableAudio = enableAudio; | ||
| _captureSessionQueue = captureSessionQueue; | ||
| _photoIOQueue = dispatch_queue_create("io.flutter.camera.photoIOQueue", NULL); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| _captureSession = [[AVCaptureSession alloc] init]; | ||
| _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName]; | ||
| _flashMode = _captureDevice.hasFlash ? FLTFlashModeAuto : FLTFlashModeOff; | ||
|
|
@@ -280,9 +220,11 @@ - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10) | |
| return; | ||
| } | ||
|
|
||
| [_capturePhotoOutput capturePhotoWithSettings:settings | ||
| delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path | ||
| result:result]]; | ||
| [_capturePhotoOutput | ||
| capturePhotoWithSettings:settings | ||
| delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path | ||
| result:result | ||
| ioQueue:self.photoIOQueue]]; | ||
| } | ||
|
|
||
| - (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| // Copyright 2013 The Flutter Authors. All rights reserved. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is now exposed in the plugin API, I don't think we want that. I think keep most of this in the implementation and only expose
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I didn't add it to the umbrella header so it's not exposed outside of this package.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also removed thread safe wrappers from the umbrella header
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh right right I forgot this uses a real umbrella header and not one generated by CocoaPods. I think you're still exposing it in the test module since you're
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original header and the test header are both exposed for unit test only. I think this is what we want. |
||
| // 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 "FLTThreadSafeFlutterResult.h" | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| /** | ||
| Delegate object that handles photo capture results. | ||
| */ | ||
| @interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate> | ||
| /// The file path for the captured photo. | ||
| @property(readonly, nonatomic) NSString *path; | ||
| /// The thread safe flutter result wrapper to report the result. | ||
| @property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; | ||
| /// The queue on which captured photos are wrote to disk. | ||
| @property(strong, nonatomic) dispatch_queue_t ioQueue; | ||
| /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. | ||
| @property(strong, nonatomic, nullable) FLTSavePhotoDelegate *selfReference; | ||
|
|
||
| /** | ||
| * Initialize a photo capture delegate. | ||
| * @param path the path for captured photo file. | ||
| * @param result the thread safe flutter result wrapper to report the result. | ||
| * @param ioQueue the queue on which captured photos are wrote to disk. | ||
| */ | ||
| - (instancetype)initWithPath:(NSString *)path | ||
| result:(FLTThreadSafeFlutterResult *)result | ||
| ioQueue:(dispatch_queue_t)ioQueue; | ||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END | ||
Uh oh!
There was an error while loading. Please reload this page.