Skip to content

Commit affa244

Browse files
[camera_avfoundation] Implementation swift migration - part 13 (#9930)
Migrates camera implementation as part of flutter/flutter#119109 This PR migrates the last chunk of `FLTCam` class to Swift: * `updateOrientation` * `setCaptureSessionPreset` * Removes `FLTCam` class ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 34eec78 commit affa244

File tree

7 files changed

+186
-266
lines changed

7 files changed

+186
-266
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+4
2+
3+
* Migrates `updateOrientation` and `setCaptureSessionPreset` methods to Swift.
4+
* Removes `FLTCam` class.
5+
16
## 0.9.21+3
27

38
* Removes code for versions of iOS older than 13.0.

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

Lines changed: 180 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import CoreMotion
99
import camera_avfoundation_objc
1010
#endif
1111

12-
final class DefaultCamera: FLTCam, Camera {
12+
final class DefaultCamera: NSObject, Camera {
1313
var dartAPI: FCPCameraEventApi?
1414
var onFrameAvailable: (() -> Void)?
1515

@@ -23,16 +23,6 @@ final class DefaultCamera: FLTCam, Camera {
2323

2424
private(set) var isPreviewPaused = false
2525

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-
3626
var minimumExposureOffset: CGFloat { CGFloat(captureDevice.minExposureTargetBias) }
3727
var maximumExposureOffset: CGFloat { CGFloat(captureDevice.maxExposureTargetBias) }
3828
var minimumAvailableZoomFactor: CGFloat { captureDevice.minAvailableVideoZoomFactor }
@@ -53,18 +43,34 @@ final class DefaultCamera: FLTCam, Camera {
5343
private let mediaSettings: FCPPlatformMediaSettings
5444
private let mediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper
5545

46+
private let videoCaptureSession: FLTCaptureSession
47+
private let audioCaptureSession: FLTCaptureSession
48+
5649
/// A wrapper for AVCaptureDevice creation to allow for dependency injection in tests.
5750
private let captureDeviceFactory: CaptureDeviceFactory
5851
private let audioCaptureDeviceFactory: AudioCaptureDeviceFactory
5952
private let captureDeviceInputFactory: FLTCaptureDeviceInputFactory
6053
private let assetWriterFactory: AssetWriterFactory
6154
private let inputPixelBufferAdaptorFactory: InputPixelBufferAdaptorFactory
6255

56+
/// A wrapper for CMVideoFormatDescriptionGetDimensions.
57+
/// Allows for alternate implementations in tests.
58+
private let videoDimensionsForFormat: VideoDimensionsForFormat
59+
6360
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
6469

6570
private var videoWriter: FLTAssetWriter?
6671
private var videoWriterInput: FLTAssetWriterInput?
6772
private var audioWriterInput: FLTAssetWriterInput?
73+
private var assetWriterPixelBufferAdaptor: FLTAssetWriterInputPixelBufferAdaptor?
6874
private var videoAdaptor: FLTAssetWriterInputPixelBufferAdaptor?
6975

7076
/// A dictionary to retain all in-progress FLTSavePhotoDelegates. The key of the dictionary is the
@@ -76,11 +82,20 @@ final class DefaultCamera: FLTCam, Camera {
7682

7783
private var imageStreamHandler: FLTImageStreamHandler?
7884

85+
private var previewSize: CGSize?
86+
var deviceOrientation: UIDeviceOrientation {
87+
didSet {
88+
guard deviceOrientation != oldValue else { return }
89+
updateOrientation()
90+
}
91+
}
92+
7993
/// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback.
8094
/// Used to deliver the latest pixel buffer to the flutter engine via the `copyPixelBuffer` API.
8195
private var latestPixelBuffer: CVPixelBuffer?
8296

8397
private var videoRecordingPath: String?
98+
private var isRecording = false
8499
private var isRecordingPaused = false
85100
private var isFirstVideoSample = false
86101
private var videoIsDisconnected = false
@@ -103,6 +118,8 @@ final class DefaultCamera: FLTCam, Camera {
103118
/// https://github.com/flutter/plugins/pull/4520#discussion_r766335637
104119
private var maxStreamingPendingFramesCount = 4
105120

121+
private var fileFormat = FCPPlatformImageFileFormat.jpeg
122+
private var lockedCaptureOrientation = UIDeviceOrientation.unknown
106123
private var exposureMode = FCPPlatformExposureMode.auto
107124
private var focusMode = FCPPlatformFocusMode.auto
108125
private var flashMode: FCPPlatformFlashMode
@@ -146,24 +163,19 @@ final class DefaultCamera: FLTCam, Camera {
146163
captureSessionQueue = configuration.captureSessionQueue
147164
mediaSettings = configuration.mediaSettings
148165
mediaSettingsAVWrapper = configuration.mediaSettingsWrapper
166+
videoCaptureSession = configuration.videoCaptureSession
167+
audioCaptureSession = configuration.audioCaptureSession
149168
captureDeviceFactory = configuration.captureDeviceFactory
150169
audioCaptureDeviceFactory = configuration.audioCaptureDeviceFactory
151170
captureDeviceInputFactory = configuration.captureDeviceInputFactory
152171
assetWriterFactory = configuration.assetWriterFactory
153172
inputPixelBufferAdaptorFactory = configuration.inputPixelBufferAdaptorFactory
173+
videoDimensionsForFormat = configuration.videoDimensionsForFormat
154174
deviceOrientationProvider = configuration.deviceOrientationProvider
155175

156-
let captureDevice = captureDeviceFactory(configuration.initialCameraName)
176+
captureDevice = captureDeviceFactory(configuration.initialCameraName)
157177
flashMode = captureDevice.hasFlash ? .auto : .off
158178

159-
super.init()
160-
161-
videoCaptureSession = configuration.videoCaptureSession
162-
audioCaptureSession = configuration.audioCaptureSession
163-
videoDimensionsForFormat = configuration.videoDimensionsForFormat
164-
165-
self.captureDevice = captureDevice
166-
167179
capturePhotoOutput = FLTDefaultCapturePhotoOutput(photoOutput: AVCapturePhotoOutput())
168180
capturePhotoOutput.highResolutionCaptureEnabled = true
169181

@@ -178,6 +190,8 @@ final class DefaultCamera: FLTCam, Camera {
178190
videoFormat: videoFormat,
179191
captureDeviceInputFactory: configuration.captureDeviceInputFactory)
180192

193+
super.init()
194+
181195
captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue)
182196

183197
videoCaptureSession.addInputWithNoConnections(captureVideoInput)
@@ -196,11 +210,6 @@ final class DefaultCamera: FLTCam, Camera {
196210
mediaSettingsAVWrapper.beginConfiguration(for: videoCaptureSession)
197211
defer { mediaSettingsAVWrapper.commitConfiguration(for: videoCaptureSession) }
198212

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.
204213
try setCaptureSessionPreset(mediaSettings.resolutionPreset)
205214

206215
FLTSelectBestFormatForRequestedFrameRate(
@@ -225,6 +234,110 @@ final class DefaultCamera: FLTCam, Camera {
225234
updateOrientation()
226235
}
227236

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+
228341
func setUpCaptureSessionForAudioIfNeeded() {
229342
// Don't setup audio twice or we will lose the audio.
230343
guard !mediaSettings.enableAudio || !isAudioSetup else { return }
@@ -315,8 +428,9 @@ final class DefaultCamera: FLTCam, Camera {
315428
// Get all the state on the current thread, not the main thread.
316429
let state = FCPPlatformCameraState.make(
317430
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
320434
),
321435
exposureMode: exposureMode,
322436
focusMode: focusMode,
@@ -621,6 +735,45 @@ final class DefaultCamera: FLTCam, Camera {
621735
return file
622736
}
623737

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+
624777
func lockCaptureOrientation(_ pigeonOrientation: FCPPlatformDeviceOrientation) {
625778
let orientation = FCPGetUIDeviceOrientationForPigeonDeviceOrientation(pigeonOrientation)
626779
if lockedCaptureOrientation != orientation {

0 commit comments

Comments
 (0)