import Foundation import AVFoundation import SwiftSignalKit import TelegramCore private let defaultFPS: Double = 30.0 final class CameraDevice { var position: Camera.Position = .back public private(set) var videoDevice: AVCaptureDevice? = nil { didSet { self.videoDevicePromise.set(.single(self.videoDevice)) } } private var videoDevicePromise = Promise() public private(set) var audioDevice: AVCaptureDevice? = nil func configure(for session: AVCaptureSession, position: Camera.Position) { self.position = position if let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: position).devices.first { self.videoDevice = videoDevice self.videoDevicePromise.set(.single(videoDevice)) } self.audioDevice = AVCaptureDevice.default(for: .audio) } func configureDeviceFormat(maxDimensions: CMVideoDimensions, maxFramerate: Double) { guard let device = self.videoDevice else { return } self.transaction(device) { device in var maxWidth: Int32 = 0 var maxHeight: Int32 = 0 var hasSecondaryZoomLevels = false var candidates: [AVCaptureDevice.Format] = [] outer: for format in device.formats { if format.mediaType != .video || format.value(forKey: "isPhotoFormat") as? Bool == true { continue } let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription) if dimensions.width >= maxWidth && dimensions.width <= maxDimensions.width && dimensions.height >= maxHeight && dimensions.height <= maxDimensions.height { if dimensions.width > maxWidth { hasSecondaryZoomLevels = false candidates.removeAll() } let subtype = CMFormatDescriptionGetMediaSubType(format.formatDescription) if subtype == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange { for range in format.videoSupportedFrameRateRanges { if range.maxFrameRate > maxFramerate { continue outer } } maxWidth = dimensions.width maxHeight = dimensions.height if #available(iOS 16.0, *), !format.secondaryNativeResolutionZoomFactors.isEmpty { hasSecondaryZoomLevels = true candidates.append(format) } else if !hasSecondaryZoomLevels { candidates.append(format) } } } } if let bestFormat = candidates.last { device.activeFormat = bestFormat Logger.shared.log("Camera", "Available formats:") for format in device.formats { Logger.shared.log("Camera", format.description) } Logger.shared.log("Camera", "Selected format:") Logger.shared.log("Camera", bestFormat.description) } if let targetFPS = device.actualFPS(maxFramerate) { device.activeVideoMinFrameDuration = targetFPS.duration device.activeVideoMaxFrameDuration = targetFPS.duration } } } func transaction(_ device: AVCaptureDevice, update: (AVCaptureDevice) -> Void) { if let _ = try? device.lockForConfiguration() { update(device) device.unlockForConfiguration() } } private func subscribeForChanges() { NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaChanged), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice) } private func unsubscribeFromChanges() { NotificationCenter.default.removeObserver(self, name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: self.videoDevice) } @objc private func subjectAreaChanged() { self.setFocusPoint(CGPoint(x: 0.5, y: 0.5), focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: false) } var fps: Double = defaultFPS { didSet { guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else { return } self.fps = targetFPS.fps self.transaction(device) { device in device.activeVideoMinFrameDuration = targetFPS.duration device.activeVideoMaxFrameDuration = targetFPS.duration } } } var isTorchAvailable: Signal { return self.videoDevicePromise.get() |> mapToSignal { device -> Signal in return Signal { subscriber in guard let device else { return EmptyDisposable } subscriber.putNext(device.isFlashAvailable) let observer = device.observe(\.isFlashAvailable, options: [.new], changeHandler: { device, _ in subscriber.putNext(device.isFlashAvailable) }) return ActionDisposable { observer.invalidate() } } |> distinctUntilChanged } } var isAdjustingFocus: Signal { return self.videoDevicePromise.get() |> mapToSignal { device -> Signal in return Signal { subscriber in guard let device else { return EmptyDisposable } subscriber.putNext(device.isAdjustingFocus) let observer = device.observe(\.isAdjustingFocus, options: [.new], changeHandler: { device, _ in subscriber.putNext(device.isAdjustingFocus) }) return ActionDisposable { observer.invalidate() } } |> distinctUntilChanged } } func setFocusPoint(_ point: CGPoint, focusMode: Camera.FocusMode, exposureMode: Camera.ExposureMode, monitorSubjectAreaChange: Bool) { guard let device = self.videoDevice else { return } self.transaction(device) { device in if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) { device.exposurePointOfInterest = point device.exposureMode = exposureMode } if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) { device.focusPointOfInterest = point device.focusMode = focusMode } } } func setExposureTargetBias(_ bias: Float) { guard let device = self.videoDevice else { return } self.transaction(device) { device in let extremum = (bias >= 0) ? device.maxExposureTargetBias : device.minExposureTargetBias; let value = abs(bias) * extremum * 0.85 device.setExposureTargetBias(value, completionHandler: nil) } } func setTorchActive(_ active: Bool) { guard let device = self.videoDevice else { return } self.transaction(device) { device in device.torchMode = active ? .on : .off } } func setZoomLevel(_ zoomLevel: CGFloat) { guard let device = self.videoDevice else { return } self.transaction(device) { device in device.videoZoomFactor = max(1.0, min(10.0, zoomLevel)) } } }