diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 7e16bdfffff..4dbbce29e93 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.19+3 + +* Fixes race condition when starting image stream. + + ## 0.9.19+2 * Adds the `Camera` Swift protocol. diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 161c8ebda27..a0bc25c8cdc 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 978D90B42D5F630300CD817E /* StreamingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978D90B32D5F630300CD817E /* StreamingTests.swift */; }; 97922B0D2D6380C300A9B4CF /* SampleBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97922B0C2D6380C300A9B4CF /* SampleBufferTests.swift */; }; 979B3DFB2D5B6BC7009BDE1A /* ExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 979B3DFA2D5B6BC7009BDE1A /* ExceptionCatcher.m */; }; - 979B3DFE2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979B3DFD2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift */; }; + 979B3DFE2D5B985B009BDE1A /* CameraInitRaceConditionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979B3DFD2D5B985B009BDE1A /* CameraInitRaceConditionsTests.swift */; }; 979B3E002D5B9E6C009BDE1A /* CameraMethodChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979B3DFF2D5B9E6C009BDE1A /* CameraMethodChannelTests.swift */; }; 979B3E022D5BA48F009BDE1A /* CameraOrientationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979B3E012D5BA48F009BDE1A /* CameraOrientationTests.swift */; }; 97BD4A0E2D5CC5AE00F857D5 /* CameraSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97BD4A0D2D5CC5AE00F857D5 /* CameraSettingsTests.swift */; }; @@ -114,7 +114,7 @@ 979B3DF92D5B6BA2009BDE1A /* ExceptionCatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExceptionCatcher.h; sourceTree = ""; }; 979B3DFA2D5B6BC7009BDE1A /* ExceptionCatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExceptionCatcher.m; sourceTree = ""; }; 979B3DFC2D5B985B009BDE1A /* RunnerTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RunnerTests-Bridging-Header.h"; sourceTree = ""; }; - 979B3DFD2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureSessionQueueRaceConditionTests.swift; sourceTree = ""; }; + 979B3DFD2D5B985B009BDE1A /* CameraInitRaceConditionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraInitRaceConditionsTests.swift; sourceTree = ""; }; 979B3DFF2D5B9E6C009BDE1A /* CameraMethodChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMethodChannelTests.swift; sourceTree = ""; }; 979B3E012D5BA48F009BDE1A /* CameraOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraOrientationTests.swift; sourceTree = ""; }; 97BD4A0D2D5CC5AE00F857D5 /* CameraSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraSettingsTests.swift; sourceTree = ""; }; @@ -191,7 +191,7 @@ 979B3DF92D5B6BA2009BDE1A /* ExceptionCatcher.h */, 979B3DFA2D5B6BC7009BDE1A /* ExceptionCatcher.m */, 979B3DFC2D5B985B009BDE1A /* RunnerTests-Bridging-Header.h */, - 979B3DFD2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift */, + 979B3DFD2D5B985B009BDE1A /* CameraInitRaceConditionsTests.swift */, 979B3DFF2D5B9E6C009BDE1A /* CameraMethodChannelTests.swift */, 979B3E012D5BA48F009BDE1A /* CameraOrientationTests.swift */, 97BD4A0D2D5CC5AE00F857D5 /* CameraSettingsTests.swift */, @@ -552,7 +552,7 @@ E1ABED6D2D94392700AED9CC /* MockAssetWriterInput.swift in Sources */, 977A25242D5A511600931E34 /* CameraPermissionTests.swift in Sources */, 970ADABE2D6740A900EFDCD9 /* MockWritableData.swift in Sources */, - 979B3DFE2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift in Sources */, + 979B3DFE2D5B985B009BDE1A /* CameraInitRaceConditionsTests.swift in Sources */, E142F13A2D85940600824824 /* MockCapturePhotoOutput.swift in Sources */, E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */, 97922B0D2D6380C300A9B4CF /* SampleBufferTests.swift in Sources */, diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraInitRaceConditionsTests.swift similarity index 69% rename from packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraInitRaceConditionsTests.swift index d2fd50a1ce3..b71f71f7d52 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraInitRaceConditionsTests.swift @@ -11,7 +11,7 @@ import XCTest import camera_avfoundation_objc #endif -final class CameraCaptureSessionQueueRaceConditionTests: XCTestCase { +final class CameraInitRaceConditionsTests: XCTestCase { private func createCameraPlugin() -> (CameraPlugin, DispatchQueue) { let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") @@ -62,4 +62,36 @@ final class CameraCaptureSessionQueueRaceConditionTests: XCTestCase { XCTAssertNotNil( captureSessionQueue, "captureSessionQueue must not be nil after create method.") } + + func testFlutterChannelInitializedWhenStartingImageStream() { + let (cameraPlugin, _captureSessionQueue) = createCameraPlugin() + let createExpectation = expectation(description: "create's result block must be called") + + cameraPlugin.createCameraOnSessionQueue( + withName: "acamera", + settings: FCPPlatformMediaSettings.make( + with: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result, error in + createExpectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + // Start stream and wait for its completion. + let startStreamExpectation = expectation( + description: "startImageStream's result block must be called") + cameraPlugin.startImageStream(completion: { + _ in + startStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertEqual(cameraPlugin.camera?.isStreamingImages, true) + } + } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift index b860b357bcf..9bae2981990 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift @@ -261,8 +261,9 @@ final class CameraPluginDelegatingMethodTests: XCTestCase { let expectation = expectation(description: "Call completed") var startImageStreamCalled = false - mockCamera.startImageStreamStub = { _ in + mockCamera.startImageStreamStub = { messenger, completion in startImageStreamCalled = true + completion(nil) } cameraPlugin.startImageStream { error in diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift index 3f719dacdb2..7cb2dd6dccd 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift @@ -39,7 +39,7 @@ final class MockCamera: NSObject, Camera { var pausePreviewStub: (() -> Void)? var resumePreviewStub: (() -> Void)? var setDescriptionWhileRecordingStub: ((String, ((FlutterError?) -> Void)?) -> Void)? - var startImageStreamStub: ((FlutterBinaryMessenger) -> Void)? + var startImageStreamStub: ((FlutterBinaryMessenger, (FlutterError?) -> Void) -> Void)? var stopImageStreamStub: (() -> Void)? var dartAPI: FCPCameraEventApi? { @@ -63,6 +63,7 @@ final class MockCamera: NSObject, Camera { var videoFormat: FourCharCode = kCVPixelFormatType_32BGRA var isPreviewPaused: Bool = false + var isStreamingImages: Bool = false var minimumExposureOffset: CGFloat { return getMinimumExposureOffsetStub?() ?? 0 @@ -186,8 +187,11 @@ final class MockCamera: NSObject, Camera { setDescriptionWhileRecordingStub?(cameraName, completion) } - func startImageStream(with messenger: FlutterBinaryMessenger) { - startImageStreamStub?(messenger) + func startImageStream( + with messenger: FlutterBinaryMessenger, + completion: @escaping (FlutterError?) -> Void + ) { + startImageStreamStub?(messenger, completion) } func stopImageStream() { diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift index edb7a5fc479..5fa358b5017 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/StreamingTests.swift @@ -52,14 +52,28 @@ final class StreamingTests: XCTestCase { func testExceedMaxStreamingPendingFramesCount() { let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera() + let handlerMock = MockImageStreamHandler() + + let finishStartStreamExpectation = expectation( + description: "Finish startStream") + + let messenger = MockFlutterBinaryMessenger() + camera.startImageStream( + with: messenger, imageStreamHandler: handlerMock, + completion: { + _ in + finishStartStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + + // Setup mocked event sink after the stream starts let streamingExpectation = expectation( description: "Must not call handler over maxStreamingPendingFramesCount") - let handlerMock = MockImageStreamHandler() + handlerMock.eventSinkStub = { event in streamingExpectation.fulfill() } - let messenger = MockFlutterBinaryMessenger() - camera.startImageStream(with: messenger, imageStreamHandler: handlerMock) waitForQueueRoundTrip(with: DispatchQueue.main) XCTAssertEqual(camera.isStreamingImages, true) @@ -74,14 +88,27 @@ final class StreamingTests: XCTestCase { func testReceivedImageStreamData() { let (camera, testAudioOutput, sampleBuffer, testAudioConnection) = createCamera() + let handlerMock = MockImageStreamHandler() + + let finishStartStreamExpectation = expectation( + description: "Finish startStream") + + let messenger = MockFlutterBinaryMessenger() + camera.startImageStream( + with: messenger, imageStreamHandler: handlerMock, + completion: { + _ in + finishStartStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + + // Setup mocked event sink after the stream starts let streamingExpectation = expectation( description: "Must be able to call the handler again when receivedImageStreamData is called") - let handlerMock = MockImageStreamHandler() handlerMock.eventSinkStub = { event in streamingExpectation.fulfill() } - let messenger = MockFlutterBinaryMessenger() - camera.startImageStream(with: messenger, imageStreamHandler: handlerMock) waitForQueueRoundTrip(with: DispatchQueue.main) XCTAssertEqual(camera.isStreamingImages, true) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift index 7fdde60680d..30b0cb0bbe6 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift @@ -25,6 +25,7 @@ protocol Camera: FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate, var videoFormat: FourCharCode { get set } var isPreviewPaused: Bool { get } + var isStreamingImages: Bool { get } var minimumAvailableZoomFactor: CGFloat { get } var maximumAvailableZoomFactor: CGFloat { get } @@ -86,7 +87,8 @@ protocol Camera: FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate, withCompletion: @escaping (_ error: FlutterError?) -> Void ) - func startImageStream(with: FlutterBinaryMessenger) + func startImageStream( + with: FlutterBinaryMessenger, completion: @escaping (_ error: FlutterError?) -> Void) func stopImageStream() // Override to make `AVCaptureVideoDataOutputSampleBufferDelegate`/ diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift index 62d570a578f..d3c3c81275f 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift @@ -309,9 +309,11 @@ extension CameraPlugin: FCPCameraApi { public func startImageStream(completion: @escaping (FlutterError?) -> Void) { captureSessionQueue.async { [weak self] in - guard let strongSelf = self else { return } - strongSelf.camera?.startImageStream(with: strongSelf.messenger) - completion(nil) + guard let strongSelf = self else { + completion(nil) + return + } + strongSelf.camera?.startImageStream(with: strongSelf.messenger, completion: completion) } } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m index e67c586eda1..9e3dfda732e 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCam.m @@ -306,7 +306,7 @@ - (void)captureToFileWithCompletion:(void (^)(NSString *_Nullable, NSString *path = [self getTemporaryFilePathWithExtension:extension subfolder:@"pictures" prefix:@"CAP_" - error:error]; + error:&error]; if (error) { completion(nil, FlutterErrorFromNSError(error)); return; @@ -362,7 +362,7 @@ - (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation: - (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension subfolder:(NSString *)subfolder prefix:(NSString *)prefix - error:(NSError *)error { + error:(NSError **)error { NSString *docDir = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; NSString *fileDir = @@ -373,11 +373,11 @@ - (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension NSFileManager *fm = [NSFileManager defaultManager]; if (![fm fileExistsAtPath:fileDir]) { - [[NSFileManager defaultManager] createDirectoryAtPath:fileDir - withIntermediateDirectories:true - attributes:nil - error:&error]; - if (error) { + BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:fileDir + withIntermediateDirectories:true + attributes:nil + error:error]; + if (!success) { return nil; } } @@ -498,43 +498,50 @@ - (void)dealloc { [_motionManager stopAccelerometerUpdates]; } +/// Main logic to setup the video recording. +- (void)setUpVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion { + NSError *error; + _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" + subfolder:@"videos" + prefix:@"REC_" + error:&error]; + if (error) { + completion(FlutterErrorFromNSError(error)); + return; + } + if (![self setupWriterForPath:_videoRecordingPath]) { + completion([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]); + return; + } + // startWriting should not be called in didOutputSampleBuffer where it can cause state + // in which _isRecording is YES but _videoWriter.status is AVAssetWriterStatusUnknown + // in stopVideoRecording if it is called after startVideoRecording but before + // didOutputSampleBuffer had chance to call startWriting and lag at start of video + // https://github.com/flutter/flutter/issues/132016 + // https://github.com/flutter/flutter/issues/151319 + [_videoWriter startWriting]; + _isFirstVideoSample = YES; + _isRecording = YES; + _isRecordingPaused = NO; + _videoTimeOffset = CMTimeMake(0, 1); + _audioTimeOffset = CMTimeMake(0, 1); + _videoIsDisconnected = NO; + _audioIsDisconnected = NO; + completion(nil); +} + - (void)startVideoRecordingWithCompletion:(void (^)(FlutterError *_Nullable))completion messengerForStreaming:(nullable NSObject *)messenger { if (!_isRecording) { if (messenger != nil) { - [self startImageStreamWithMessenger:messenger]; - } - - NSError *error; - _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" - subfolder:@"videos" - prefix:@"REC_" - error:error]; - if (error) { - completion(FlutterErrorFromNSError(error)); - return; - } - if (![self setupWriterForPath:_videoRecordingPath]) { - completion([FlutterError errorWithCode:@"IOError" - message:@"Setup Writer Failed" - details:nil]); + [self startImageStreamWithMessenger:messenger + completion:^(FlutterError *_Nullable error) { + [self setUpVideoRecordingWithCompletion:completion]; + }]; return; } - // startWriting should not be called in didOutputSampleBuffer where it can cause state - // in which _isRecording is YES but _videoWriter.status is AVAssetWriterStatusUnknown - // in stopVideoRecording if it is called after startVideoRecording but before - // didOutputSampleBuffer had chance to call startWriting and lag at start of video - // https://github.com/flutter/flutter/issues/132016 - // https://github.com/flutter/flutter/issues/151319 - [_videoWriter startWriting]; - _isFirstVideoSample = YES; - _isRecording = YES; - _isRecordingPaused = NO; - _videoTimeOffset = CMTimeMake(0, 1); - _audioTimeOffset = CMTimeMake(0, 1); - _videoIsDisconnected = NO; - _audioIsDisconnected = NO; - completion(nil); + + [self setUpVideoRecordingWithCompletion:completion]; } else { completion([FlutterError errorWithCode:@"Error" message:@"Video is already recording" @@ -831,14 +838,17 @@ - (void)setExposureOffset:(double)offset { [_captureDevice unlockForConfiguration]; } -- (void)startImageStreamWithMessenger:(NSObject *)messenger { +- (void)startImageStreamWithMessenger:(NSObject *)messenger + completion:(void (^)(FlutterError *))completion { [self startImageStreamWithMessenger:messenger imageStreamHandler:[[FLTImageStreamHandler alloc] - initWithCaptureSessionQueue:_captureSessionQueue]]; + initWithCaptureSessionQueue:_captureSessionQueue] + completion:completion]; } - (void)startImageStreamWithMessenger:(NSObject *)messenger - imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler { + imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler + completion:(void (^)(FlutterError *))completion { if (!_isStreamingImages) { id eventChannel = [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera_avfoundation/imageStream" @@ -851,19 +861,27 @@ - (void)startImageStreamWithMessenger:(NSObject *)messen [threadSafeEventChannel setStreamHandler:_imageStreamHandler completion:^{ typeof(self) strongSelf = weakSelf; - if (!strongSelf) return; + if (!strongSelf) { + completion(nil); + return; + } dispatch_async(strongSelf.captureSessionQueue, ^{ // cannot use the outter strongSelf typeof(self) strongSelf = weakSelf; - if (!strongSelf) return; + if (!strongSelf) { + completion(nil); + return; + } strongSelf.isStreamingImages = YES; strongSelf.streamingPendingFramesCount = 0; + completion(nil); }); }]; } else { [self reportErrorMessage:@"Images from camera are already streaming!"]; + completion(nil); } } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h index 4724009fe5c..6c19d22f9a7 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam.h @@ -117,7 +117,8 @@ NS_ASSUME_NONNULL_BEGIN withCompletion:(void (^)(FlutterError *_Nullable))completion NS_SWIFT_NAME(setFocusPoint(_:completion:)); - (void)setExposureOffset:(double)offset; -- (void)startImageStreamWithMessenger:(NSObject *)messenger; +- (void)startImageStreamWithMessenger:(NSObject *)messenger + completion:(nonnull void (^)(FlutterError *_Nullable))completion; - (void)stopImageStream; - (void)setZoomLevel:(CGFloat)zoom withCompletion:(void (^)(FlutterError *_Nullable))completion; - (void)setUpCaptureSessionForAudioIfNeeded; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam_Test.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam_Test.h index e7bfdb55223..2e9ab4aafb5 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam_Test.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCam_Test.h @@ -33,6 +33,7 @@ /// Start streaming images. - (void)startImageStreamWithMessenger:(NSObject *)messenger - imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler; + imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler + completion:(void (^)(FlutterError *))completion; @end diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index e0441452e23..b34c3a8e0db 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.19+2 +version: 0.9.19+3 environment: sdk: ^3.6.0