@@ -9,7 +9,7 @@ import CoreMotion
9
9
import camera_avfoundation_objc
10
10
#endif
11
11
12
- final class DefaultCamera : FLTCam , Camera {
12
+ final class DefaultCamera : NSObject , Camera {
13
13
var dartAPI : FCPCameraEventApi ?
14
14
var onFrameAvailable : ( ( ) -> Void ) ?
15
15
@@ -23,16 +23,6 @@ final class DefaultCamera: FLTCam, Camera {
23
23
24
24
private( set) var isPreviewPaused = false
25
25
26
- override var deviceOrientation : UIDeviceOrientation {
27
- get { super. deviceOrientation }
28
- set {
29
- guard newValue != super. deviceOrientation else { return }
30
-
31
- super. deviceOrientation = newValue
32
- updateOrientation ( )
33
- }
34
- }
35
-
36
26
var minimumExposureOffset : CGFloat { CGFloat ( captureDevice. minExposureTargetBias) }
37
27
var maximumExposureOffset : CGFloat { CGFloat ( captureDevice. maxExposureTargetBias) }
38
28
var minimumAvailableZoomFactor : CGFloat { captureDevice. minAvailableVideoZoomFactor }
@@ -53,18 +43,34 @@ final class DefaultCamera: FLTCam, Camera {
53
43
private let mediaSettings : FCPPlatformMediaSettings
54
44
private let mediaSettingsAVWrapper : FLTCamMediaSettingsAVWrapper
55
45
46
+ private let videoCaptureSession : FLTCaptureSession
47
+ private let audioCaptureSession : FLTCaptureSession
48
+
56
49
/// A wrapper for AVCaptureDevice creation to allow for dependency injection in tests.
57
50
private let captureDeviceFactory : CaptureDeviceFactory
58
51
private let audioCaptureDeviceFactory : AudioCaptureDeviceFactory
59
52
private let captureDeviceInputFactory : FLTCaptureDeviceInputFactory
60
53
private let assetWriterFactory : AssetWriterFactory
61
54
private let inputPixelBufferAdaptorFactory : InputPixelBufferAdaptorFactory
62
55
56
+ /// A wrapper for CMVideoFormatDescriptionGetDimensions.
57
+ /// Allows for alternate implementations in tests.
58
+ private let videoDimensionsForFormat : VideoDimensionsForFormat
59
+
63
60
private let deviceOrientationProvider : FLTDeviceOrientationProviding
61
+ private let motionManager = CMMotionManager ( )
62
+
63
+ private( set) var captureDevice : FLTCaptureDevice
64
+ // Setter exposed for tests.
65
+ var captureVideoOutput : FLTCaptureVideoDataOutput
66
+ // Setter exposed for tests.
67
+ var capturePhotoOutput : FLTCapturePhotoOutput
68
+ private var captureVideoInput : FLTCaptureInput
64
69
65
70
private var videoWriter : FLTAssetWriter ?
66
71
private var videoWriterInput : FLTAssetWriterInput ?
67
72
private var audioWriterInput : FLTAssetWriterInput ?
73
+ private var assetWriterPixelBufferAdaptor : FLTAssetWriterInputPixelBufferAdaptor ?
68
74
private var videoAdaptor : FLTAssetWriterInputPixelBufferAdaptor ?
69
75
70
76
/// A dictionary to retain all in-progress FLTSavePhotoDelegates. The key of the dictionary is the
@@ -76,11 +82,20 @@ final class DefaultCamera: FLTCam, Camera {
76
82
77
83
private var imageStreamHandler : FLTImageStreamHandler ?
78
84
85
+ private var previewSize : CGSize ?
86
+ var deviceOrientation : UIDeviceOrientation {
87
+ didSet {
88
+ guard deviceOrientation != oldValue else { return }
89
+ updateOrientation ( )
90
+ }
91
+ }
92
+
79
93
/// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback.
80
94
/// Used to deliver the latest pixel buffer to the flutter engine via the `copyPixelBuffer` API.
81
95
private var latestPixelBuffer : CVPixelBuffer ?
82
96
83
97
private var videoRecordingPath : String ?
98
+ private var isRecording = false
84
99
private var isRecordingPaused = false
85
100
private var isFirstVideoSample = false
86
101
private var videoIsDisconnected = false
@@ -103,6 +118,8 @@ final class DefaultCamera: FLTCam, Camera {
103
118
/// https://github.com/flutter/plugins/pull/4520#discussion_r766335637
104
119
private var maxStreamingPendingFramesCount = 4
105
120
121
+ private var fileFormat = FCPPlatformImageFileFormat . jpeg
122
+ private var lockedCaptureOrientation = UIDeviceOrientation . unknown
106
123
private var exposureMode = FCPPlatformExposureMode . auto
107
124
private var focusMode = FCPPlatformFocusMode . auto
108
125
private var flashMode : FCPPlatformFlashMode
@@ -146,24 +163,19 @@ final class DefaultCamera: FLTCam, Camera {
146
163
captureSessionQueue = configuration. captureSessionQueue
147
164
mediaSettings = configuration. mediaSettings
148
165
mediaSettingsAVWrapper = configuration. mediaSettingsWrapper
166
+ videoCaptureSession = configuration. videoCaptureSession
167
+ audioCaptureSession = configuration. audioCaptureSession
149
168
captureDeviceFactory = configuration. captureDeviceFactory
150
169
audioCaptureDeviceFactory = configuration. audioCaptureDeviceFactory
151
170
captureDeviceInputFactory = configuration. captureDeviceInputFactory
152
171
assetWriterFactory = configuration. assetWriterFactory
153
172
inputPixelBufferAdaptorFactory = configuration. inputPixelBufferAdaptorFactory
173
+ videoDimensionsForFormat = configuration. videoDimensionsForFormat
154
174
deviceOrientationProvider = configuration. deviceOrientationProvider
155
175
156
- let captureDevice = captureDeviceFactory ( configuration. initialCameraName)
176
+ captureDevice = captureDeviceFactory ( configuration. initialCameraName)
157
177
flashMode = captureDevice. hasFlash ? . auto : . off
158
178
159
- super. init ( )
160
-
161
- videoCaptureSession = configuration. videoCaptureSession
162
- audioCaptureSession = configuration. audioCaptureSession
163
- videoDimensionsForFormat = configuration. videoDimensionsForFormat
164
-
165
- self . captureDevice = captureDevice
166
-
167
179
capturePhotoOutput = FLTDefaultCapturePhotoOutput ( photoOutput: AVCapturePhotoOutput ( ) )
168
180
capturePhotoOutput. highResolutionCaptureEnabled = true
169
181
@@ -178,6 +190,8 @@ final class DefaultCamera: FLTCam, Camera {
178
190
videoFormat: videoFormat,
179
191
captureDeviceInputFactory: configuration. captureDeviceInputFactory)
180
192
193
+ super. init ( )
194
+
181
195
captureVideoOutput. setSampleBufferDelegate ( self , queue: captureSessionQueue)
182
196
183
197
videoCaptureSession. addInputWithNoConnections ( captureVideoInput)
@@ -196,11 +210,6 @@ final class DefaultCamera: FLTCam, Camera {
196
210
mediaSettingsAVWrapper. beginConfiguration ( for: videoCaptureSession)
197
211
defer { mediaSettingsAVWrapper. commitConfiguration ( for: videoCaptureSession) }
198
212
199
- // Possible values for presets are hard-coded in FLT interface having
200
- // corresponding AVCaptureSessionPreset counterparts.
201
- // If _resolutionPreset is not supported by camera there is
202
- // fallback to lower resolution presets.
203
- // If none can be selected there is error condition.
204
213
try setCaptureSessionPreset ( mediaSettings. resolutionPreset)
205
214
206
215
FLTSelectBestFormatForRequestedFrameRate (
@@ -225,6 +234,110 @@ final class DefaultCamera: FLTCam, Camera {
225
234
updateOrientation ( )
226
235
}
227
236
237
+ // Possible values for presets are hard-coded in FLT interface having
238
+ // corresponding AVCaptureSessionPreset counterparts.
239
+ // If _resolutionPreset is not supported by camera there is
240
+ // fallback to lower resolution presets.
241
+ // If none can be selected there is error condition.
242
+ private func setCaptureSessionPreset(
243
+ _ resolutionPreset: FCPPlatformResolutionPreset
244
+ ) throws {
245
+ switch resolutionPreset {
246
+ case . max:
247
+ if let bestFormat = highestResolutionFormat ( forCaptureDevice: captureDevice) {
248
+ videoCaptureSession. sessionPreset = . inputPriority
249
+ do {
250
+ try captureDevice. lockForConfiguration ( )
251
+ // Set the best device format found and finish the device configuration.
252
+ captureDevice. activeFormat = bestFormat
253
+ captureDevice. unlockForConfiguration ( )
254
+ break
255
+ }
256
+ }
257
+ fallthrough
258
+ case . ultraHigh:
259
+ if videoCaptureSession. canSetSessionPreset ( . hd4K3840x2160) {
260
+ videoCaptureSession. sessionPreset = . hd4K3840x2160
261
+ break
262
+ }
263
+ if videoCaptureSession. canSetSessionPreset ( . high) {
264
+ videoCaptureSession. sessionPreset = . high
265
+ break
266
+ }
267
+ fallthrough
268
+ case . veryHigh:
269
+ if videoCaptureSession. canSetSessionPreset ( . hd1920x1080) {
270
+ videoCaptureSession. sessionPreset = . hd1920x1080
271
+ break
272
+ }
273
+ fallthrough
274
+ case . high:
275
+ if videoCaptureSession. canSetSessionPreset ( . hd1280x720) {
276
+ videoCaptureSession. sessionPreset = . hd1280x720
277
+ break
278
+ }
279
+ fallthrough
280
+ case . medium:
281
+ if videoCaptureSession. canSetSessionPreset ( . vga640x480) {
282
+ videoCaptureSession. sessionPreset = . vga640x480
283
+ break
284
+ }
285
+ fallthrough
286
+ case . low:
287
+ if videoCaptureSession. canSetSessionPreset ( . cif352x288) {
288
+ videoCaptureSession. sessionPreset = . cif352x288
289
+ break
290
+ }
291
+ fallthrough
292
+ default :
293
+ if videoCaptureSession. canSetSessionPreset ( . low) {
294
+ videoCaptureSession. sessionPreset = . low
295
+ } else {
296
+ throw NSError (
297
+ domain: NSCocoaErrorDomain,
298
+ code: URLError . unknown. rawValue,
299
+ userInfo: [
300
+ NSLocalizedDescriptionKey: " No capture session available for current capture session. "
301
+ ] )
302
+ }
303
+ }
304
+
305
+ let size = videoDimensionsForFormat ( captureDevice. activeFormat)
306
+ previewSize = CGSize ( width: CGFloat ( size. width) , height: CGFloat ( size. height) )
307
+ audioCaptureSession. sessionPreset = videoCaptureSession. sessionPreset
308
+ }
309
+
310
+ /// Finds the highest available resolution in terms of pixel count for the given device.
311
+ /// Preferred are formats with the same subtype as current activeFormat.
312
+ private func highestResolutionFormat( forCaptureDevice captureDevice: FLTCaptureDevice )
313
+ -> FLTCaptureDeviceFormat ?
314
+ {
315
+ let preferredSubType = CMFormatDescriptionGetMediaSubType (
316
+ captureDevice. activeFormat. formatDescription)
317
+ var bestFormat : FLTCaptureDeviceFormat ? = nil
318
+ var maxPixelCount : UInt = 0
319
+ var isBestSubTypePreferred = false
320
+
321
+ for format in captureDevice. formats {
322
+ let resolution = videoDimensionsForFormat ( format)
323
+ let height = UInt ( resolution. height)
324
+ let width = UInt ( resolution. width)
325
+ let pixelCount = height * width
326
+ let subType = CMFormatDescriptionGetMediaSubType ( format. formatDescription)
327
+ let isSubTypePreferred = subType == preferredSubType
328
+
329
+ if pixelCount > maxPixelCount
330
+ || ( pixelCount == maxPixelCount && isSubTypePreferred && !isBestSubTypePreferred)
331
+ {
332
+ bestFormat = format
333
+ maxPixelCount = pixelCount
334
+ isBestSubTypePreferred = isSubTypePreferred
335
+ }
336
+ }
337
+
338
+ return bestFormat
339
+ }
340
+
228
341
func setUpCaptureSessionForAudioIfNeeded( ) {
229
342
// Don't setup audio twice or we will lose the audio.
230
343
guard !mediaSettings. enableAudio || !isAudioSetup else { return }
@@ -315,8 +428,9 @@ final class DefaultCamera: FLTCam, Camera {
315
428
// Get all the state on the current thread, not the main thread.
316
429
let state = FCPPlatformCameraState . make (
317
430
withPreviewSize: FCPPlatformSize . make (
318
- withWidth: Double ( previewSize. width) ,
319
- height: Double ( previewSize. height)
431
+ // previewSize is set during init, so it will never be nil.
432
+ withWidth: previewSize!. width,
433
+ height: previewSize!. height
320
434
) ,
321
435
exposureMode: exposureMode,
322
436
focusMode: focusMode,
@@ -621,6 +735,45 @@ final class DefaultCamera: FLTCam, Camera {
621
735
return file
622
736
}
623
737
738
+ private func updateOrientation( ) {
739
+ guard !isRecording else { return }
740
+
741
+ let orientation =
742
+ ( lockedCaptureOrientation != . unknown)
743
+ ? lockedCaptureOrientation
744
+ : deviceOrientation
745
+
746
+ updateOrientation ( orientation, forCaptureOutput: capturePhotoOutput)
747
+ updateOrientation ( orientation, forCaptureOutput: captureVideoOutput)
748
+ }
749
+
750
+ private func updateOrientation(
751
+ _ orientation: UIDeviceOrientation , forCaptureOutput captureOutput: FLTCaptureOutput
752
+ ) {
753
+ if let connection = captureOutput. connection ( withMediaType: . video) ,
754
+ connection. isVideoOrientationSupported
755
+ {
756
+ connection. videoOrientation = videoOrientation ( forDeviceOrientation: orientation)
757
+ }
758
+ }
759
+
760
+ private func videoOrientation( forDeviceOrientation deviceOrientation: UIDeviceOrientation )
761
+ -> AVCaptureVideoOrientation
762
+ {
763
+ switch deviceOrientation {
764
+ case . portrait:
765
+ return . portrait
766
+ case . landscapeLeft:
767
+ return . landscapeRight
768
+ case . landscapeRight:
769
+ return . landscapeLeft
770
+ case . portraitUpsideDown:
771
+ return . portraitUpsideDown
772
+ default :
773
+ return . portrait
774
+ }
775
+ }
776
+
624
777
func lockCaptureOrientation( _ pigeonOrientation: FCPPlatformDeviceOrientation ) {
625
778
let orientation = FCPGetUIDeviceOrientationForPigeonDeviceOrientation ( pigeonOrientation)
626
779
if lockedCaptureOrientation != orientation {
0 commit comments