diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 113f2583fb29..51ad42e38854 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.19 + +* Migrates the CameraPlugin class to Swift. +* Fixes camera name being ignored in `setDescriptionWhileRecording`. + ## 0.9.18+14 * Creates Swift Package Manager target for Swift implementation. 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 f58349a9d99b..7d6ca0e5068b 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97DB234D2D566D0700CEFE66 /* CameraPreviewPauseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */; }; - E0CDBAC227CD9729002561D9 /* QueueTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E0CDBAC127CD9729002561D9 /* QueueTestUtils.m */; }; E11D6A8F2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */; }; E11D6A912D82C7740031E6C5 /* FLTCamExposureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */; }; E12C4FF62D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */; }; @@ -131,8 +130,6 @@ 9DDC4CE84A8B378AE4A8CD9C /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A8F314CD1C64E9257EBC811D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; B61D98BBC8FB276D1C4A7BB2 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - E0CDBAC027CD9729002561D9 /* QueueTestUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QueueTestUtils.h; sourceTree = ""; }; - E0CDBAC127CD9729002561D9 /* QueueTestUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueTestUtils.m; sourceTree = ""; }; E11D6A8E2D81B81D0031E6C5 /* MockCaptureVideoDataOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCaptureVideoDataOutput.swift; sourceTree = ""; }; E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamExposureTests.swift; sourceTree = ""; }; E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginDelegatingMethodTests.swift; sourceTree = ""; }; @@ -147,12 +144,12 @@ E142F13F2D85AD7900824824 /* MockCaptureConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCaptureConnection.swift; sourceTree = ""; }; E142F1412D85AFA400824824 /* MockGlobalEventApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGlobalEventApi.swift; sourceTree = ""; }; E15139172D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetDeviceOrientationTests.swift; sourceTree = ""; }; + E15BC7E32D86D08700F66474 /* MockFlutterTextureRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFlutterTextureRegistry.swift; sourceTree = ""; }; + E15BC7E52D86D17D00F66474 /* MockFlutterBinaryMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFlutterBinaryMessenger.swift; sourceTree = ""; }; E15BC7E72D86D29F00F66474 /* MockAssetWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAssetWriter.swift; sourceTree = ""; }; E15BC7E92D86D41F00F66474 /* MockAssetWriterInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAssetWriterInput.swift; sourceTree = ""; }; E15BC7EB2D86D50200F66474 /* MockAssetWriterInputPixelBufferAdaptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAssetWriterInputPixelBufferAdaptor.swift; sourceTree = ""; }; E15BC7ED2D86D85500F66474 /* MockCaptureDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCaptureDevice.swift; sourceTree = ""; }; - E15BC7E32D86D08700F66474 /* MockFlutterTextureRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFlutterTextureRegistry.swift; sourceTree = ""; }; - E15BC7E52D86D17D00F66474 /* MockFlutterBinaryMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFlutterBinaryMessenger.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 = ""; }; E1ABED702D943DC700AED9CC /* MockCaptureDeviceInputFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockCaptureDeviceInputFactory.swift; sourceTree = ""; }; @@ -190,8 +187,6 @@ children = ( 7F29EB3F2D281C6D00740257 /* Mocks */, 03BB766C2665316900CE5A93 /* Info.plist */, - E0CDBAC027CD9729002561D9 /* QueueTestUtils.h */, - E0CDBAC127CD9729002561D9 /* QueueTestUtils.m */, E142681E2D8566230046CBBC /* CameraTestUtils.swift */, 979B3DF92D5B6BA2009BDE1A /* ExceptionCatcher.h */, 979B3DFA2D5B6BC7009BDE1A /* ExceptionCatcher.m */, @@ -565,7 +560,6 @@ E15139182D80980900FEE47B /* FLTCamSetDeviceOrientationTests.swift in Sources */, 972CA92B2D5A1D8C004B846F /* CameraPropertiesTests.swift in Sources */, E15BC7E42D86D08700F66474 /* MockFlutterTextureRegistry.swift in Sources */, - E0CDBAC227CD9729002561D9 /* QueueTestUtils.m in Sources */, 978296CF2D5F744B0009BDD3 /* PhotoCaptureTests.swift in Sources */, 979B3E002D5B9E6C009BDE1A /* CameraMethodChannelTests.swift in Sources */, E142F13C2D8596F100824824 /* MockCaptureDeviceFormat.swift in Sources */, diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift index c2fd88f2aece..f371640e89bb 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift @@ -23,7 +23,8 @@ final class AvailableCamerasTest: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { MockCaptureSession() }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") ) } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift index 7f6b86cca080..e2a746524118 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift @@ -12,8 +12,10 @@ import XCTest #endif final class CameraCaptureSessionQueueRaceConditionTests: XCTestCase { - private func createCameraPlugin() -> CameraPlugin { - return CameraPlugin( + private func createCameraPlugin() -> (CameraPlugin, DispatchQueue) { + let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + + let cameraPlugin = CameraPlugin( registry: MockFlutterTextureRegistry(), messenger: MockFlutterBinaryMessenger(), globalAPI: MockGlobalEventApi(), @@ -21,12 +23,15 @@ final class CameraCaptureSessionQueueRaceConditionTests: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { MockCaptureSession() }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue ) + + return (cameraPlugin, captureSessionQueue) } func testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition() { - let cameraPlugin = createCameraPlugin() + let (cameraPlugin, captureSessionQueue) = createCameraPlugin() let disposeExpectation = expectation(description: "dispose's result block must be called") let createExpectation = expectation(description: "create's result block must be called") @@ -55,6 +60,6 @@ final class CameraCaptureSessionQueueRaceConditionTests: XCTestCase { // `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:` // API will cause a crash. XCTAssertNotNil( - cameraPlugin.captureSessionQueue, "captureSessionQueue must not be nil after create method.") + captureSessionQueue, "captureSessionQueue must not be nil after create method.") } } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift index d067ddc98394..d621a4077c47 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift @@ -22,7 +22,8 @@ final class CameraMethodChannelTests: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { session }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") ) } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift index 0fc47b518abf..3b12f1d7e823 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift @@ -23,12 +23,18 @@ private final class MockUIDevice: UIDevice { final class CameraOrientationTests: XCTestCase { private func createCameraPlugin() -> ( - CameraPlugin, MockFLTCam, MockGlobalEventApi, MockCaptureDevice, MockCameraDeviceDiscoverer + cameraPlugin: CameraPlugin, + mockCamera: MockFLTCam, + mockEventAPI: MockGlobalEventApi, + mockDevice: MockCaptureDevice, + mockDeviceDiscoverer: MockCameraDeviceDiscoverer, + captureSessionQueue: DispatchQueue ) { let mockDevice = MockCaptureDevice() let mockCamera = MockFLTCam() let mockEventAPI = MockGlobalEventApi() let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() + let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") let cameraPlugin = CameraPlugin( registry: MockFlutterTextureRegistry(), @@ -38,11 +44,19 @@ final class CameraOrientationTests: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in mockDevice }, captureSessionFactory: { MockCaptureSession() }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue ) cameraPlugin.camera = mockCamera - return (cameraPlugin, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer) + return ( + cameraPlugin, + mockCamera, + mockEventAPI, + mockDevice, + mockDeviceDiscoverer, + captureSessionQueue + ) } private func waitForRoundTrip(with queue: DispatchQueue) { @@ -55,9 +69,13 @@ final class CameraOrientationTests: XCTestCase { waitForExpectations(timeout: 30, handler: nil) } - private func sendOrientation(_ orientation: UIDeviceOrientation, to cameraPlugin: CameraPlugin) { + private func sendOrientation( + _ orientation: UIDeviceOrientation, + to cameraPlugin: CameraPlugin, + captureSessionQueue: DispatchQueue + ) { cameraPlugin.orientationChanged(createMockNotification(for: orientation)) - waitForRoundTrip(with: cameraPlugin.captureSessionQueue) + waitForRoundTrip(with: captureSessionQueue) } private func createMockNotification(for deviceOrientation: UIDeviceOrientation) -> Notification { @@ -67,36 +85,36 @@ final class CameraOrientationTests: XCTestCase { } func testOrientationNotifications() { - let (cameraPlugin, _, mockEventAPI, _, _) = createCameraPlugin() + let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() - sendOrientation(.portraitUpsideDown, to: cameraPlugin) + sendOrientation(.portraitUpsideDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .portraitDown) - sendOrientation(.portrait, to: cameraPlugin) + sendOrientation(.portrait, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .portraitUp) - sendOrientation(.landscapeLeft, to: cameraPlugin) + sendOrientation(.landscapeLeft, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeLeft) - sendOrientation(.landscapeRight, to: cameraPlugin) + sendOrientation(.landscapeRight, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeRight) } func testOrientationNotificationsNotCalledForFaceUp() { - let (cameraPlugin, _, mockEventAPI, _, _) = createCameraPlugin() - sendOrientation(.faceUp, to: cameraPlugin) + let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() + sendOrientation(.faceUp, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) } func testOrientationNotificationsNotCalledForFaceDown() { - let (cameraPlugin, _, mockEventAPI, _, _) = createCameraPlugin() - sendOrientation(.faceDown, to: cameraPlugin) + let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() + sendOrientation(.faceDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) } func testOrientationUpdateMustBeOnCaptureSessionQueue() { let queueExpectation = expectation( description: "Orientation update must happen on the capture session queue") - let (cameraPlugin, mockCamera, _, _, _) = createCameraPlugin() + let (cameraPlugin, mockCamera, _, _, _, captureSessionQueue) = createCameraPlugin() let captureSessionQueueSpecific = DispatchSpecificKey() - cameraPlugin.captureSessionQueue.setSpecific( + captureSessionQueue.setSpecific( key: captureSessionQueueSpecific, value: ()) @@ -111,7 +129,7 @@ final class CameraOrientationTests: XCTestCase { } func testOrientationChangedNoRetainCycle() { - let (_, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer) = createCameraPlugin() + let (_, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer, _) = createCameraPlugin() let captureSessionQueue = DispatchQueue(label: "capture_session_queue") weak var weakPlugin: CameraPlugin? weak var weakDevice = mockDevice @@ -125,10 +143,10 @@ final class CameraOrientationTests: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in weakDevice! }, captureSessionFactory: { MockCaptureSession() }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue ) weakPlugin = cameraPlugin - cameraPlugin.captureSessionQueue = captureSessionQueue cameraPlugin.camera = mockCamera cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft)) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginCreateCameraTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginCreateCameraTests.swift index e1f41dc7fe9a..cce002fe2abe 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginCreateCameraTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginCreateCameraTests.swift @@ -26,7 +26,8 @@ final class CameraPluginCreateCameraTests: XCTestCase { permissionManager: mockPermissionManager, deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { mockCaptureSession }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") ) return (cameraPlugin, mockPermissionManager, mockCaptureSession) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift index 7010e80afd2a..0bf445a131bf 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift @@ -24,7 +24,8 @@ final class CameraPluginDelegatingMethodTests: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { MockCaptureSession() }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") ) cameraPlugin.camera = mockCamera diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginInitializeCameraTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginInitializeCameraTests.swift index 92fbf9f72075..87b61050d395 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginInitializeCameraTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginInitializeCameraTests.swift @@ -13,10 +13,11 @@ import XCTest final class CameraPluginInitializeCameraTests: XCTestCase { private func createCameraPlugin() -> ( - CameraPlugin, MockFLTCam, MockGlobalEventApi + CameraPlugin, MockFLTCam, MockGlobalEventApi, DispatchQueue ) { let mockCamera = MockFLTCam() let mockGlobalEventApi = MockGlobalEventApi() + let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") let cameraPlugin = CameraPlugin( registry: MockFlutterTextureRegistry(), @@ -26,11 +27,12 @@ final class CameraPluginInitializeCameraTests: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { MockCaptureSession() }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue ) cameraPlugin.camera = mockCamera - return (cameraPlugin, mockCamera, mockGlobalEventApi) + return (cameraPlugin, mockCamera, mockGlobalEventApi, captureSessionQueue) } private func waitForRoundTrip(with queue: DispatchQueue) { @@ -44,7 +46,7 @@ final class CameraPluginInitializeCameraTests: XCTestCase { } func testInitializeCamera_setsCameraOnFrameAvailableCallback() { - let (cameraPlugin, mockCamera, _) = createCameraPlugin() + let (cameraPlugin, mockCamera, _, _) = createCameraPlugin() let expectation = expectation(description: "Initialization completed") var onFrameAvailableSet = false @@ -64,7 +66,7 @@ final class CameraPluginInitializeCameraTests: XCTestCase { } func testInitializeCamera_setsCameraDartAPI() { - let (cameraPlugin, mockCamera, _) = createCameraPlugin() + let (cameraPlugin, mockCamera, _, _) = createCameraPlugin() let expectation = expectation(description: "Initialization completed") var dartAPISet = false @@ -84,20 +86,20 @@ final class CameraPluginInitializeCameraTests: XCTestCase { } func testInitializeCamera_sendsDeviceOrientation() { - let (cameraPlugin, _, mockGlobalEventApi) = createCameraPlugin() + let (cameraPlugin, _, mockGlobalEventApi, captureSessionQueue) = createCameraPlugin() cameraPlugin.initializeCamera(0, withImageFormat: FCPPlatformImageFormatGroup.bgra8888) { error in XCTAssertNil(error) } - waitForRoundTrip(with: cameraPlugin.captureSessionQueue) + waitForRoundTrip(with: captureSessionQueue) XCTAssertTrue(mockGlobalEventApi.deviceOrientationChangedCalled) } func testInitializeCamera_startsCamera() { - let (cameraPlugin, mockCamera, _) = createCameraPlugin() + let (cameraPlugin, mockCamera, _, _) = createCameraPlugin() let expectation = expectation(description: "Initialization completed") var startCalled = false diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift index a0ea59fb4522..c8ed7814251b 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift @@ -35,7 +35,7 @@ final class CameraSessionPresetsTests: XCTestCase { } let configuration = CameraTestUtils.createTestCameraConfiguration() - configuration.captureDeviceFactory = { captureDeviceMock } + configuration.captureDeviceFactory = { _ in captureDeviceMock } configuration.videoDimensionsForFormat = { format in return CMVideoDimensions(width: 1, height: 1) } @@ -65,7 +65,7 @@ final class CameraSessionPresetsTests: XCTestCase { configuration.videoCaptureSession = videoSessionMock configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( resolutionPreset: FCPPlatformResolutionPreset.max) - configuration.captureDeviceFactory = { MockCaptureDevice() } + configuration.captureDeviceFactory = { _ in MockCaptureDevice() } let _ = FLTCam(configuration: configuration, error: nil) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index 64b8e10f5044..aedb0a21a73a 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -163,7 +163,8 @@ final class CameraSettingsTests: XCTestCase { permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in mockDevice }, captureSessionFactory: { mockSession }, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") ) let expectation = self.expectation(description: "Result finished") diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.swift index 805a4095e458..46e443d4e65f 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.swift @@ -60,10 +60,11 @@ enum CameraTestUtils { mediaSettings: createDefaultMediaSettings( resolutionPreset: FCPPlatformResolutionPreset.medium), mediaSettingsWrapper: FLTCamMediaSettingsAVWrapper(), - captureDeviceFactory: { captureDeviceMock }, + captureDeviceFactory: { _ in captureDeviceMock }, captureSessionFactory: { videoSessionMock }, captureSessionQueue: captureSessionQueue, - captureDeviceInputFactory: MockCaptureDeviceInputFactory() + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + initialCameraName: "camera_name" ) configuration.videoCaptureSession = videoSessionMock diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift index 8a749524a8e1..dc910d42f659 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift @@ -17,7 +17,7 @@ final class FLTCamExposureTests: XCTestCase { let mockDeviceOrientationProvider = MockDeviceOrientationProvider() let configuration = CameraTestUtils.createTestCameraConfiguration() - configuration.captureDeviceFactory = { mockDevice } + configuration.captureDeviceFactory = { _ in mockDevice } configuration.deviceOrientationProvider = mockDeviceOrientationProvider let camera = FLTCam(configuration: configuration, error: nil) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamFocusTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamFocusTests.swift index 1c1090dd74ae..bac25ca2d120 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamFocusTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamFocusTests.swift @@ -18,7 +18,7 @@ final class FLTCamSetFocusModeTests: XCTestCase { let mockDeviceOrientationProvider = MockDeviceOrientationProvider() let configuration = CameraTestUtils.createTestCameraConfiguration() - configuration.captureDeviceFactory = { mockDevice } + configuration.captureDeviceFactory = { _ in mockDevice } configuration.deviceOrientationProvider = mockDeviceOrientationProvider let camera = FLTCam(configuration: configuration, error: nil) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetFlashModeTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetFlashModeTests.swift index 16d99c0dc3c0..ce742ddde5b0 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetFlashModeTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamSetFlashModeTests.swift @@ -18,7 +18,7 @@ final class FLTCamSetFlashModeTests: XCTestCase { let mockCapturePhotoOutput = MockCapturePhotoOutput() let configuration = CameraTestUtils.createTestCameraConfiguration() - configuration.captureDeviceFactory = { mockDevice } + configuration.captureDeviceFactory = { _ in mockDevice } let camera = FLTCam(configuration: configuration, error: nil) camera.capturePhotoOutput = mockCapturePhotoOutput diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.swift index add21edbc929..249871bfa8e3 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.swift @@ -17,7 +17,7 @@ final class FLTCamZoomTests: XCTestCase { let mockDevice = MockCaptureDevice() let configuration = CameraTestUtils.createTestCameraConfiguration() - configuration.captureDeviceFactory = { mockDevice } + configuration.captureDeviceFactory = { _ in mockDevice } let camera = FLTCam(configuration: configuration, error: nil) return (camera, mockDevice) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift index 6fbdc1ef04a9..5e0f487b76c8 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift @@ -173,7 +173,7 @@ final class PhotoCaptureTests: XCTestCase { FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) let configuration = CameraTestUtils.createTestCameraConfiguration() configuration.captureSessionQueue = captureSessionQueue - configuration.captureDeviceFactory = { captureDeviceMock } + configuration.captureDeviceFactory = { _ in captureDeviceMock } let cam = FLTCam(configuration: configuration, error: nil) let filePath = "test" diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueTestUtils.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueTestUtils.h deleted file mode 100644 index 0d5d7ee6bd18..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueTestUtils.h +++ /dev/null @@ -1,16 +0,0 @@ -// 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; - -NS_ASSUME_NONNULL_BEGIN - -/// Calls `dispatch_queue_set_specific` with a key that is used to identify the queue. -/// This method is needed for comaptibility of Swift tests with Objective-C code. -/// In Swift, the API for settinng key-value pairs on a queue is different, so Swift tests -/// need to call this method to set the key-value pair on the queue in a way that's -/// compatible with the existing Objective-C code. -extern void FLTDispatchQueueSetSpecific(dispatch_queue_t queue, const void *key); - -NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueTestUtils.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueTestUtils.m deleted file mode 100644 index 32198b7f461c..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/QueueTestUtils.m +++ /dev/null @@ -1,11 +0,0 @@ -// 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 "QueueTestUtils.h" - -@import camera_avfoundation; - -void FLTDispatchQueueSetSpecific(dispatch_queue_t queue, const void *key) { - dispatch_queue_set_specific(queue, key, (void *)key, NULL); -} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h index d7eaeeac4e1f..83c9c862be13 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h @@ -3,4 +3,3 @@ // found in the LICENSE file. #import "ExceptionCatcher.h" -#import "QueueTestUtils.h" 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 new file mode 100644 index 000000000000..a058afb50e4a --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift @@ -0,0 +1,531 @@ +// 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 Flutter +import ObjectiveC + +// Import Objectice-C part of the implementation when SwiftPM is used. +#if canImport(camera_avfoundation_objc) + import camera_avfoundation_objc +#endif + +public final class CameraPlugin: NSObject, FlutterPlugin { + private let registry: FlutterTextureRegistry + private let messenger: FlutterBinaryMessenger + private let globalEventAPI: FCPCameraGlobalEventApi + private let deviceDiscoverer: FLTCameraDeviceDiscovering + private let permissionManager: FLTCameraPermissionManager + private let captureDeviceFactory: CaptureDeviceFactory + private let captureSessionFactory: CaptureSessionFactory + private let captureDeviceInputFactory: FLTCaptureDeviceInputFactory + + /// All FLTCam's state access and capture session related operations should be on run on this queue. + private let captureSessionQueue: DispatchQueue + + /// An internal camera object that manages camera's state and performs camera operations. + var camera: FLTCam? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = CameraPlugin( + registry: registrar.textures(), + messenger: registrar.messenger(), + globalAPI: FCPCameraGlobalEventApi(binaryMessenger: registrar.messenger()), + deviceDiscoverer: FLTDefaultCameraDeviceDiscoverer(), + permissionManager: FLTCameraPermissionManager( + permissionService: FLTDefaultPermissionService()), + deviceFactory: { name in + // TODO(RobertOdrowaz) Implement better error handling and remove non-null assertion + FLTDefaultCaptureDevice(device: AVCaptureDevice(uniqueID: name)!) + }, + captureSessionFactory: { FLTDefaultCaptureSession(captureSession: AVCaptureSession()) }, + captureDeviceInputFactory: FLTDefaultCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + ) + + SetUpFCPCameraApi(registrar.messenger(), instance) + } + + init( + registry: FlutterTextureRegistry, + messenger: FlutterBinaryMessenger, + globalAPI: FCPCameraGlobalEventApi, + deviceDiscoverer: FLTCameraDeviceDiscovering, + permissionManager: FLTCameraPermissionManager, + deviceFactory: @escaping CaptureDeviceFactory, + captureSessionFactory: @escaping CaptureSessionFactory, + captureDeviceInputFactory: FLTCaptureDeviceInputFactory, + captureSessionQueue: DispatchQueue + ) { + self.registry = registry + self.messenger = messenger + self.globalEventAPI = globalAPI + self.deviceDiscoverer = deviceDiscoverer + self.permissionManager = permissionManager + self.captureDeviceFactory = deviceFactory + self.captureSessionFactory = captureSessionFactory + self.captureDeviceInputFactory = captureDeviceInputFactory + self.captureSessionQueue = captureSessionQueue + + super.init() + + FLTDispatchQueueSetSpecific(captureSessionQueue, FLTCaptureSessionQueueSpecific) + + UIDevice.current.beginGeneratingDeviceOrientationNotifications() + NotificationCenter.default.addObserver( + forName: UIDevice.orientationDidChangeNotification, + object: UIDevice.current, + queue: .main + ) { [weak self] notification in + self?.orientationChanged(notification) + } + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + UIDevice.current.endGeneratingDeviceOrientationNotifications() + } + + private static func flutterErrorFromNSError(_ error: NSError) -> FlutterError { + return FlutterError( + code: "Error \(error.code)", + message: error.localizedDescription, + details: error.domain) + } + + func orientationChanged(_ notification: Notification) { + guard let device = notification.object as? UIDevice else { return } + let orientation = device.orientation + + if orientation == .faceUp || orientation == .faceDown { + // Do not change when oriented flat. + return + } + + self.captureSessionQueue.async { [weak self] in + guard let strongSelf = self else { return } + // `FLTCam.setDeviceOrientation` must be called on capture session queue. + strongSelf.camera?.setDeviceOrientation(orientation) + // `CameraPlugin.sendDeviceOrientation` can be called on any queue. + strongSelf.sendDeviceOrientation(orientation) + } + } + + func sendDeviceOrientation(_ orientation: UIDeviceOrientation) { + DispatchQueue.main.async { [weak self] in + self?.globalEventAPI.deviceOrientationChangedOrientation( + FCPGetPigeonDeviceOrientationForOrientation(orientation) + ) { _ in + // Ignore errors; this is essentially a broadcast stream, and + // it's fine if the other end doesn't receive the message + // (e.g., if it doesn't currently have a listener set up). + } + } + } +} + +extension CameraPlugin: FCPCameraApi { + public func availableCameras( + completion: @escaping ([FCPPlatformCameraDescription]?, FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + guard let strongSelf = self else { return } + + var discoveryDevices: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, + .builtInTelephotoCamera, + ] + + if #available(iOS 13.0, *) { + discoveryDevices.append(.builtInUltraWideCamera) + } + + let devices = strongSelf.deviceDiscoverer.discoverySession( + withDeviceTypes: discoveryDevices, + mediaType: .video, + position: .unspecified) + + var reply: [FCPPlatformCameraDescription] = [] + + for device in devices { + var lensFacing: FCPPlatformCameraLensDirection + + switch device.position { + case .back: + lensFacing = .back + case .front: + lensFacing = .front + case .unspecified: + lensFacing = .external + @unknown default: + lensFacing = .external + } + + let cameraDescription = FCPPlatformCameraDescription.make( + withName: device.uniqueID, + lensDirection: lensFacing + ) + reply.append(cameraDescription) + } + + completion(reply, nil) + } + } + + public func createCamera( + withName cameraName: String, + settings: FCPPlatformMediaSettings, + completion: @escaping (NSNumber?, FlutterError?) -> Void + ) { + // Create FLTCam only if granted camera access (and audio access if audio is enabled) + captureSessionQueue.async { [weak self] in + self?.permissionManager.requestCameraPermission { error in + guard let strongSelf = self else { return } + + if let error = error { + completion(nil, error) + return + } + + // Request audio permission on `create` call with `enableAudio` argument instead of the + // `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is + // optional, and used as a workaround to fix a missing frame issue on iOS. + if settings.enableAudio { + // Setup audio capture session only if granted audio access. + strongSelf.permissionManager.requestAudioPermission { [weak self] audioError in + // cannot use the outter `strongSelf` + guard let strongSelf = self else { return } + + if let audioError = audioError { + completion(nil, audioError) + return + } + + strongSelf.createCameraOnSessionQueue( + withName: cameraName, + settings: settings, + completion: completion) + } + } else { + strongSelf.createCameraOnSessionQueue( + withName: cameraName, + settings: settings, + completion: completion) + } + } + } + } + + func createCameraOnSessionQueue( + withName: String, + settings: FCPPlatformMediaSettings, + completion: @escaping (NSNumber?, FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.sessionQueueCreateCamera(name: withName, settings: settings, completion: completion) + } + } + + // This must be called on captureSessionQueue. It is extracted from createCameraOnSessionQueue + // to make it easier to reason about strong/weak self pointers. + private func sessionQueueCreateCamera( + name: String, + settings: FCPPlatformMediaSettings, + completion: @escaping (NSNumber?, FlutterError?) -> Void + ) { + let mediaSettingsAVWrapper = FLTCamMediaSettingsAVWrapper() + + let camConfiguration = FLTCamConfiguration( + mediaSettings: settings, + mediaSettingsWrapper: mediaSettingsAVWrapper, + captureDeviceFactory: captureDeviceFactory, + captureSessionFactory: captureSessionFactory, + captureSessionQueue: captureSessionQueue, + captureDeviceInputFactory: captureDeviceInputFactory, + initialCameraName: name + ) + + var error: NSError? + let newCamera = FLTCam(configuration: camConfiguration, error: &error) + + if let error = error { + completion(nil, CameraPlugin.flutterErrorFromNSError(error)) + } else { + camera?.close() + camera = newCamera + + FLTEnsureToRunOnMainQueue { [weak self] in + guard let strongSelf = self else { return } + completion(NSNumber(value: strongSelf.registry.register(newCamera)), nil) + } + } + } + + public func initializeCamera( + _ cameraId: Int, + withImageFormat imageFormat: FCPPlatformImageFormatGroup, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.sessionQueueInitializeCamera( + cameraId, + withImageFormat: imageFormat, + completion: completion) + } + } + + // This must be called on captureSessionQueue. It is extracted from initializeCamera to make it + // easier to reason about strong/weak self pointers. + private func sessionQueueInitializeCamera( + _ cameraId: Int, + withImageFormat imageFormat: FCPPlatformImageFormatGroup, + completion: @escaping (FlutterError?) -> Void + ) { + guard let camera = camera else { return } + + camera.videoFormat = FCPGetPixelFormatForPigeonFormat(imageFormat) + + camera.onFrameAvailable = { [weak self] in + guard let camera = self?.camera else { return } + if !camera.isPreviewPaused { + FLTEnsureToRunOnMainQueue { + self?.registry.textureFrameAvailable(Int64(cameraId)) + } + } + } + + camera.dartAPI = FCPCameraEventApi( + binaryMessenger: messenger, + messageChannelSuffix: "\(cameraId)" + ) + + camera.reportInitializationState() + sendDeviceOrientation(UIDevice.current.orientation) + camera.start() + completion(nil) + } + + 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) + } + } + + public func stopImageStream(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.stopImageStream() + completion(nil) + } + } + + public func receivedImageStreamData(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.receivedImageStreamData() + completion(nil) + } + } + + public func disposeCamera(_ cameraId: Int, completion: @escaping (FlutterError?) -> Void) { + registry.unregisterTexture(Int64(cameraId)) + captureSessionQueue.async { [weak self] in + if let strongSelf = self { + strongSelf.camera?.close() + strongSelf.camera = nil + } + completion(nil) + } + } + + public func lockCapture( + _ orientation: FCPPlatformDeviceOrientation, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.lockCapture(orientation) + completion(nil) + } + } + + public func unlockCaptureOrientation(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.unlockCaptureOrientation() + completion(nil) + } + } + + public func takePicture(completion: @escaping (String?, FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.captureToFile(completion: completion) + } + } + + public func prepareForVideoRecording(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setUpCaptureSessionForAudioIfNeeded() + completion(nil) + } + } + + public func startVideoRecording( + withStreaming enableStream: Bool, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + guard let strongSelf = self else { return } + strongSelf.camera?.startVideoRecording( + completion: completion, + messengerForStreaming: enableStream ? strongSelf.messenger : nil) + } + } + + public func stopVideoRecording(completion: @escaping (String?, FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.stopVideoRecording(completion: completion) + } + } + + public func pauseVideoRecording(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.pauseVideoRecording() + completion(nil) + } + } + + public func resumeVideoRecording(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.resumeVideoRecording() + completion(nil) + } + } + + public func setFlashMode( + _ mode: FCPPlatformFlashMode, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setFlashMode(mode, withCompletion: completion) + } + } + + public func setExposureMode( + _ mode: FCPPlatformExposureMode, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setExposureMode(mode) + completion(nil) + } + } + + public func setExposurePoint( + _ point: FCPPlatformPoint?, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setExposurePoint(point, withCompletion: completion) + } + } + + public func getMinimumExposureOffset(_ completion: @escaping (NSNumber?, FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + if let minOffset = self?.camera?.minimumExposureOffset { + completion(NSNumber(value: minOffset), nil) + } else { + completion(nil, nil) + } + } + } + + public func getMaximumExposureOffset(_ completion: @escaping (NSNumber?, FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + if let maxOffset = self?.camera?.maximumExposureOffset { + completion(NSNumber(value: maxOffset), nil) + } else { + completion(nil, nil) + } + } + } + + public func setExposureOffset(_ offset: Double, completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setExposureOffset(offset) + completion(nil) + } + } + + public func setFocusMode( + _ mode: FCPPlatformFocusMode, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setFocusMode(mode) + completion(nil) + } + } + + public func setFocus(_ point: FCPPlatformPoint?, completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setFocusPoint(point, completion: completion) + } + } + + public func getMinimumZoomLevel(_ completion: @escaping (NSNumber?, FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + if let minZoom = self?.camera?.minimumAvailableZoomFactor { + completion(NSNumber(value: minZoom), nil) + } else { + completion(nil, nil) + } + } + } + + public func getMaximumZoomLevel(_ completion: @escaping (NSNumber?, FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + if let maxZoom = self?.camera?.maximumAvailableZoomFactor { + completion(NSNumber(value: maxZoom), nil) + } else { + completion(nil, nil) + } + } + } + + public func setZoomLevel(_ zoom: Double, completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setZoomLevel(zoom, withCompletion: completion) + } + } + + public func pausePreview(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.pausePreview() + completion(nil) + } + } + + public func resumePreview(completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.resumePreview() + completion(nil) + } + } + + public func updateDescriptionWhileRecordingCameraName( + _ cameraName: String, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setDescriptionWhileRecording(cameraName, withCompletion: completion) + } + } + + public func setImageFileFormat( + _ format: FCPPlatformImageFileFormat, + completion: @escaping (FlutterError?) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setImageFileFormat(format) + completion(nil) + } + } +} diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Placeholder.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Placeholder.swift deleted file mode 100644 index e7217c7d7b3f..000000000000 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Placeholder.swift +++ /dev/null @@ -1,3 +0,0 @@ -// 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. diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/CameraPlugin.m deleted file mode 100644 index b9e1ef1850c3..000000000000 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/CameraPlugin.m +++ /dev/null @@ -1,529 +0,0 @@ -// 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/CameraPlugin.h" -#import "./include/camera_avfoundation/CameraPlugin_Test.h" - -@import AVFoundation; -@import Flutter; - -#import "./include/camera_avfoundation/CameraProperties.h" -#import "./include/camera_avfoundation/FLTCam.h" -#import "./include/camera_avfoundation/FLTCameraDeviceDiscovering.h" -#import "./include/camera_avfoundation/FLTCameraPermissionManager.h" -#import "./include/camera_avfoundation/FLTThreadSafeEventChannel.h" -#import "./include/camera_avfoundation/QueueUtils.h" -#import "./include/camera_avfoundation/messages.g.h" - -static FlutterError *FlutterErrorFromNSError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.localizedDescription - details:error.domain]; -} - -@interface CameraPlugin () -@property(readonly, nonatomic) NSObject *registry; -@property(readonly, nonatomic) NSObject *messenger; -@property(nonatomic) FCPCameraGlobalEventApi *globalEventAPI; -@property(readonly, nonatomic) FLTCameraPermissionManager *permissionManager; -@property(readonly, nonatomic) NSObject *deviceDiscoverer; -@property(readonly, nonatomic) CaptureNamedDeviceFactory captureDeviceFactory; -@property(readonly, nonatomic) CaptureSessionFactory captureSessionFactory; -@property(readonly, nonatomic) NSObject *captureDeviceInputFactory; -@end - -@implementation CameraPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - CameraPlugin *instance = [[CameraPlugin alloc] initWithRegistry:[registrar textures] - messenger:[registrar messenger]]; - SetUpFCPCameraApi([registrar messenger], instance); -} - -- (instancetype)initWithRegistry:(NSObject *)registry - messenger:(NSObject *)messenger { - id permissionService = [[FLTDefaultPermissionService alloc] init]; - - return [self initWithRegistry:registry - messenger:messenger - globalAPI:[[FCPCameraGlobalEventApi alloc] initWithBinaryMessenger:messenger] - deviceDiscoverer:[[FLTDefaultCameraDeviceDiscoverer alloc] init] - permissionManager:[[FLTCameraPermissionManager alloc] - initWithPermissionService:permissionService] - deviceFactory:^NSObject *(NSString *name) { - return [[FLTDefaultCaptureDevice alloc] - initWithDevice:[AVCaptureDevice deviceWithUniqueID:name]]; - } - captureSessionFactory:^NSObject *(void) { - return [[FLTDefaultCaptureSession alloc] - initWithCaptureSession:[[AVCaptureSession alloc] init]]; - } - captureDeviceInputFactory:[[FLTDefaultCaptureDeviceInputFactory alloc] init]]; -} - -- (instancetype)initWithRegistry:(NSObject *)registry - messenger:(NSObject *)messenger - globalAPI:(FCPCameraGlobalEventApi *)globalAPI - deviceDiscoverer:(NSObject *)deviceDiscoverer - permissionManager:(FLTCameraPermissionManager *)permissionManager - deviceFactory:(CaptureNamedDeviceFactory)deviceFactory - captureSessionFactory:(CaptureSessionFactory)captureSessionFactory - captureDeviceInputFactory: - (NSObject *)captureDeviceInputFactory { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _registry = registry; - _messenger = messenger; - _globalEventAPI = globalAPI; - _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); - _deviceDiscoverer = deviceDiscoverer; - _permissionManager = permissionManager; - _captureDeviceFactory = deviceFactory; - _captureSessionFactory = captureSessionFactory; - _captureDeviceInputFactory = captureDeviceInputFactory; - - dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, - (void *)FLTCaptureSessionQueueSpecific, NULL); - - [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(orientationChanged:) - name:UIDeviceOrientationDidChangeNotification - object:[UIDevice currentDevice]]; - return self; -} - -- (void)detachFromEngineForRegistrar:(NSObject *)registrar { - [UIDevice.currentDevice endGeneratingDeviceOrientationNotifications]; -} - -- (void)orientationChanged:(NSNotification *)note { - UIDevice *device = note.object; - UIDeviceOrientation orientation = device.orientation; - - if (orientation == UIDeviceOrientationFaceUp || orientation == UIDeviceOrientationFaceDown) { - // Do not change when oriented flat. - return; - } - - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - // `FLTCam::setDeviceOrientation` must be called on capture session queue. - [weakSelf.camera setDeviceOrientation:orientation]; - // `CameraPlugin::sendDeviceOrientation` can be called on any queue. - [weakSelf sendDeviceOrientation:orientation]; - }); -} - -- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { - __weak typeof(self) weakSelf = self; - dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf.globalEventAPI - deviceOrientationChangedOrientation:FCPGetPigeonDeviceOrientationForOrientation(orientation) - completion:^(FlutterError *error){ - // Ignore errors; this is essentially a broadcast stream, and - // it's fine if the other end - // doesn't receive the message (e.g., if it doesn't currently - // have a listener set up). - }]; - }); -} - -#pragma mark FCPCameraApi Implementation - -- (void)availableCamerasWithCompletion: - (nonnull void (^)(NSArray *_Nullable, - FlutterError *_Nullable))completion { - dispatch_async(self.captureSessionQueue, ^{ - NSMutableArray *discoveryDevices = - [@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ] - mutableCopy]; - if (@available(iOS 13.0, *)) { - [discoveryDevices addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera]; - } - NSArray *> *devices = - [self.deviceDiscoverer discoverySessionWithDeviceTypes:discoveryDevices - mediaType:AVMediaTypeVideo - position:AVCaptureDevicePositionUnspecified]; - NSMutableArray *reply = - [[NSMutableArray alloc] initWithCapacity:devices.count]; - for (NSObject *device in devices) { - FCPPlatformCameraLensDirection lensFacing; - switch (device.position) { - case AVCaptureDevicePositionBack: - lensFacing = FCPPlatformCameraLensDirectionBack; - break; - case AVCaptureDevicePositionFront: - lensFacing = FCPPlatformCameraLensDirectionFront; - break; - case AVCaptureDevicePositionUnspecified: - lensFacing = FCPPlatformCameraLensDirectionExternal; - break; - } - [reply addObject:[FCPPlatformCameraDescription makeWithName:device.uniqueID - lensDirection:lensFacing]]; - } - completion(reply, nil); - }); -} - -- (void)createCameraWithName:(nonnull NSString *)cameraName - settings:(nonnull FCPPlatformMediaSettings *)settings - completion: - (nonnull void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion { - // Create FLTCam only if granted camera access (and audio access if audio is enabled) - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [self->_permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) { - typeof(self) strongSelf = weakSelf; - if (!strongSelf) return; - - if (error) { - completion(nil, error); - } else { - // Request audio permission on `create` call with `enableAudio` argument instead of the - // `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is - // optional, and used as a workaround to fix a missing frame issue on iOS. - if (settings.enableAudio) { - // Setup audio capture session only if granted audio access. - [self->_permissionManager - requestAudioPermissionWithCompletionHandler:^(FlutterError *error) { - // cannot use the outter `strongSelf` - typeof(self) strongSelf = weakSelf; - if (!strongSelf) return; - if (error) { - completion(nil, error); - } else { - [strongSelf createCameraOnSessionQueueWithName:cameraName - settings:settings - completion:completion]; - } - }]; - } else { - [strongSelf createCameraOnSessionQueueWithName:cameraName - settings:settings - completion:completion]; - } - } - }]; - }); -} - -- (void)initializeCamera:(NSInteger)cameraId - withImageFormat:(FCPPlatformImageFormatGroup)imageFormat - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf sessionQueueInitializeCamera:cameraId - withImageFormat:imageFormat - completion:completion]; - }); -} - -- (void)startImageStreamWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera startImageStreamWithMessenger:weakSelf.messenger]; - completion(nil); - }); -} - -- (void)stopImageStreamWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera stopImageStream]; - completion(nil); - }); -} - -- (void)receivedImageStreamDataWithCompletion: - (nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera receivedImageStreamData]; - completion(nil); - }); -} - -- (void)takePictureWithCompletion:(nonnull void (^)(NSString *_Nullable, - FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera captureToFileWithCompletion:completion]; - }); -} - -- (void)prepareForVideoRecordingWithCompletion: - (nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setUpCaptureSessionForAudioIfNeeded]; - completion(nil); - }); -} - -- (void)startVideoRecordingWithStreaming:(BOOL)enableStream - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - typeof(self) strongSelf = weakSelf; - if (!strongSelf) return; - [strongSelf.camera - startVideoRecordingWithCompletion:completion - messengerForStreaming:(enableStream ? strongSelf.messenger : nil)]; - }); -} - -- (void)stopVideoRecordingWithCompletion:(nonnull void (^)(NSString *_Nullable, - FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera stopVideoRecordingWithCompletion:completion]; - }); -} - -- (void)pauseVideoRecordingWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera pauseVideoRecording]; - completion(nil); - }); -} - -- (void)resumeVideoRecordingWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera resumeVideoRecording]; - completion(nil); - }); -} - -- (void)getMinimumZoomLevel:(nonnull void (^)(NSNumber *_Nullable, - FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - completion(@(weakSelf.camera.minimumAvailableZoomFactor), nil); - }); -} - -- (void)getMaximumZoomLevel:(nonnull void (^)(NSNumber *_Nullable, - FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - completion(@(weakSelf.camera.maximumAvailableZoomFactor), nil); - }); -} - -- (void)setZoomLevel:(double)zoom completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setZoomLevel:zoom withCompletion:completion]; - }); -} - -- (void)setFlashMode:(FCPPlatformFlashMode)mode - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setFlashMode:mode withCompletion:completion]; - }); -} - -- (void)setExposureMode:(FCPPlatformExposureMode)mode - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setExposureMode:mode]; - completion(nil); - }); -} - -- (void)setExposurePoint:(nullable FCPPlatformPoint *)point - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setExposurePoint:point withCompletion:completion]; - }); -} - -- (void)getMinimumExposureOffset:(nonnull void (^)(NSNumber *_Nullable, - FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - completion(@(weakSelf.camera.minimumExposureOffset), nil); - }); -} - -- (void)getMaximumExposureOffset:(nonnull void (^)(NSNumber *_Nullable, - FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - completion(@(weakSelf.camera.maximumExposureOffset), nil); - }); -} - -- (void)setExposureOffset:(double)offset - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setExposureOffset:offset]; - completion(nil); - }); -} - -- (void)setFocusMode:(FCPPlatformFocusMode)mode - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setFocusMode:mode]; - completion(nil); - }); -} - -- (void)setFocusPoint:(nullable FCPPlatformPoint *)point - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setFocusPoint:point withCompletion:completion]; - }); -} - -- (void)lockCaptureOrientation:(FCPPlatformDeviceOrientation)orientation - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera lockCaptureOrientation:orientation]; - completion(nil); - }); -} - -- (void)unlockCaptureOrientationWithCompletion: - (nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera unlockCaptureOrientation]; - completion(nil); - }); -} - -- (void)pausePreviewWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera pausePreview]; - completion(nil); - }); -} - -- (void)resumePreviewWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera resumePreview]; - completion(nil); - }); -} - -- (void)setImageFileFormat:(FCPPlatformImageFileFormat)format - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setImageFileFormat:format]; - completion(nil); - }); -} - -- (void)updateDescriptionWhileRecordingCameraName:(nonnull NSString *)cameraName - completion: - (nonnull void (^)(FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera setDescriptionWhileRecording:cameraName withCompletion:completion]; - }); -} - -- (void)disposeCamera:(NSInteger)cameraId - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - [_registry unregisterTexture:cameraId]; - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf.camera close]; - weakSelf.camera = nil; - completion(nil); - }); -} - -#pragma mark Private - -// This must be called on captureSessionQueue. It is extracted from -// initializeCamera:withImageFormat:completion: to make it easier to reason about strong/weak -// self pointers. -- (void)sessionQueueInitializeCamera:(NSInteger)cameraId - withImageFormat:(FCPPlatformImageFormatGroup)imageFormat - completion:(nonnull void (^)(FlutterError *_Nullable))completion { - [_camera setVideoFormat:FCPGetPixelFormatForPigeonFormat(imageFormat)]; - - __weak CameraPlugin *weakSelf = self; - _camera.onFrameAvailable = ^{ - typeof(self) strongSelf = weakSelf; - if (!strongSelf) return; - if (![strongSelf.camera isPreviewPaused]) { - FLTEnsureToRunOnMainQueue(^{ - [weakSelf.registry textureFrameAvailable:cameraId]; - }); - } - }; - _camera.dartAPI = [[FCPCameraEventApi alloc] - initWithBinaryMessenger:_messenger - messageChannelSuffix:[NSString stringWithFormat:@"%ld", cameraId]]; - [_camera reportInitializationState]; - [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; - [_camera start]; - completion(nil); -} - -- (void)createCameraOnSessionQueueWithName:(NSString *)name - settings:(FCPPlatformMediaSettings *)settings - completion:(nonnull void (^)(NSNumber *_Nullable, - FlutterError *_Nullable))completion { - __weak typeof(self) weakSelf = self; - dispatch_async(self.captureSessionQueue, ^{ - [weakSelf sessionQueueCreateCameraWithName:name settings:settings completion:completion]; - }); -} - -// This must be called on captureSessionQueue. It is extracted from -// initializeCamera:withImageFormat:completion: to make it easier to reason about strong/weak -// self pointers. -- (void)sessionQueueCreateCameraWithName:(NSString *)name - settings:(FCPPlatformMediaSettings *)settings - completion:(nonnull void (^)(NSNumber *_Nullable, - FlutterError *_Nullable))completion { - FLTCamMediaSettingsAVWrapper *mediaSettingsAVWrapper = - [[FLTCamMediaSettingsAVWrapper alloc] init]; - - FLTCamConfiguration *camConfiguration = - [[FLTCamConfiguration alloc] initWithMediaSettings:settings - mediaSettingsWrapper:mediaSettingsAVWrapper - captureDeviceFactory:^NSObject *_Nonnull { - return self.captureDeviceFactory(name); - } - captureSessionFactory:_captureSessionFactory - captureSessionQueue:_captureSessionQueue - captureDeviceInputFactory:_captureDeviceInputFactory]; - - NSError *error; - FLTCam *cam = [[FLTCam alloc] initWithConfiguration:camConfiguration error:&error]; - - if (error) { - completion(nil, FlutterErrorFromNSError(error)); - } else { - [_camera close]; - _camera = cam; - __weak typeof(self) weakSelf = self; - FLTEnsureToRunOnMainQueue(^{ - completion(@([weakSelf.registry registerTexture:cam]), nil); - }); - } -} - -@end 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 e22b1f0dfa7d..974c3a92f782 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 @@ -193,7 +193,7 @@ - (instancetype)initWithConfiguration:(nonnull FLTCamConfiguration *)configurati _videoCaptureSession = configuration.videoCaptureSession; _audioCaptureSession = configuration.audioCaptureSession; _captureDeviceFactory = configuration.captureDeviceFactory; - _captureDevice = _captureDeviceFactory(); + _captureDevice = _captureDeviceFactory(configuration.initialCameraName); _captureDeviceInputFactory = configuration.captureDeviceInputFactory; _videoDimensionsForFormat = configuration.videoDimensionsForFormat; _flashMode = _captureDevice.hasFlash ? FCPPlatformFlashModeAuto : FCPPlatformFlashModeOff; @@ -1056,7 +1056,7 @@ - (void)setDescriptionWhileRecording:(NSString *)cameraName return; } - _captureDevice = self.captureDeviceFactory(); + _captureDevice = self.captureDeviceFactory(cameraName); NSObject *oldConnection = [_captureVideoOutput connectionWithMediaType:AVMediaTypeVideo]; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCamConfiguration.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCamConfiguration.m index 51629074fa0e..88b16d0cee5b 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCamConfiguration.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCamConfiguration.m @@ -12,7 +12,8 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings captureSessionFactory:(CaptureSessionFactory)captureSessionFactory captureSessionQueue:(dispatch_queue_t)captureSessionQueue captureDeviceInputFactory: - (NSObject *)captureDeviceInputFactory { + (NSObject *)captureDeviceInputFactory + initialCameraName:(NSString *)initialCameraName { self = [super init]; if (self) { _mediaSettings = mediaSettings; @@ -27,6 +28,7 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings return CMVideoFormatDescriptionGetDimensions(format.formatDescription); }; _captureDeviceInputFactory = captureDeviceInputFactory; + _initialCameraName = initialCameraName; _assetWriterFactory = ^id(NSURL *url, AVFileType fileType, NSError **error) { return [[FLTDefaultAssetWriter alloc] initWithURL:url fileType:fileType error:error]; }; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/QueueUtils.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/QueueUtils.m index 8ea83ede2b71..cb0895b5c2e8 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/QueueUtils.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/QueueUtils.m @@ -13,3 +13,7 @@ void FLTEnsureToRunOnMainQueue(dispatch_block_t block) { block(); } } + +void FLTDispatchQueueSetSpecific(dispatch_queue_t queue, const void *key) { + dispatch_queue_set_specific(queue, key, (void *)key, NULL); +} diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraPlugin_Test.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraPlugin_Test.h deleted file mode 100644 index dd15e3f3e6d5..000000000000 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraPlugin_Test.h +++ /dev/null @@ -1,63 +0,0 @@ -// 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. - -// This header is available in the Test module. Import via "@import camera_avfoundation.Test;" - -#import "CameraPlugin.h" -#import "FLTCam.h" -#import "FLTCamConfiguration.h" -#import "FLTCameraDeviceDiscovering.h" -#import "FLTCameraPermissionManager.h" -#import "FLTCaptureDevice.h" -#import "messages.g.h" - -NS_ASSUME_NONNULL_BEGIN - -typedef NSObject *_Nonnull (^CaptureNamedDeviceFactory)(NSString *name); - -/// APIs exposed for unit testing. -@interface CameraPlugin () - -/// All FLTCam's state access and capture session related operations should be on run on this queue. -@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; - -/// An internal camera object that manages camera's state and performs camera operations. -@property(nonatomic, strong) FLTCam *_Nullable camera; - -/// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing. -- (instancetype)initWithRegistry:(NSObject *)registry - messenger:(NSObject *)messenger; - -/// Inject @p FlutterTextureRegistry, @p FlutterBinaryMessenger, and Pigeon callback handler for -/// unit testing. -- (instancetype)initWithRegistry:(NSObject *)registry - messenger:(NSObject *)messenger - globalAPI:(FCPCameraGlobalEventApi *)globalAPI - deviceDiscoverer:(id)deviceDiscoverer - permissionManager:(FLTCameraPermissionManager *)permissionManager - deviceFactory:(CaptureNamedDeviceFactory)deviceFactory - captureSessionFactory:(CaptureSessionFactory)captureSessionFactory - captureDeviceInputFactory:(id)captureDeviceInputFactory - NS_DESIGNATED_INITIALIZER; - -/// Hide the default public constructor. -- (instancetype)init NS_UNAVAILABLE; - -/// Called by the @c NSNotificationManager each time the device's orientation is changed. -/// -/// @param notification @c NSNotification instance containing a reference to the `UIDevice` object -/// that triggered the orientation change. -- (void)orientationChanged:(NSNotification *)notification; - -/// Creates FLTCam on session queue and reports the creation result. -/// @param name the name of the camera. -/// @param settings the creation settings. -/// @param completion the callback to inform the Dart side of the plugin of creation. -- (void)createCameraOnSessionQueueWithName:(NSString *)name - settings:(FCPPlatformMediaSettings *)settings - completion:(void (^)(NSNumber *_Nullable, - FlutterError *_Nullable))completion; -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCamConfiguration.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCamConfiguration.h index d47381c4e463..8426f129e89a 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCamConfiguration.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCamConfiguration.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN /// Factory block returning an AVCaptureDevice. /// Used in tests to inject a device into FLTCam. -typedef NSObject *_Nonnull (^CaptureDeviceFactory)(void); +typedef NSObject *_Nonnull (^CaptureDeviceFactory)(NSString *); typedef NSObject *_Nonnull (^CaptureSessionFactory)(void); @@ -41,7 +41,8 @@ typedef CMVideoDimensions (^VideoDimensionsForFormat)(NSObject *)captureDeviceInputFactory; + (NSObject *)captureDeviceInputFactory + initialCameraName:(NSString *)initialCameraName; @property(nonatomic, strong) id deviceOrientationProvider; @property(nonatomic, strong) dispatch_queue_t captureSessionQueue; @@ -56,6 +57,7 @@ typedef CMVideoDimensions (^VideoDimensionsForFormat)(NSObject *captureDeviceInputFactory; @property(nonatomic, copy) AssetWriterFactory assetWriterFactory; @property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory; +@property(nonatomic, copy) NSString *initialCameraName; @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h index a7e22da716d0..bc8fc49840dd 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/QueueUtils.h @@ -16,4 +16,13 @@ extern const char* FLTCaptureSessionQueueSpecific; /// @param block the block to be run on the main queue. extern void FLTEnsureToRunOnMainQueue(dispatch_block_t block); +/// Calls `dispatch_queue_set_specific` with a key that is used to identify the +/// queue. This method is needed for compatibility of Swift implementation with +/// Objective-C code. In Swift, the API for setting key-value pairs on a queue +/// is different, so Swift code need to call this method to set the key-value +/// pair on the queue in a way that's compatible with the existing Objective-C +/// code. +extern void FLTDispatchQueueSetSpecific(dispatch_queue_t queue, + const void* key); + NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraPlugin.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/camera_avfoundation.h similarity index 50% rename from packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraPlugin.h rename to packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/camera_avfoundation.h index 586b2fc87085..96ffe59f0eb6 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraPlugin.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/camera_avfoundation.h @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import - +#import "FLTCam.h" +#import "FLTCamConfiguration.h" +#import "FLTCameraDeviceDiscovering.h" +#import "FLTCameraPermissionManager.h" +#import "FLTCaptureDevice.h" +#import "QueueUtils.h" #import "messages.g.h" - -@interface CameraPlugin : NSObject -@end diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 1d40fd04c635..e325431c3b82 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+14 +version: 0.9.19 environment: sdk: ^3.4.0