diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index c088bd5b5373..4b96fd6493e5 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.9.4+15 +* Uses dispatch queue for pixel buffer synchronization on iOS. * Minor iOS internal code cleanup related to queue helper functions. ## 0.9.4+14 diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 8c102bfa38b1..6c171505a8ca 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; }; E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; }; E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; + E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */; }; E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; }; E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; @@ -91,6 +92,8 @@ E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; + E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraTestUtils.h; sourceTree = ""; }; + E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraTestUtils.m; sourceTree = ""; }; E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; @@ -132,6 +135,8 @@ E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */, E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */, E01EE4A72799F3A5008C1950 /* QueueUtilsTests.m */, + E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */, + E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */, @@ -408,6 +413,7 @@ E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, + E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */, E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, diff --git a/packages/camera/camera/example/ios/Runner/main.m b/packages/camera/camera/example/ios/Runner/main.m index 8b3fb8d5b361..d1224fea37ed 100644 --- a/packages/camera/camera/example/ios/Runner/main.m +++ b/packages/camera/camera/example/ios/Runner/main.m @@ -11,7 +11,7 @@ int main(int argc, char *argv[]) { // The setup logic in `AppDelegate::didFinishLaunchingWithOptions:` eventually sends camera // operations on the background queue, which would run concurrently with the test cases during // unit tests, making the debugging process confusing. This setup is actually not necessary for - // the unit tests, so here we want to skip the AppDelegate when running unit tests. + // the unit tests, so it is better to skip the AppDelegate when running unit tests. BOOL isTesting = NSClassFromString(@"XCTestCase") != nil; return UIApplicationMain(argc, argv, nil, isTesting ? nil : NSStringFromClass([AppDelegate class])); diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraTestUtils.h b/packages/camera/camera/example/ios/RunnerTests/CameraTestUtils.h new file mode 100644 index 000000000000..9fe67dc650e2 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraTestUtils.h @@ -0,0 +1,18 @@ +// 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; + +NS_ASSUME_NONNULL_BEGIN + +/// Creates an `FLTCam` that runs its capture session operations on a given queue. +/// @param captureSessionQueue the capture session queue +/// @return an FLTCam object. +extern FLTCam *FLTCreateCamWithCaptureSessionQueue(dispatch_queue_t captureSessionQueue); + +/// Creates a test sample buffer. +/// @return a test sample buffer. +extern CMSampleBufferRef FLTCreateTestSampleBuffer(void); + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraTestUtils.m b/packages/camera/camera/example/ios/RunnerTests/CameraTestUtils.m new file mode 100644 index 000000000000..0ae4887eb631 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraTestUtils.m @@ -0,0 +1,44 @@ +// 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 "CameraTestUtils.h" +#import +@import AVFoundation; + +FLTCam *FLTCreateCamWithCaptureSessionQueue(dispatch_queue_t captureSessionQueue) { + id inputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) + .andReturn(inputMock); + + id sessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + return [[FLTCam alloc] initWithCameraName:@"camera" + resolutionPreset:@"medium" + enableAudio:true + orientation:UIDeviceOrientationPortrait + captureSession:sessionMock + captureSessionQueue:captureSessionQueue + error:nil]; +} + +CMSampleBufferRef FLTCreateTestSampleBuffer(void) { + CVPixelBufferRef pixelBuffer; + CVPixelBufferCreate(kCFAllocatorDefault, 100, 100, kCVPixelFormatType_32BGRA, NULL, &pixelBuffer); + + CMFormatDescriptionRef formatDescription; + CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, + &formatDescription); + + CMSampleTimingInfo timingInfo = {CMTimeMake(1, 44100), kCMTimeZero, kCMTimeInvalid}; + + CMSampleBufferRef sampleBuffer; + CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault, pixelBuffer, formatDescription, + &timingInfo, &sampleBuffer); + + CFRelease(pixelBuffer); + CFRelease(formatDescription); + return sampleBuffer; +} diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index fdb2abd4933e..ed3e6a9793fd 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -7,7 +7,9 @@ @import AVFoundation; @import XCTest; #import +#import "CameraTestUtils.h" +/// Includes test cases related to photo capture operations for FLTCam class. @interface FLTCamPhotoCaptureTests : XCTestCase @end @@ -22,7 +24,7 @@ - (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsW dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, (void *)FLTCaptureSessionQueueSpecific, NULL); - FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); OCMStub([mockSettings photoSettings]).andReturn(settings); @@ -61,7 +63,7 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, (void *)FLTCaptureSessionQueueSpecific, NULL); - FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); @@ -92,23 +94,4 @@ - (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWi [self waitForExpectationsWithTimeout:1 handler:nil]; } -/// Creates an `FLTCam` that runs its operations on a given capture session queue. -- (FLTCam *)createFLTCamWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { - id inputMock = OCMClassMock([AVCaptureDeviceInput class]); - OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) - .andReturn(inputMock); - - id sessionMock = OCMClassMock([AVCaptureSession class]); - OCMStub([sessionMock alloc]).andReturn(sessionMock); - OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op - OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - - return [[FLTCam alloc] initWithCameraName:@"camera" - resolutionPreset:@"medium" - enableAudio:true - orientation:UIDeviceOrientationPortrait - captureSessionQueue:captureSessionQueue - error:nil]; -} - @end diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m index ccc8de5b23bd..8f65c4e9eaa5 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m @@ -7,7 +7,9 @@ @import AVFoundation; @import XCTest; #import +#import "CameraTestUtils.h" +/// Includes test cases related to sample buffer handling for FLTCam class. @interface FLTCamSampleBufferTests : XCTestCase @end @@ -15,23 +17,25 @@ @interface FLTCamSampleBufferTests : XCTestCase @implementation FLTCamSampleBufferTests - (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue { - id inputMock = OCMClassMock([AVCaptureDeviceInput class]); - OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) - .andReturn(inputMock); - - id sessionMock = OCMClassMock([AVCaptureSession class]); - OCMStub([sessionMock alloc]).andReturn(sessionMock); - OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op - OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - dispatch_queue_t captureSessionQueue = dispatch_queue_create("testing", NULL); - FLTCam *cam = [[FLTCam alloc] initWithCameraName:@"camera" - resolutionPreset:@"medium" - enableAudio:true - orientation:UIDeviceOrientationPortrait - captureSessionQueue:captureSessionQueue - error:nil]; - XCTAssertEqual(captureSessionQueue, cam.captureVideoOutput.sampleBufferCallbackQueue); + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); + XCTAssertEqual(captureSessionQueue, cam.captureVideoOutput.sampleBufferCallbackQueue, + @"Sample buffer callback queue must be the capture session queue."); +} + +- (void)testCopyPixelBuffer { + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(dispatch_queue_create("test", NULL)); + CMSampleBufferRef capturedSampleBuffer = FLTCreateTestSampleBuffer(); + CVPixelBufferRef capturedPixelBuffer = CMSampleBufferGetImageBuffer(capturedSampleBuffer); + // Mimic sample buffer callback when captured a new video sample + [cam captureOutput:cam.captureVideoOutput + didOutputSampleBuffer:capturedSampleBuffer + fromConnection:OCMClassMock([AVCaptureConnection class])]; + CVPixelBufferRef deliveriedPixelBuffer = [cam copyPixelBuffer]; + XCTAssertEqual(deliveriedPixelBuffer, capturedPixelBuffer, + @"FLTCam must deliver the latest captured pixel buffer to copyPixelBuffer API."); + CFRelease(capturedSampleBuffer); + CFRelease(deliveriedPixelBuffer); } @end diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index 9e8e2441f0b9..a70a572ac0f2 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -53,7 +53,7 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { [completionExpectation fulfill]; }]; - // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // Do not 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 @@ -82,7 +82,7 @@ - (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { [completionExpectation fulfill]; }]; - // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // Do not 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:filePath options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) @@ -107,7 +107,7 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue const char *ioQueueSpecific = "io_queue_specific"; dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); - // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. + // Do not 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]]) diff --git a/packages/camera/camera/ios/Classes/FLTCam.h b/packages/camera/camera/ios/Classes/FLTCam.h index 417a1d74db21..0cd135e0e41f 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.h +++ b/packages/camera/camera/ios/Classes/FLTCam.h @@ -31,6 +31,13 @@ NS_ASSUME_NONNULL_BEGIN // Format used for video and image streaming. @property(assign, nonatomic) FourCharCode videoFormat; +/// Initializes an `FLTCam` instance. +/// @param cameraName a name used to uniquely identify the camera. +/// @param resolutionPreset the resolution preset +/// @param enableAudio YES if audio should be enabled for video capturing; NO otherwise. +/// @param orientation the orientation of camera +/// @param captureSessionQueue the queue on which camera's capture session operations happen. +/// @param error report to the caller if any error happened creating the camera. - (instancetype)initWithCameraName:(NSString *)cameraName resolutionPreset:(NSString *)resolutionPreset enableAudio:(BOOL)enableAudio diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 669ce334059e..30c177bd4b2a 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -52,7 +52,9 @@ @interface FLTCam () *inProgressSavePhotoDelegates; +/// Delegate callback when receiving a new video or audio sample. +/// Exposed for unit tests. +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection; + +/// Initializes a camera instance. +/// Allows for injecting dependencies that are usually internal. +- (instancetype)initWithCameraName:(NSString *)cameraName + resolutionPreset:(NSString *)resolutionPreset + enableAudio:(BOOL)enableAudio + orientation:(UIDeviceOrientation)orientation + captureSession:(AVCaptureSession *)captureSession + captureSessionQueue:(dispatch_queue_t)captureSessionQueue + error:(NSError **)error; + @end diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7bc0e452bb29..fcdce02e947f 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.4+14 +version: 0.9.4+15 environment: sdk: ">=2.14.0 <3.0.0"