From 94e0373307d42002cf606aaa208d6002597bbc56 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Wed, 12 Mar 2025 15:01:48 +0100 Subject: [PATCH 1/4] Add tests for setting device orientation --- .../camera/camera_avfoundation/CHANGELOG.md | 5 + .../ios/Runner.xcodeproj/project.pbxproj | 8 ++ .../ios/RunnerTests/CameraSettingsTests.swift | 3 +- .../FLTCamSetDeviceOrientationTests.swift | 109 ++++++++++++++++++ .../RunnerTests/Mocks/MockCaptureConnection.h | 3 + .../RunnerTests/Mocks/MockCaptureConnection.m | 8 ++ .../Mocks/MockCapturePhotoOutput.h | 6 +- .../Mocks/MockCapturePhotoOutput.m | 9 +- .../Mocks/MockCaptureVideoDataOutput.swift | 23 ++++ .../ios/RunnerTests/SampleBufferTests.swift | 27 ++--- .../Sources/camera_avfoundation/FLTCam.m | 30 ++--- .../FLTCamMediaSettingsAVWrapper.m | 5 +- .../FLTCaptureConnection.m | 8 +- .../FLTCapturePhotoOutput.m | 24 ++-- .../FLTCaptureVideoDataOutput.m | 51 ++++++++ .../FLTCamMediaSettingsAVWrapper.h | 6 +- .../include/camera_avfoundation/FLTCam_Test.h | 3 +- .../FLTCaptureConnection.h | 4 - .../camera_avfoundation/FLTCaptureOutput.h | 23 ++++ .../FLTCapturePhotoOutput.h | 7 +- .../FLTCaptureVideoDataOutput.h | 40 +++++++ .../camera/camera_avfoundation/pubspec.yaml | 2 +- 22 files changed, 342 insertions(+), 62 deletions(-) create mode 100644 packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetDeviceOrientationTests.swift create mode 100644 packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift create mode 100644 packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m create mode 100644 packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureOutput.h create mode 100644 packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 49a386b8b17..4e350433bab 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.18+11 + +* Backfills unit tests for the `FLTCam` class. +* Refactors implementation to allow mocking of `AVCaptureVideoDataOutput` in tests. + ## 0.9.18+10 * Backfills unit tests for the `FLTCam` class. 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 1adda4c2706..95853f2b7a1 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -49,10 +49,12 @@ 97DB234D2D566D0700CEFE66 /* CameraPreviewPauseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */; }; E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */; }; E11D6A912D82C7740031E6C5 /* FLTCamExposureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */; }; + E11D6A8F2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */; }; E12C4FF62D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */; }; E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */; }; E16602952D8471C0003CFE12 /* FLTCamZoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */; }; E1A5F4E32D80259C0005BA64 /* FLTCamSetFlashModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */; }; + E15139182D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */; }; E1FFEAAD2D6C8DD700B14107 /* MockFLTCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */; }; E1FFEAAF2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */; }; E1FFEAB12D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */; }; @@ -148,10 +150,12 @@ 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 = ""; }; E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamExposureTests.swift; sourceTree = ""; }; + E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCaptureVideoDataOutput.swift; sourceTree = ""; }; E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginDelegatingMethodTests.swift; sourceTree = ""; }; E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCameraPermissionManager.swift; sourceTree = ""; }; E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamZoomTests.swift; sourceTree = ""; }; E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetFlashModeTests.swift; sourceTree = ""; }; + E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetDeviceOrientationTests.swift; sourceTree = ""; }; E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCam.swift; sourceTree = ""; }; E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginCreateCameraTests.swift; sourceTree = ""; }; E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginInitializeCameraTests.swift; sourceTree = ""; }; @@ -212,6 +216,7 @@ 977A25212D5A49EC00931E34 /* FLTCamFocusTests.swift */, E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */, E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */, + E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -254,6 +259,7 @@ 970ADABF2D6764CC00EFDCD9 /* MockEventChannel.swift */, E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */, 970ADABD2D6740A900EFDCD9 /* MockWritableData.swift */, + E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */, ); path = Mocks; sourceTree = ""; @@ -557,6 +563,7 @@ 7F8FD22F2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m in Sources */, E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */, 97922B0D2D6380C300A9B4CF /* SampleBufferTests.swift in Sources */, + E15139182D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift in Sources */, 972CA92B2D5A1D8C004B846F /* CameraPropertiesTests.swift in Sources */, E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */, 978296CF2D5F744B0009BDD3 /* PhotoCaptureTests.swift in Sources */, @@ -577,6 +584,7 @@ 978D90B42D5F630300CD817E /* StreamingTests.swift in Sources */, 7F29EB412D281C7E00740257 /* MockCaptureSession.m in Sources */, 7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */, + E11D6A8F2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift in Sources */, 977CAC9F2D5E5180001E5DC3 /* ThreadSafeEventChannelTests.swift in Sources */, 7FCEDD362D43C2B900EA1CA8 /* MockCaptureDevice.m in Sources */, 7F8FD22C2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m in Sources */, diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index b73769e5d02..c0e966a6d1f 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -101,11 +101,10 @@ private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { } override func recommendedVideoSettingsForAssetWriter( - withFileType fileType: AVFileType, for output: AVCaptureVideoDataOutput + withFileType fileType: AVFileType, for output: FLTCaptureVideoDataOutput ) -> [String: Any]? { return [:] } - } final class CameraSettingsTests: XCTestCase { diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetDeviceOrientationTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetDeviceOrientationTests.swift new file mode 100644 index 00000000000..5829d54313f --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetDeviceOrientationTests.swift @@ -0,0 +1,109 @@ +// 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 AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class FLTCamSetDeviceOrientationTests: XCTestCase { + private func createCamera() -> (FLTCam, MockCaptureConnection, MockCaptureConnection) { + let configuration = FLTCreateTestCameraConfiguration() + let camera = FLTCreateCamWithConfiguration(configuration) + + let mockCapturePhotoOutput = MockCapturePhotoOutput() + let mockPhotoCaptureConnection = MockCaptureConnection() + mockPhotoCaptureConnection.isVideoOrientationSupported = true + + mockCapturePhotoOutput.connectionWithMediaTypeStub = { _ in mockPhotoCaptureConnection } + camera.capturePhotoOutput = mockCapturePhotoOutput + + let mockCaptureVideoDataOutput = MockCaptureVideoDataOutput() + let mockVideoCaptureConnection = MockCaptureConnection() + mockVideoCaptureConnection.isVideoOrientationSupported = true + + mockCaptureVideoDataOutput.connectionWithMediaTypeStub = { _ in mockVideoCaptureConnection } + camera.captureVideoOutput = mockCaptureVideoDataOutput + + return (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) + } + + func testSetDeviceOrientation_setsOrientationsOfCaptureConnections() { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + var photoSetVideoOrientationCalled = false + mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in + // Device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape left the video orientation should be landscape right. + XCTAssertEqual(orientation, .landscapeRight) + photoSetVideoOrientationCalled = true + } + + var videoSetVideoOrientationCalled = false + mockVideoCaptureConnection.setVideoOrientationStub = { orientation in + // Device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape left the video orientation should be landscape right. + XCTAssertEqual(orientation, .landscapeRight) + videoSetVideoOrientationCalled = true + } + + camera.setDeviceOrientation(.landscapeLeft) + + XCTAssertTrue(photoSetVideoOrientationCalled) + XCTAssertTrue(videoSetVideoOrientationCalled) + } + + func + testSetDeviceOrientation_setsLockedOrientationsOfCaptureConnection_ifCaptureOrientationIsLocked() + { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + var photoSetVideoOrientationCalled = false + mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in + XCTAssertEqual(orientation, .portraitUpsideDown) + photoSetVideoOrientationCalled = true + } + + var videoSetVideoOrientationCalled = false + mockVideoCaptureConnection.setVideoOrientationStub = { orientation in + XCTAssertEqual(orientation, .portraitUpsideDown) + videoSetVideoOrientationCalled = true + } + + camera.lockCapture(FCPPlatformDeviceOrientation.portraitDown) + + camera.setDeviceOrientation(.landscapeLeft) + + XCTAssertTrue(photoSetVideoOrientationCalled) + XCTAssertTrue(videoSetVideoOrientationCalled) + } + + func testSetDeviceOrientation_doesNotSetOrientations_ifRecordingIsInProgress() { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + + camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil) + + mockPhotoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() } + mockVideoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() } + + camera.setDeviceOrientation(.landscapeLeft) + } + + func testSetDeviceOrientation_doesNotSetOrientations_forDuplicateUpdates() { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + var photoSetVideoOrientationCallCount = 0 + mockPhotoCaptureConnection.setVideoOrientationStub = { _ in + photoSetVideoOrientationCallCount += 1 + } + + var videoSetVideoOrientationCallCount = 0 + mockVideoCaptureConnection.setVideoOrientationStub = { _ in + videoSetVideoOrientationCallCount += 1 + } + + camera.setDeviceOrientation(.landscapeRight) + camera.setDeviceOrientation(.landscapeRight) + + XCTAssertEqual(photoSetVideoOrientationCallCount, 1) + XCTAssertEqual(videoSetVideoOrientationCallCount, 1) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.h index e17edef883f..54eff8ecff1 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.h +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.h @@ -19,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign, getter=isVideoMirroringSupported) BOOL supportsVideoMirroring; @property(nonatomic, assign, getter=isVideoOrientationSupported) BOOL supportsVideoOrientation; +// Stub that is called when the corresponding public method is called. +@property(nonatomic, copy) void (^setVideoOrientationStub)(AVCaptureVideoOrientation); + @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.m index feeaa812472..ef3813aae71 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.m @@ -6,4 +6,12 @@ @implementation MockCaptureConnection +- (void)setVideoOrientation:(AVCaptureVideoOrientation)videoOrientation { + if (self.setVideoOrientationStub) { + _setVideoOrientationStub(videoOrientation); + } else { + _videoOrientation = videoOrientation; + } +} + @end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.h index 403c9582f32..e2c757de884 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.h +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.h @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @interface MockCapturePhotoOutput : NSObject // Properties re-declared as read/write so a mocked value can be set during testing. -@property(nonatomic, strong) AVCapturePhotoOutput *photoOutput; +@property(nonatomic, strong) AVCapturePhotoOutput *avOutput; @property(nonatomic, strong) NSArray *availablePhotoCodecTypes; @property(nonatomic, assign) BOOL highResolutionCaptureEnabled; @property(nonatomic, strong) NSArray *supportedFlashModes; @@ -21,6 +21,10 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, copy) void (^capturePhotoWithSettingsStub) (AVCapturePhotoSettings *, NSObject *); +// Stub that is called when the corresponding public method is called. +@property(nonatomic, copy) NSObject * (^connectionWithMediaTypeStub) + (AVMediaType); + @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.m index 3287be95e59..4011a58220b 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCapturePhotoOutput.m @@ -12,8 +12,13 @@ - (void)capturePhotoWithSettings:(AVCapturePhotoSettings *)settings } } -- (nullable AVCaptureConnection *)connectionWithMediaType:(nonnull AVMediaType)mediaType { - return nil; +- (nullable NSObject *)connectionWithMediaType: + (nonnull AVMediaType)mediaType { + if (self.connectionWithMediaTypeStub) { + return self.connectionWithMediaTypeStub(mediaType); + } else { + return NULL; + } } @end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift new file mode 100644 index 00000000000..035b34fa674 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift @@ -0,0 +1,23 @@ +// 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. + +/// Mock implementation of `FLTCaptureVideoDataOutput` protocol which allows injecting a custom +/// implementation. +class MockCaptureVideoDataOutput: NSObject, FLTCaptureVideoDataOutput { + + var avOutput: AVCaptureVideoDataOutput = AVCaptureVideoDataOutput() + var alwaysDiscardsLateVideoFrames: Bool = false + var videoSettings: [String: Any] = [:] + + var connectionWithMediaTypeStub: ((AVMediaType) -> FLTCaptureConnection?)? + + func connection(withMediaType mediaType: AVMediaType) -> FLTCaptureConnection? { + return connectionWithMediaTypeStub?(mediaType) + } + + func setSampleBufferDelegate( + _ sampleBufferDelegate: AVCaptureVideoDataOutputSampleBufferDelegate?, + queue sampleBufferCallbackQueue: DispatchQueue? + ) {} +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/SampleBufferTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/SampleBufferTests.swift index 686f437a93c..6ea6773c870 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/SampleBufferTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/SampleBufferTests.swift @@ -55,7 +55,7 @@ private class FakeMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { } override func recommendedVideoSettingsForAssetWriter( - withFileType fileType: AVFileType, for output: AVCaptureVideoDataOutput + withFileType fileType: AVFileType, for output: FLTCaptureVideoDataOutput ) -> [String: Any]? { return [:] } @@ -100,7 +100,7 @@ final class CameraSampleBufferTests: XCTestCase { let captureSessionQueue = DispatchQueue(label: "testing") let camera = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue) XCTAssertEqual( - captureSessionQueue, camera.captureVideoOutput.sampleBufferCallbackQueue, + captureSessionQueue, camera.captureVideoOutput.avOutput.sampleBufferCallbackQueue, "Sample buffer callback queue must be the capture session queue.") } @@ -110,7 +110,8 @@ final class CameraSampleBufferTests: XCTestCase { let capturedPixelBuffer = CMSampleBufferGetImageBuffer(capturedSampleBuffer)! // Mimic sample buffer callback when captured a new video sample. camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: capturedSampleBuffer, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: capturedSampleBuffer, + from: connectionMock) let deliveredPixelBuffer = camera.copyPixelBuffer()?.takeRetainedValue() XCTAssertEqual( deliveredPixelBuffer, capturedPixelBuffer, @@ -129,7 +130,7 @@ final class CameraSampleBufferTests: XCTestCase { camera.resumeVideoRecording() camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: sampleBuffer, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: sampleBuffer, from: connectionMock) let finalRetainCount = CFGetRetainCount(sampleBuffer) XCTAssertEqual( @@ -166,7 +167,7 @@ final class CameraSampleBufferTests: XCTestCase { camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock) camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock) camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock) camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock) let expectedSamples = ["video", "audio"] @@ -207,17 +208,17 @@ final class CameraSampleBufferTests: XCTestCase { camera.pauseVideoRecording() camera.resumeVideoRecording() camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock) camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock) camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock) camera.captureOutput(nil, didOutputSampleBuffer: audioSample, from: connectionMock) XCTAssert(videoAppended && audioAppended, "Video or audio was not appended.") } func testDidOutputSampleBufferMustNotAppendSampleWhenReadyForMoreMediaDataIsFalse() { - let (camera, writerMock, adaptorMock, inputMock, connectionMock) = createCamera() + let (camera, _, adaptorMock, inputMock, connectionMock) = createCamera() let videoSample = FLTCreateTestSampleBuffer().takeRetainedValue() @@ -232,18 +233,18 @@ final class CameraSampleBufferTests: XCTestCase { inputMock.readyForMoreMediaData = true sampleAppended = false camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock) XCTAssertTrue(sampleAppended, "Sample was not appended.") inputMock.readyForMoreMediaData = false sampleAppended = false camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock) XCTAssertFalse(sampleAppended, "Sample cannot be appended when readyForMoreMediaData is NO.") } func testStopVideoRecordingWithCompletionMustCallCompletion() { - let (camera, writerMock, adaptorMock, inputMock, _) = createCamera() + let (camera, writerMock, _, _, _) = createCamera() var status = AVAssetWriter.Status.unknown writerMock.startWritingStub = { @@ -291,13 +292,13 @@ final class CameraSampleBufferTests: XCTestCase { let startWritingCalledBefore = startWritingCalled camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock) XCTAssert( (startWritingCalledBefore && videoAppended) || (startWritingCalled && !videoAppended), "The startWriting was called between sample creation and appending.") camera.captureOutput( - camera.captureVideoOutput, didOutputSampleBuffer: videoSample, from: connectionMock) + camera.captureVideoOutput.avOutput, didOutputSampleBuffer: videoSample, from: connectionMock) XCTAssert(videoAppended, "Video was not appended.") } 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 640727be94e..e22b1f0dfa7 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 @@ -224,13 +224,13 @@ - (instancetype)initWithConfiguration:(nonnull FLTCamConfiguration *)configurati } [_videoCaptureSession addInputWithNoConnections:_captureVideoInput]; - [_videoCaptureSession addOutputWithNoConnections:_captureVideoOutput]; + [_videoCaptureSession addOutputWithNoConnections:_captureVideoOutput.avOutput]; [_videoCaptureSession addConnection:connection]; _capturePhotoOutput = [[FLTDefaultCapturePhotoOutput alloc] initWithPhotoOutput:[AVCapturePhotoOutput new]]; [_capturePhotoOutput setHighResolutionCaptureEnabled:YES]; - [_videoCaptureSession addOutput:_capturePhotoOutput.photoOutput]; + [_videoCaptureSession addOutput:_capturePhotoOutput.avOutput]; _motionManager = [[CMMotionManager alloc] init]; [_motionManager startAccelerometerUpdates]; @@ -295,7 +295,8 @@ - (AVCaptureConnection *)createConnection:(NSError **)error { } // Setup video capture output. - _captureVideoOutput = [AVCaptureVideoDataOutput new]; + _captureVideoOutput = [[FLTDefaultCaptureVideoDataOutput alloc] + initWithCaptureVideoOutput:[AVCaptureVideoDataOutput new]]; _captureVideoOutput.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(_videoFormat)}; [_captureVideoOutput setAlwaysDiscardsLateVideoFrames:YES]; @@ -304,7 +305,7 @@ - (AVCaptureConnection *)createConnection:(NSError **)error { // Setup video capture connection. AVCaptureConnection *connection = [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports - output:_captureVideoOutput]; + output:_captureVideoOutput.avOutput]; if ([_captureDevice position] == AVCaptureDevicePositionFront) { connection.videoMirrored = YES; } @@ -369,17 +370,18 @@ - (void)updateOrientation { ? _lockedCaptureOrientation : _deviceOrientation; - [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput.photoOutput]; + [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput]; [self updateOrientation:orientation forCaptureOutput:_captureVideoOutput]; } - (void)updateOrientation:(UIDeviceOrientation)orientation - forCaptureOutput:(AVCaptureOutput *)captureOutput { + forCaptureOutput:(NSObject *)captureOutput { if (!captureOutput) { return; } - AVCaptureConnection *connection = [captureOutput connectionWithMediaType:AVMediaTypeVideo]; + NSObject *connection = + [captureOutput connectionWithMediaType:AVMediaTypeVideo]; if (connection && connection.isVideoOrientationSupported) { connection.videoOrientation = [self getVideoOrientationForDeviceOrientation:orientation]; } @@ -589,7 +591,7 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(NSObject *)connection { - if (output == _captureVideoOutput) { + if (output == _captureVideoOutput.avOutput) { CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); CFRetain(newBuffer); @@ -691,7 +693,7 @@ - (void)captureOutput:(AVCaptureOutput *)output // ignore audio samples until the first video sample arrives to avoid black frames // https://github.com/flutter/flutter/issues/57831 - if (_isFirstVideoSample && output != _captureVideoOutput) { + if (_isFirstVideoSample && output != _captureVideoOutput.avOutput) { return; } @@ -707,7 +709,7 @@ - (void)captureOutput:(AVCaptureOutput *)output _isFirstVideoSample = NO; } - if (output == _captureVideoOutput) { + if (output == _captureVideoOutput.avOutput) { if (_videoIsDisconnected) { _videoIsDisconnected = NO; @@ -1056,7 +1058,7 @@ - (void)setDescriptionWhileRecording:(NSString *)cameraName _captureDevice = self.captureDeviceFactory(); - AVCaptureConnection *oldConnection = + NSObject *oldConnection = [_captureVideoOutput connectionWithMediaType:AVMediaTypeVideo]; // Stop video capture from the old output. @@ -1065,7 +1067,7 @@ - (void)setDescriptionWhileRecording:(NSString *)cameraName // Remove the old video capture connections. [_videoCaptureSession beginConfiguration]; [_videoCaptureSession removeInput:_captureVideoInput]; - [_videoCaptureSession removeOutput:_captureVideoOutput]; + [_videoCaptureSession removeOutput:_captureVideoOutput.avOutput]; NSError *error = nil; AVCaptureConnection *newConnection = [self createConnection:&error]; @@ -1085,11 +1087,11 @@ - (void)setDescriptionWhileRecording:(NSString *)cameraName message:@"Unable switch video input" details:nil]); [_videoCaptureSession addInputWithNoConnections:_captureVideoInput]; - if (![_videoCaptureSession canAddOutput:_captureVideoOutput]) + if (![_videoCaptureSession canAddOutput:_captureVideoOutput.avOutput]) completion([FlutterError errorWithCode:@"VideoError" message:@"Unable switch video output" details:nil]); - [_videoCaptureSession addOutputWithNoConnections:_captureVideoOutput]; + [_videoCaptureSession addOutputWithNoConnections:_captureVideoOutput.avOutput]; if (![_videoCaptureSession canAddConnection:newConnection]) completion([FlutterError errorWithCode:@"VideoError" message:@"Unable switch video connection" diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m index 316c38dacf7..cb1bdc9e070 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m @@ -55,8 +55,9 @@ - (void)addInput:(NSObject *)writerInput - (nullable NSDictionary *) recommendedVideoSettingsForAssetWriterWithFileType:(AVFileType)fileType - forOutput:(AVCaptureVideoDataOutput *)output { - return [output recommendedVideoSettingsForAssetWriterWithOutputFileType:fileType]; + forOutput: + (NSObject *)output { + return [output.avOutput recommendedVideoSettingsForAssetWriterWithOutputFileType:fileType]; } @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m index be61763536e..87f05737289 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m @@ -4,11 +4,9 @@ #import "./include/camera_avfoundation/FLTCaptureConnection.h" -@interface FLTDefaultCaptureConnection () -@property(nonatomic, strong) AVCaptureConnection *connection; -@end - -@implementation FLTDefaultCaptureConnection +@implementation FLTDefaultCaptureConnection { + AVCaptureConnection *_connection; +} - (instancetype)initWithConnection:(AVCaptureConnection *)connection { self = [super init]; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m index aaf2a9f7c03..f975119b723 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m @@ -5,44 +5,46 @@ #import "./include/camera_avfoundation/FLTCapturePhotoOutput.h" @implementation FLTDefaultCapturePhotoOutput { - AVCapturePhotoOutput *_photoOutput; + AVCapturePhotoOutput *_avOutput; } - (instancetype)initWithPhotoOutput:(AVCapturePhotoOutput *)photoOutput { self = [super init]; if (self) { - _photoOutput = photoOutput; + _avOutput = photoOutput; } return self; } -- (AVCapturePhotoOutput *)photoOutput { - return _photoOutput; +- (AVCapturePhotoOutput *)avOutput { + return _avOutput; } - (NSArray *)availablePhotoCodecTypes { - return _photoOutput.availablePhotoCodecTypes; + return _avOutput.availablePhotoCodecTypes; } - (BOOL)highResolutionCaptureEnabled { - return _photoOutput.isHighResolutionCaptureEnabled; + return _avOutput.isHighResolutionCaptureEnabled; } - (void)setHighResolutionCaptureEnabled:(BOOL)enabled { - [_photoOutput setHighResolutionCaptureEnabled:enabled]; + [_avOutput setHighResolutionCaptureEnabled:enabled]; } - (void)capturePhotoWithSettings:(AVCapturePhotoSettings *)settings delegate:(NSObject *)delegate { - [_photoOutput capturePhotoWithSettings:settings delegate:delegate]; + [_avOutput capturePhotoWithSettings:settings delegate:delegate]; } -- (nullable AVCaptureConnection *)connectionWithMediaType:(nonnull AVMediaType)mediaType { - return [_photoOutput connectionWithMediaType:mediaType]; +- (nullable NSObject *)connectionWithMediaType: + (nonnull AVMediaType)mediaType { + return [[FLTDefaultCaptureConnection alloc] + initWithConnection:[_avOutput connectionWithMediaType:mediaType]]; } - (NSArray *)supportedFlashModes { - return _photoOutput.supportedFlashModes; + return _avOutput.supportedFlashModes; } @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m new file mode 100644 index 00000000000..d3b9516f4b0 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m @@ -0,0 +1,51 @@ +// 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 "./include/camera_avfoundation/FLTCaptureVideoDataOutput.h" + +@implementation FLTDefaultCaptureVideoDataOutput { + AVCaptureVideoDataOutput *_avOutput; +} + +- (instancetype)initWithCaptureVideoOutput:(AVCaptureVideoDataOutput *)videoOutput { + self = [super init]; + if (self) { + _avOutput = videoOutput; + } + return self; +} + +- (AVCaptureVideoDataOutput *)avOutput { + return _avOutput; +} + +- (BOOL)alwaysDiscardsLateVideoFrames { + return _avOutput.alwaysDiscardsLateVideoFrames; +} + +- (void)setAlwaysDiscardsLateVideoFrames:(BOOL)alwaysDiscardsLateVideoFrames { + _avOutput.alwaysDiscardsLateVideoFrames = alwaysDiscardsLateVideoFrames; +} + +- (NSDictionary *)videoSettings { + return _avOutput.videoSettings; +} + +- (void)setVideoSettings:(NSDictionary *)videoSettings { + _avOutput.videoSettings = videoSettings; +} + +- (nullable NSObject *)connectionWithMediaType: + (nonnull AVMediaType)mediaType { + return [[FLTDefaultCaptureConnection alloc] + initWithConnection:[_avOutput connectionWithMediaType:mediaType]]; +} + +- (void)setSampleBufferDelegate: + (nullable id)sampleBufferDelegate + queue:(nullable dispatch_queue_t)sampleBufferCallbackQueue { + [_avOutput setSampleBufferDelegate:sampleBufferDelegate queue:sampleBufferCallbackQueue]; +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h index 14f5db2c7d0..a2b3d6205e7 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h @@ -8,6 +8,7 @@ #import "FLTAssetWriter.h" #import "FLTCaptureDevice.h" #import "FLTCaptureSession.h" +#import "FLTCaptureVideoDataOutput.h" NS_ASSUME_NONNULL_BEGIN @@ -107,12 +108,13 @@ NS_ASSUME_NONNULL_BEGIN * @abstract Specifies the recommended video settings for `AVCaptureVideoDataOutput`. * @param fileType Specifies the UTI of the file type to be written (see AVMediaFormat.h for a list * of file format UTIs). - * @param output The `AVCaptureVideoDataOutput` instance. + * @param output The `FLTCaptureVideoDataOutput` instance. * @result A fully populated dictionary of keys and values that are compatible with AVAssetWriter. */ - (nullable NSDictionary *) recommendedVideoSettingsForAssetWriterWithFileType:(AVFileType)fileType - forOutput:(AVCaptureVideoDataOutput *)output; + forOutput: + (NSObject *)output; @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h index de4caf7b145..3e3a44922dd 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h @@ -6,6 +6,7 @@ #import "FLTCaptureConnection.h" #import "FLTCaptureDevice.h" #import "FLTCapturePhotoOutput.h" +#import "FLTCaptureVideoDataOutput.h" #import "FLTDeviceOrientationProviding.h" #import "FLTSavePhotoDelegate.h" @@ -26,7 +27,7 @@ @interface FLTCam () /// The output for video capturing. -@property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput; +@property(strong, nonatomic) NSObject *captureVideoOutput; /// The output for photo capturing. Exposed setter for unit tests. @property(strong, nonatomic) NSObject *capturePhotoOutput; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h index ee1562c2825..f6685d715c4 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h @@ -10,10 +10,6 @@ NS_ASSUME_NONNULL_BEGIN /// `AVCaptureConnection` in tests. @protocol FLTCaptureConnection -/// Underlying `AVCaptureConnection` instance. All methods and properties are passed through to -/// this. -@property(nonatomic, readonly) AVCaptureConnection *connection; - @property(nonatomic, getter=isVideoMirrored) BOOL videoMirrored; @property(nonatomic) AVCaptureVideoOrientation videoOrientation; @property(nonatomic, readonly) NSArray *inputPorts; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureOutput.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureOutput.h new file mode 100644 index 00000000000..9fc22bd7c1c --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureOutput.h @@ -0,0 +1,23 @@ +// 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 Foundation; +@import AVFoundation; + +#import "FLTCaptureConnection.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol which is a direct passthrough to `AVCapturePhotoOutput`. It exists to allow mocking +/// `AVCapturePhotoOutput` in tests. +@protocol FLTCaptureOutput + +///// The underlying instance of `AVCapturePhotoOutput`. +//@property(nonatomic, readonly) AVCaptureOutput *avOutput; + +- (nullable NSObject *)connectionWithMediaType:(AVMediaType)mediaType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h index f569f739c52..2d6dcfb92bf 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h @@ -5,16 +5,16 @@ @import Foundation; @import AVFoundation; -#import "FLTCapturePhotoOutput.h" +#import "FLTCaptureOutput.h" NS_ASSUME_NONNULL_BEGIN /// A protocol which is a direct passthrough to `AVCapturePhotoOutput`. It exists to allow mocking /// `AVCapturePhotoOutput` in tests. -@protocol FLTCapturePhotoOutput +@protocol FLTCapturePhotoOutput /// The underlying instance of `AVCapturePhotoOutput`. -@property(nonatomic, readonly) AVCapturePhotoOutput *photoOutput; +@property(nonatomic, readonly) AVCapturePhotoOutput *avOutput; @property(nonatomic, readonly) NSArray *availablePhotoCodecTypes; @property(nonatomic, assign) BOOL highResolutionCaptureEnabled; @@ -22,7 +22,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)capturePhotoWithSettings:(AVCapturePhotoSettings *)settings delegate:(NSObject *)delegate; -- (nullable AVCaptureConnection *)connectionWithMediaType:(AVMediaType)mediaType; @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h new file mode 100644 index 00000000000..89200ab08fe --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h @@ -0,0 +1,40 @@ +// 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 Foundation; +@import AVFoundation; + +#import "FLTCaptureOutput.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol which is a direct passthrough to `AVCaptureVideoDataOutput`. It exists to allow +/// mocking `AVCaptureVideoDataOutput` in tests. +@protocol FLTCaptureVideoDataOutput + +/// The underlying instance of `AVCaptureVideoDataOutput`. +@property(nonatomic, readonly) AVCaptureVideoDataOutput *avOutput; + +@property(nonatomic) BOOL alwaysDiscardsLateVideoFrames; + +@property(nonatomic, copy, null_resettable) NSDictionary *videoSettings; + +- (void)setSampleBufferDelegate: + (nullable id)sampleBufferDelegate + queue:(nullable dispatch_queue_t)sampleBufferCallbackQueue; + +@end + +/// A default implementation of `FLTCaptureVideoDataOutput` which wraps an instance of +/// `AVCaptureVideoDataOutput`. +@interface FLTDefaultCaptureVideoDataOutput : NSObject + +/// Initializes an instance of `FLTDefaultCaptureVideDataOutput` with the given +/// `AVCaptureVideoDataOutput`. All method and property calls will be forwarded to the given +/// `videoOutput`. +- (instancetype)initWithCaptureVideoOutput:(AVCaptureVideoDataOutput *)videoOutput; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 6263c6531b2..4b9e4d459f9 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.18+10 +version: 0.9.18+11 environment: sdk: ^3.4.0 From 9b5fd3b2ed59d0686cf20617c8a5df94e0b05830 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 14 Mar 2025 09:59:43 +0100 Subject: [PATCH 2/4] Remove unncessary type declarations --- .../ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift index 035b34fa674..690efd38092 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureVideoDataOutput.swift @@ -6,8 +6,8 @@ /// implementation. class MockCaptureVideoDataOutput: NSObject, FLTCaptureVideoDataOutput { - var avOutput: AVCaptureVideoDataOutput = AVCaptureVideoDataOutput() - var alwaysDiscardsLateVideoFrames: Bool = false + var avOutput = AVCaptureVideoDataOutput() + var alwaysDiscardsLateVideoFrames = false var videoSettings: [String: Any] = [:] var connectionWithMediaTypeStub: ((AVMediaType) -> FLTCaptureConnection?)? From c4c20a7a5bc0a7db959e6fe17db70dd47f692e11 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 14 Mar 2025 09:59:59 +0100 Subject: [PATCH 3/4] Replace ivars with properties --- .../ios/Runner.xcodeproj/project.pbxproj | 2 ++ .../FLTCaptureConnection.m | 22 +++++++++-------- .../FLTCapturePhotoOutput.m | 24 +++++++++---------- .../FLTCaptureVideoDataOutput.m | 24 +++++++++---------- 4 files changed, 36 insertions(+), 36 deletions(-) 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 95853f2b7a1..d12a1bcc2d9 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ E16602952D8471C0003CFE12 /* FLTCamZoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */; }; E1A5F4E32D80259C0005BA64 /* FLTCamSetFlashModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */; }; E15139182D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */; }; + E1A5F4E32D80259C0005BA64 /* FLTCamSetFlashModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */; }; E1FFEAAD2D6C8DD700B14107 /* MockFLTCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */; }; E1FFEAAF2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */; }; E1FFEAB12D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */; }; @@ -156,6 +157,7 @@ E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamZoomTests.swift; sourceTree = ""; }; E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetFlashModeTests.swift; sourceTree = ""; }; E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetDeviceOrientationTests.swift; sourceTree = ""; }; + E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetFlashModeTests.swift; sourceTree = ""; }; E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCam.swift; sourceTree = ""; }; E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginCreateCameraTests.swift; sourceTree = ""; }; E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginInitializeCameraTests.swift; sourceTree = ""; }; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m index 87f05737289..c6e1a4a3fa5 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureConnection.m @@ -4,9 +4,11 @@ #import "./include/camera_avfoundation/FLTCaptureConnection.h" -@implementation FLTDefaultCaptureConnection { - AVCaptureConnection *_connection; -} +@interface FLTDefaultCaptureConnection () +@property(nonatomic, strong) AVCaptureConnection *connection; +@end + +@implementation FLTDefaultCaptureConnection - (instancetype)initWithConnection:(AVCaptureConnection *)connection { self = [super init]; @@ -17,31 +19,31 @@ - (instancetype)initWithConnection:(AVCaptureConnection *)connection { } - (BOOL)isVideoMirroringSupported { - return _connection.isVideoMirroringSupported; + return self.connection.isVideoMirroringSupported; } - (BOOL)isVideoOrientationSupported { - return _connection.isVideoOrientationSupported; + return self.connection.isVideoOrientationSupported; } - (void)setVideoMirrored:(BOOL)videoMirrored { - _connection.videoMirrored = videoMirrored; + self.connection.videoMirrored = videoMirrored; } - (BOOL)isVideoMirrored { - return _connection.isVideoMirrored; + return self.connection.isVideoMirrored; } - (void)setVideoOrientation:(AVCaptureVideoOrientation)videoOrientation { - _connection.videoOrientation = videoOrientation; + self.connection.videoOrientation = videoOrientation; } - (AVCaptureVideoOrientation)videoOrientation { - return _connection.videoOrientation; + return self.connection.videoOrientation; } - (NSArray *)inputPorts { - return _connection.inputPorts; + return self.connection.inputPorts; } @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m index f975119b723..5549eb84d08 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCapturePhotoOutput.m @@ -4,9 +4,11 @@ #import "./include/camera_avfoundation/FLTCapturePhotoOutput.h" -@implementation FLTDefaultCapturePhotoOutput { - AVCapturePhotoOutput *_avOutput; -} +@interface FLTDefaultCapturePhotoOutput () +@property(nonatomic, strong) AVCapturePhotoOutput *avOutput; +@end + +@implementation FLTDefaultCapturePhotoOutput - (instancetype)initWithPhotoOutput:(AVCapturePhotoOutput *)photoOutput { self = [super init]; @@ -16,35 +18,31 @@ - (instancetype)initWithPhotoOutput:(AVCapturePhotoOutput *)photoOutput { return self; } -- (AVCapturePhotoOutput *)avOutput { - return _avOutput; -} - - (NSArray *)availablePhotoCodecTypes { - return _avOutput.availablePhotoCodecTypes; + return self.avOutput.availablePhotoCodecTypes; } - (BOOL)highResolutionCaptureEnabled { - return _avOutput.isHighResolutionCaptureEnabled; + return self.avOutput.isHighResolutionCaptureEnabled; } - (void)setHighResolutionCaptureEnabled:(BOOL)enabled { - [_avOutput setHighResolutionCaptureEnabled:enabled]; + [self.avOutput setHighResolutionCaptureEnabled:enabled]; } - (void)capturePhotoWithSettings:(AVCapturePhotoSettings *)settings delegate:(NSObject *)delegate { - [_avOutput capturePhotoWithSettings:settings delegate:delegate]; + [self.avOutput capturePhotoWithSettings:settings delegate:delegate]; } - (nullable NSObject *)connectionWithMediaType: (nonnull AVMediaType)mediaType { return [[FLTDefaultCaptureConnection alloc] - initWithConnection:[_avOutput connectionWithMediaType:mediaType]]; + initWithConnection:[self.avOutput connectionWithMediaType:mediaType]]; } - (NSArray *)supportedFlashModes { - return _avOutput.supportedFlashModes; + return self.avOutput.supportedFlashModes; } @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m index d3b9516f4b0..1505ef15fe2 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureVideoDataOutput.m @@ -4,9 +4,11 @@ #import "./include/camera_avfoundation/FLTCaptureVideoDataOutput.h" -@implementation FLTDefaultCaptureVideoDataOutput { - AVCaptureVideoDataOutput *_avOutput; -} +@interface FLTDefaultCaptureVideoDataOutput () +@property(nonatomic, strong) AVCaptureVideoDataOutput *avOutput; +@end + +@implementation FLTDefaultCaptureVideoDataOutput - (instancetype)initWithCaptureVideoOutput:(AVCaptureVideoDataOutput *)videoOutput { self = [super init]; @@ -16,36 +18,32 @@ - (instancetype)initWithCaptureVideoOutput:(AVCaptureVideoDataOutput *)videoOutp return self; } -- (AVCaptureVideoDataOutput *)avOutput { - return _avOutput; -} - - (BOOL)alwaysDiscardsLateVideoFrames { - return _avOutput.alwaysDiscardsLateVideoFrames; + return self.avOutput.alwaysDiscardsLateVideoFrames; } - (void)setAlwaysDiscardsLateVideoFrames:(BOOL)alwaysDiscardsLateVideoFrames { - _avOutput.alwaysDiscardsLateVideoFrames = alwaysDiscardsLateVideoFrames; + self.avOutput.alwaysDiscardsLateVideoFrames = alwaysDiscardsLateVideoFrames; } - (NSDictionary *)videoSettings { - return _avOutput.videoSettings; + return self.avOutput.videoSettings; } - (void)setVideoSettings:(NSDictionary *)videoSettings { - _avOutput.videoSettings = videoSettings; + self.avOutput.videoSettings = videoSettings; } - (nullable NSObject *)connectionWithMediaType: (nonnull AVMediaType)mediaType { return [[FLTDefaultCaptureConnection alloc] - initWithConnection:[_avOutput connectionWithMediaType:mediaType]]; + initWithConnection:[self.avOutput connectionWithMediaType:mediaType]]; } - (void)setSampleBufferDelegate: (nullable id)sampleBufferDelegate queue:(nullable dispatch_queue_t)sampleBufferCallbackQueue { - [_avOutput setSampleBufferDelegate:sampleBufferDelegate queue:sampleBufferCallbackQueue]; + [self.avOutput setSampleBufferDelegate:sampleBufferDelegate queue:sampleBufferCallbackQueue]; } @end From db239d1f23b739cd12eab572508c0c9a671efd1d Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Wed, 19 Mar 2025 08:29:27 +0100 Subject: [PATCH 4/4] Add comments to wrapper methods and properties --- .../include/camera_avfoundation/FLTCaptureConnection.h | 9 +++++++++ .../include/camera_avfoundation/FLTCapturePhotoOutput.h | 6 ++++++ .../camera_avfoundation/FLTCaptureVideoDataOutput.h | 3 +++ 3 files changed, 18 insertions(+) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h index f6685d715c4..a0de806f10e 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureConnection.h @@ -10,10 +10,19 @@ NS_ASSUME_NONNULL_BEGIN /// `AVCaptureConnection` in tests. @protocol FLTCaptureConnection +/// Corresponds to the `videoMirrored` property of `AVCaptureConnection` @property(nonatomic, getter=isVideoMirrored) BOOL videoMirrored; + +/// Corresponds to the `videoOrientation` property of `AVCaptureConnection` @property(nonatomic) AVCaptureVideoOrientation videoOrientation; + +/// Corresponds to the `inputPorts` property of `AVCaptureConnection` @property(nonatomic, readonly) NSArray *inputPorts; + +/// Corresponds to the `supportsVideoMirroring` property of `AVCaptureConnection` @property(nonatomic, readonly, getter=isVideoMirroringSupported) BOOL supportsVideoMirroring; + +/// Corresponds to the `supportsVideoOrientation` property of `AVCaptureConnection` @property(nonatomic, readonly, getter=isVideoOrientationSupported) BOOL supportsVideoOrientation; @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h index 2d6dcfb92bf..9a2ac8f0964 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCapturePhotoOutput.h @@ -16,10 +16,16 @@ NS_ASSUME_NONNULL_BEGIN /// The underlying instance of `AVCapturePhotoOutput`. @property(nonatomic, readonly) AVCapturePhotoOutput *avOutput; +/// Corresponds to the `availablePhotoCodecTypes` property of `AVCapturePhotoOutput` @property(nonatomic, readonly) NSArray *availablePhotoCodecTypes; + +/// Corresponds to the `highResolutionCaptureEnabled` property of `AVCapturePhotoOutput` @property(nonatomic, assign) BOOL highResolutionCaptureEnabled; + +/// Corresponds to the `supportedFlashModes` property of `AVCapturePhotoOutput` @property(nonatomic, readonly) NSArray *supportedFlashModes; +/// Corresponds to the `capturePhotoWithSettings` method of `AVCapturePhotoOutput` - (void)capturePhotoWithSettings:(AVCapturePhotoSettings *)settings delegate:(NSObject *)delegate; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h index 89200ab08fe..60e99999476 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureVideoDataOutput.h @@ -16,10 +16,13 @@ NS_ASSUME_NONNULL_BEGIN /// The underlying instance of `AVCaptureVideoDataOutput`. @property(nonatomic, readonly) AVCaptureVideoDataOutput *avOutput; +/// Corresponds to the `alwaysDiscardsLateVideoFrames` property of `AVCaptureVideoDataOutput` @property(nonatomic) BOOL alwaysDiscardsLateVideoFrames; +/// Corresponds to the `videoSettings` property of `AVCaptureVideoDataOutput` @property(nonatomic, copy, null_resettable) NSDictionary *videoSettings; +/// Corresponds to the `setSampleBufferDelegate` method of `AVCaptureVideoDataOutput` - (void)setSampleBufferDelegate: (nullable id)sampleBufferDelegate queue:(nullable dispatch_queue_t)sampleBufferCallbackQueue;