diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index d519ed410ec..0fead073fea 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.17+2 + +* Fixes stopVideoRecording waiting indefinitely and lag at start of video. + ## 0.9.17+1 * Fixes a crash due to appending sample buffers when readyForMoreMediaData is NO diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSampleBufferTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSampleBufferTests.m index 9490232e064..b6b78f2dab2 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSampleBufferTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSampleBufferTests.m @@ -251,4 +251,91 @@ - (void)testDidOutputSampleBufferMustNotAppendSampleWhenReadyForMoreMediaDataIsN CFRelease(videoSample); } +- (void)testStopVideoRecordingWithCompletionMustCallCompletion { + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(dispatch_queue_create("testing", NULL)); + + id writerMock = OCMClassMock([AVAssetWriter class]); + OCMStub([writerMock alloc]).andReturn(writerMock); + OCMStub([writerMock initWithURL:OCMOCK_ANY fileType:OCMOCK_ANY error:[OCMArg setTo:nil]]) + .andReturn(writerMock); + __block AVAssetWriterStatus status = AVAssetWriterStatusUnknown; + OCMStub([writerMock startWriting]).andDo(^(NSInvocation *invocation) { + status = AVAssetWriterStatusWriting; + }); + OCMStub([writerMock status]).andDo(^(NSInvocation *invocation) { + [invocation setReturnValue:&status]; + }); + + OCMStub([writerMock finishWritingWithCompletionHandler:[OCMArg checkWithBlock:^(id param) { + XCTAssert(status == AVAssetWriterStatusWriting, + @"Cannot call finishWritingWithCompletionHandler when status is " + @"not AVAssetWriterStatusWriting."); + void (^handler)(void) = param; + handler(); + return YES; + }]]); + + [cam + startVideoRecordingWithCompletion:^(FlutterError *_Nullable error) { + } + messengerForStreaming:nil]; + + __block BOOL completionCalled = NO; + [cam stopVideoRecordingWithCompletion:^(NSString *_Nullable path, FlutterError *_Nullable error) { + completionCalled = YES; + }]; + XCTAssert(completionCalled, @"Completion was not called."); +} + +- (void)testStartWritingShouldNotBeCalledBetweenSampleCreationAndAppending { + FLTCam *cam = FLTCreateCamWithCaptureSessionQueue(dispatch_queue_create("testing", NULL)); + CMSampleBufferRef videoSample = FLTCreateTestSampleBuffer(); + + id connectionMock = OCMClassMock([AVCaptureConnection class]); + + id writerMock = OCMClassMock([AVAssetWriter class]); + OCMStub([writerMock alloc]).andReturn(writerMock); + OCMStub([writerMock initWithURL:OCMOCK_ANY fileType:OCMOCK_ANY error:[OCMArg setTo:nil]]) + .andReturn(writerMock); + __block BOOL startWritingCalled = NO; + OCMStub([writerMock startWriting]).andDo(^(NSInvocation *invocation) { + startWritingCalled = YES; + }); + + __block BOOL videoAppended = NO; + id adaptorMock = OCMClassMock([AVAssetWriterInputPixelBufferAdaptor class]); + OCMStub([adaptorMock assetWriterInputPixelBufferAdaptorWithAssetWriterInput:OCMOCK_ANY + sourcePixelBufferAttributes:OCMOCK_ANY]) + .andReturn(adaptorMock); + OCMStub([adaptorMock appendPixelBuffer:[OCMArg anyPointer] withPresentationTime:kCMTimeZero]) + .ignoringNonObjectArgs() + .andDo(^(NSInvocation *invocation) { + videoAppended = YES; + }); + + id inputMock = OCMClassMock([AVAssetWriterInput class]); + OCMStub([inputMock assetWriterInputWithMediaType:OCMOCK_ANY outputSettings:OCMOCK_ANY]) + .andReturn(inputMock); + OCMStub([inputMock isReadyForMoreMediaData]).andReturn(YES); + + [cam + startVideoRecordingWithCompletion:^(FlutterError *_Nullable error) { + } + messengerForStreaming:nil]; + + BOOL startWritingCalledBefore = startWritingCalled; + [cam captureOutput:cam.captureVideoOutput + didOutputSampleBuffer:videoSample + fromConnection:connectionMock]; + XCTAssert((startWritingCalledBefore && videoAppended) || (startWritingCalled && !videoAppended), + @"The startWriting was called between sample creation and appending."); + + [cam captureOutput:cam.captureVideoOutput + didOutputSampleBuffer:videoSample + fromConnection:connectionMock]; + XCTAssert(videoAppended, @"Video was not appended."); + + CFRelease(videoSample); +} + @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m index 45a993e9d44..4c3a72c48c9 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m @@ -69,6 +69,7 @@ @interface FLTCam () _videoWriter.status == AVAssetWriterStatusCompleted) { - [self updateOrientation]; - completion(self->_videoRecordingPath, nil); - self->_videoRecordingPath = nil; - } else { - completion(nil, [FlutterError errorWithCode:@"IOError" - message:@"AVAssetWriter could not finish writing!" - details:nil]); - } - }]; - } + // when _isRecording is YES startWriting was already called so _videoWriter.status + // is always either AVAssetWriterStatusWriting or AVAssetWriterStatusFailed and + // finishWritingWithCompletionHandler does not throw exception so there is no need + // to check _videoWriter.status + [_videoWriter finishWritingWithCompletionHandler:^{ + if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { + [self updateOrientation]; + completion(self->_videoRecordingPath, nil); + self->_videoRecordingPath = nil; + } else { + completion(nil, [FlutterError errorWithCode:@"IOError" + message:@"AVAssetWriter could not finish writing!" + details:nil]); + } + }]; } else { NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index fbf5e3b3a76..4444026a41f 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.17+1 +version: 0.9.17+2 environment: sdk: ^3.2.3