This repository was archived by the owner on Feb 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[camera]writing file on background queue #4721
Merged
Merged
Changes from 3 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
2939d95
[camera]photo file io on background queue
hellohuanlin 429381a
[camera]update license
hellohuanlin 57f3c84
[camera]update readme
hellohuanlin 7911ecd
[camera]move photo delegate out of fltcam and expose test header to t…
hellohuanlin b0e1139
[camera]address some nits
hellohuanlin 87ecfb3
[camera]remove thread safe wrappers from umbrella header
hellohuanlin 4174a54
[camera]passing file writing error details to flutter result
hellohuanlin 86569d7
[camera]fix unit tests after nit
hellohuanlin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // 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 FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate> | ||
|
hellohuanlin marked this conversation as resolved.
Outdated
|
||
| @property(readonly, nonatomic) NSString *path; | ||
| - initWithPath:(NSString *)path | ||
| result:(FLTThreadSafeFlutterResult *)result | ||
| ioQueue:(dispatch_queue_t)ioQueue; | ||
| - (void)handlePhotoCaptureResultWithError:(nullable NSError *)error | ||
| photoDataProvider:(NSData * (^)(void))photoDataProvider; | ||
| @end | ||
|
|
||
| @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(times(1), [mockResult sendError:error]); | ||
|
hellohuanlin marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| - (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]); | ||
| OCMStub([mockResult sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]) | ||
| .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:[OCMArg any] atomically:[OCMArg any]]).andReturn(NO); | ||
|
hellohuanlin marked this conversation as resolved.
Outdated
|
||
| [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:[OCMArg any] atomically:[OCMArg any]]).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:[OCMArg 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:[OCMArg any] atomically:[OCMArg any]]) | ||
| .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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,66 +44,69 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments | |
| @interface FLTSavePhotoDelegate : NSObject <AVCapturePhotoCaptureDelegate> | ||
| @property(readonly, nonatomic) NSString *path; | ||
| @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) FLTSavePhotoDelegate *selfReference; | ||
| @end | ||
|
|
||
| @implementation FLTSavePhotoDelegate { | ||
| /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. | ||
| FLTSavePhotoDelegate *selfReference; | ||
| } | ||
| @implementation FLTSavePhotoDelegate | ||
|
|
||
| - initWithPath:(NSString *)path result:(FLTThreadSafeFlutterResult *)result { | ||
| - initWithPath:(NSString *)path | ||
| result:(FLTThreadSafeFlutterResult *)result | ||
| ioQueue:(dispatch_queue_t)ioQueue { | ||
| self = [super init]; | ||
| NSAssert(self, @"super init cannot be nil"); | ||
| _path = path; | ||
| selfReference = self; | ||
| _selfReference = self; | ||
| _result = result; | ||
| _ioQueue = ioQueue; | ||
| return self; | ||
| } | ||
|
|
||
| - (void)handlePhotoCaptureResultWithError:(NSError *)error | ||
| photoDataProvider:(NSData * (^)(void))photoDataProvider { | ||
| self.selfReference = nil; | ||
| if (error) { | ||
| [self.result sendError:error]; | ||
| return; | ||
| } | ||
| dispatch_async(self.ioQueue, ^{ | ||
| NSData *data = photoDataProvider(); | ||
| bool success = [data writeToFile:self.path atomically:YES]; | ||
|
|
||
| if (!success) { | ||
| [self.result sendErrorWithCode:@"IOError" message:@"Unable to write file" details:nil]; | ||
| return; | ||
| } | ||
| [self.result sendSuccessWithData:self.path]; | ||
| }); | ||
| } | ||
|
|
||
| - (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]; | ||
| [self handlePhotoCaptureResultWithError:error | ||
| photoDataProvider:^NSData * { | ||
| return [AVCapturePhotoOutput | ||
| JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer | ||
| previewPhotoSampleBuffer: | ||
| previewPhotoSampleBuffer]; | ||
| }]; | ||
| } | ||
|
|
||
| - (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]; | ||
| [self handlePhotoCaptureResultWithError:error | ||
| photoDataProvider:^NSData * { | ||
| return [photo fileDataRepresentation]; | ||
| }]; | ||
| } | ||
|
|
||
| @end | ||
|
|
||
| @interface FLTCam () <AVCaptureVideoDataOutputSampleBufferDelegate, | ||
|
|
@@ -138,8 +141,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 +168,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 +287,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: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.