Skip to content

Commit 7d7fe6d

Browse files
committed
Migrate DefaultCamera initialization to Swift
1 parent 816ba15 commit 7d7fe6d

File tree

9 files changed

+146
-185
lines changed

9 files changed

+146
-185
lines changed

packages/camera/camera_avfoundation/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.9.21+2
2+
3+
* Migrates `DefaultCamera` initialization to Swift.
4+
* Removes unused `textureId` field of `FLTCam` class.
5+
16
## 0.9.21+1
27

38
* Migrates `startImageStream` and `setUpCaptureSessionForAudioIfNeeded` methods to Swift.

packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ enum CameraTestUtils {
8282
}
8383

8484
static func createTestCamera(_ configuration: FLTCamConfiguration) -> DefaultCamera {
85-
return DefaultCamera(configuration: configuration, error: nil)
85+
return (try? DefaultCamera(configuration: configuration))!
8686
}
8787

8888
static func createTestCamera() -> DefaultCamera {

packages/camera/camera_avfoundation/example/ios/RunnerTests/PhotoCaptureTests.swift

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ final class PhotoCaptureTests: XCTestCase {
3232
let mockOutput = MockCapturePhotoOutput()
3333
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
3434
let delegate =
35-
cam.inProgressSavePhotoDelegates.object(forKey: settings.uniqueID)
36-
as? FLTSavePhotoDelegate
35+
cam.inProgressSavePhotoDelegates[settings.uniqueID]
3736
// Completion runs on IO queue.
3837
let ioQueue = DispatchQueue(label: "io_queue")
3938
ioQueue.async {
@@ -66,8 +65,7 @@ final class PhotoCaptureTests: XCTestCase {
6665
let mockOutput = MockCapturePhotoOutput()
6766
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
6867
let delegate =
69-
cam.inProgressSavePhotoDelegates.object(forKey: settings.uniqueID)
70-
as? FLTSavePhotoDelegate
68+
cam.inProgressSavePhotoDelegates[settings.uniqueID]
7169
// Completion runs on IO queue.
7270
let ioQueue = DispatchQueue(label: "io_queue")
7371
ioQueue.async {
@@ -101,8 +99,7 @@ final class PhotoCaptureTests: XCTestCase {
10199
mockOutput.availablePhotoCodecTypes = [AVVideoCodecType.hevc]
102100
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
103101
let delegate =
104-
cam.inProgressSavePhotoDelegates.object(forKey: settings.uniqueID)
105-
as? FLTSavePhotoDelegate
102+
cam.inProgressSavePhotoDelegates[settings.uniqueID]
106103
// Completion runs on IO queue.
107104
let ioQueue = DispatchQueue(label: "io_queue")
108105
ioQueue.async {
@@ -136,8 +133,7 @@ final class PhotoCaptureTests: XCTestCase {
136133
let mockOutput = MockCapturePhotoOutput()
137134
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
138135
let delegate =
139-
cam.inProgressSavePhotoDelegates.object(forKey: settings.uniqueID)
140-
as? FLTSavePhotoDelegate
136+
cam.inProgressSavePhotoDelegates[settings.uniqueID]
141137
// Completion runs on IO queue.
142138
let ioQueue = DispatchQueue(label: "io_queue")
143139
ioQueue.async {
@@ -185,8 +181,7 @@ final class PhotoCaptureTests: XCTestCase {
185181
let mockOutput = MockCapturePhotoOutput()
186182
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
187183
let delegate =
188-
cam.inProgressSavePhotoDelegates.object(forKey: settings.uniqueID)
189-
as? FLTSavePhotoDelegate
184+
cam.inProgressSavePhotoDelegates[settings.uniqueID]
190185
// Completion runs on IO queue.
191186
let ioQueue = DispatchQueue(label: "io_queue")
192187
ioQueue.async {

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,19 +248,18 @@ extension CameraPlugin: FCPCameraApi {
248248
initialCameraName: name
249249
)
250250

251-
var error: NSError?
252-
let newCamera = DefaultCamera(configuration: camConfiguration, error: &error)
251+
do {
252+
let newCamera = try DefaultCamera(configuration: camConfiguration)
253253

254-
if let error = error {
255-
completion(nil, CameraPlugin.flutterErrorFromNSError(error))
256-
} else {
257254
camera?.close()
258255
camera = newCamera
259256

260257
FLTEnsureToRunOnMainQueue { [weak self] in
261258
guard let strongSelf = self else { return }
262259
completion(NSNumber(value: strongSelf.registry.register(newCamera)), nil)
263260
}
261+
} catch let error as NSError {
262+
completion(nil, CameraPlugin.flutterErrorFromNSError(error))
264263
}
265264
}
266265

packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ final class DefaultCamera: FLTCam, Camera {
1313
var dartAPI: FCPCameraEventApi?
1414
var onFrameAvailable: (() -> Void)?
1515

16-
override var videoFormat: FourCharCode {
16+
var videoFormat: FourCharCode = kCVPixelFormatType_32BGRA {
1717
didSet {
1818
captureVideoOutput.videoSettings = [
1919
kCVPixelBufferPixelFormatTypeKey as String: videoFormat
@@ -47,11 +47,33 @@ final class DefaultCamera: FLTCam, Camera {
4747
/// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation.
4848
private let photoIOQueue = DispatchQueue(label: "io.flutter.camera.photoIOQueue")
4949

50+
/// All DefaultCamera's state access and capture session related operations should be run on this queue.
51+
private let captureSessionQueue: DispatchQueue
52+
53+
private let mediaSettings: FCPPlatformMediaSettings
54+
private let mediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper
55+
56+
/// A wrapper for AVCaptureDevice creation to allow for dependency injection in tests.
57+
private let captureDeviceFactory: CaptureDeviceFactory
58+
private let audioCaptureDeviceFactory: AudioCaptureDeviceFactory
59+
private let captureDeviceInputFactory: FLTCaptureDeviceInputFactory
60+
private let assetWriterFactory: AssetWriterFactory
61+
private let inputPixelBufferAdaptorFactory: InputPixelBufferAdaptorFactory
62+
63+
private let deviceOrientationProvider: FLTDeviceOrientationProviding
64+
5065
private var videoWriter: FLTAssetWriter?
5166
private var videoWriterInput: FLTAssetWriterInput?
5267
private var audioWriterInput: FLTAssetWriterInput?
5368
private var videoAdaptor: FLTAssetWriterInputPixelBufferAdaptor?
5469

70+
/// A dictionary to retain all in-progress FLTSavePhotoDelegates. The key of the dictionary is the
71+
/// AVCapturePhotoSettings's uniqueID for each photo capture operation, and the value is the
72+
/// FLTSavePhotoDelegate that handles the result of each photo capture operation. Note that photo
73+
/// capture operations may overlap, so FLTCam has to keep track of multiple delegates in progress,
74+
/// instead of just a single delegate reference.
75+
private(set) var inProgressSavePhotoDelegates = [Int64: FLTSavePhotoDelegate]()
76+
5577
private var imageStreamHandler: FLTImageStreamHandler?
5678

5779
/// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback.
@@ -69,6 +91,9 @@ final class DefaultCamera: FLTCam, Camera {
6991
private var videoTimeOffset = CMTime.zero
7092
private var audioTimeOffset = CMTime.zero
7193

94+
/// True when images from the camera are being streamed.
95+
private(set) var isStreamingImages = false
96+
7297
/// Number of frames currently pending processing.
7398
private var streamingPendingFramesCount = 0
7499

@@ -80,6 +105,7 @@ final class DefaultCamera: FLTCam, Camera {
80105

81106
private var exposureMode = FCPPlatformExposureMode.auto
82107
private var focusMode = FCPPlatformFocusMode.auto
108+
private var flashMode: FCPPlatformFlashMode
83109

84110
private static func flutterErrorFromNSError(_ error: NSError) -> FlutterError {
85111
return FlutterError(
@@ -116,6 +142,95 @@ final class DefaultCamera: FLTCam, Camera {
116142
return (captureVideoInput, captureVideoOutput, connection)
117143
}
118144

145+
init(configuration: FLTCamConfiguration) throws {
146+
captureSessionQueue = configuration.captureSessionQueue
147+
mediaSettings = configuration.mediaSettings
148+
mediaSettingsAVWrapper = configuration.mediaSettingsWrapper
149+
captureDeviceFactory = configuration.captureDeviceFactory
150+
audioCaptureDeviceFactory = configuration.audioCaptureDeviceFactory
151+
captureDeviceInputFactory = configuration.captureDeviceInputFactory
152+
assetWriterFactory = configuration.assetWriterFactory
153+
inputPixelBufferAdaptorFactory = configuration.inputPixelBufferAdaptorFactory
154+
deviceOrientationProvider = configuration.deviceOrientationProvider
155+
156+
let captureDevice = captureDeviceFactory(configuration.initialCameraName)
157+
flashMode = captureDevice.hasFlash ? .auto : .off
158+
159+
super.init()
160+
161+
videoCaptureSession = configuration.videoCaptureSession
162+
audioCaptureSession = configuration.audioCaptureSession
163+
videoDimensionsForFormat = configuration.videoDimensionsForFormat
164+
165+
self.captureDevice = captureDevice
166+
167+
capturePhotoOutput = FLTDefaultCapturePhotoOutput(photoOutput: AVCapturePhotoOutput())
168+
capturePhotoOutput.highResolutionCaptureEnabled = true
169+
170+
videoCaptureSession.automaticallyConfiguresApplicationAudioSession = false
171+
audioCaptureSession.automaticallyConfiguresApplicationAudioSession = false
172+
173+
deviceOrientation = configuration.orientation
174+
175+
let connection: AVCaptureConnection
176+
(captureVideoInput, captureVideoOutput, connection) = try DefaultCamera.createConnection(
177+
captureDevice: captureDevice,
178+
videoFormat: videoFormat,
179+
captureDeviceInputFactory: configuration.captureDeviceInputFactory)
180+
181+
captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue)
182+
183+
videoCaptureSession.addInputWithNoConnections(captureVideoInput)
184+
videoCaptureSession.addOutputWithNoConnections(captureVideoOutput.avOutput)
185+
videoCaptureSession.addConnection(connection)
186+
187+
videoCaptureSession.addOutput(capturePhotoOutput.avOutput)
188+
189+
motionManager.startAccelerometerUpdates()
190+
191+
if mediaSettings.framesPerSecond != nil {
192+
// The frame rate can be changed only on a locked for configuration device.
193+
try mediaSettingsAVWrapper.lockDevice(captureDevice)
194+
mediaSettingsAVWrapper.beginConfiguration(for: videoCaptureSession)
195+
196+
// Possible values for presets are hard-coded in FLT interface having
197+
// corresponding AVCaptureSessionPreset counterparts.
198+
// If _resolutionPreset is not supported by camera there is
199+
// fallback to lower resolution presets.
200+
// If none can be selected there is error condition.
201+
do {
202+
try setCaptureSessionPreset(mediaSettings.resolutionPreset)
203+
} catch {
204+
videoCaptureSession.commitConfiguration()
205+
captureDevice.unlockForConfiguration()
206+
throw error
207+
}
208+
209+
FLTSelectBestFormatForRequestedFrameRate(
210+
captureDevice,
211+
mediaSettings,
212+
videoDimensionsForFormat)
213+
214+
if let framesPerSecond = mediaSettings.framesPerSecond {
215+
// Set frame rate with 1/10 precision allowing non-integral values.
216+
let fpsNominator = floor(framesPerSecond.doubleValue * 10.0)
217+
let duration = CMTimeMake(value: 10, timescale: Int32(fpsNominator))
218+
219+
mediaSettingsAVWrapper.setMinFrameDuration(duration, on: captureDevice)
220+
mediaSettingsAVWrapper.setMaxFrameDuration(duration, on: captureDevice)
221+
}
222+
223+
mediaSettingsAVWrapper.commitConfiguration(for: videoCaptureSession)
224+
mediaSettingsAVWrapper.unlockDevice(captureDevice)
225+
} else {
226+
// If the frame rate is not important fall to a less restrictive
227+
// behavior (no configuration locking).
228+
try setCaptureSessionPreset(mediaSettings.resolutionPreset)
229+
}
230+
231+
updateOrientation()
232+
}
233+
119234
func setUpCaptureSessionForAudioIfNeeded() {
120235
// Don't setup audio twice or we will lose the audio.
121236
guard !mediaSettings.enableAudio || !isAudioSetup else { return }
@@ -467,8 +582,9 @@ final class DefaultCamera: FLTCam, Camera {
467582
guard let strongSelf = self else { return }
468583

469584
strongSelf.captureSessionQueue.async { [weak self] in
470-
self?.inProgressSavePhotoDelegates.removeObject(
471-
forKey: settings.uniqueID)
585+
self?.inProgressSavePhotoDelegates.removeValue(
586+
forKey:
587+
settings.uniqueID)
472588
}
473589

474590
if let error = error {

0 commit comments

Comments
 (0)