Files
Schnappix/node_modules/expo-camera/ios/Current/CameraSessionManager.swift
T

360 lines
9.2 KiB
Swift

import UIKit
@preconcurrency import AVFoundation
import ExpoModulesCore
protocol CameraSessionManagerDelegate: AnyObject {
var sessionQueue: DispatchQueue { get }
var videoQuality: VideoQuality { get }
var mode: CameraMode { get }
var pictureSize: PictureSize { get }
var isMuted: Bool { get }
var active: Bool { get }
var presetCamera: AVCaptureDevice.Position { get }
var selectedLens: String? { get }
var torchEnabled: Bool { get }
var autoFocus: AVCaptureDevice.FocusMode { get }
var zoom: CGFloat { get }
var onMountError: EventDispatcher { get }
var onCameraReady: EventDispatcher { get }
var permissionsManager: EXPermissionsInterface? { get }
var appContext: AppContext? { get }
var barcodeScanner: BarcodeScanner? { get }
func emitAvailableLenses()
func changePreviewOrientation()
}
class CameraSessionManager: NSObject {
weak var delegate: CameraSessionManagerDelegate?
let session = AVCaptureSession()
private let deviceDiscovery = DeviceDiscovery()
private var captureDeviceInput: AVCaptureDeviceInput?
private var photoOutput: AVCapturePhotoOutput?
private var videoFileOutput: AVCaptureMovieFileOutput?
init(delegate: CameraSessionManagerDelegate) {
self.delegate = delegate
super.init()
}
func initializeCaptureSessionInput() {
guard let delegate else {
return
}
delegate.sessionQueue.async {
self.updateDevice()
self.startSession()
}
}
func updateSessionPreset(preset: AVCaptureSession.Preset) {
#if !targetEnvironment(simulator)
if session.canSetSessionPreset(preset) {
if session.sessionPreset != preset {
session.beginConfiguration()
defer { session.commitConfiguration() }
session.sessionPreset = preset
}
} else {
// The selected preset cannot be used on the current device so we fall back to the highest available.
if session.sessionPreset != .high {
session.beginConfiguration()
defer { session.commitConfiguration() }
session.sessionPreset = .high
}
}
#endif
}
func updateDevice() {
guard let delegate else {
return
}
let lenses = delegate.presetCamera == .back
? deviceDiscovery.backCameraLenses
: deviceDiscovery.frontCameraLenses
let selectedDevice = lenses.first {
$0.localizedName == delegate.selectedLens
}
if let selectedDevice {
addDevice(selectedDevice)
} else {
let device = delegate.presetCamera == .back
? deviceDiscovery.defaultBackCamera
: deviceDiscovery.defaultFrontCamera
if let device {
addDevice(device)
}
}
}
func updateCameraIsActive() {
guard let delegate else {
return
}
delegate.sessionQueue.async {
if delegate.active {
if !self.session.isRunning {
self.session.startRunning()
}
} else {
self.session.stopRunning()
}
}
}
func setCameraMode() {
guard let delegate else {
return
}
if delegate.mode == .video {
if videoFileOutput == nil {
setupMovieFileCapture()
}
updateSessionAudioIsMuted()
} else {
cleanupMovieFileCapture()
}
}
func updateSessionAudioIsMuted() {
guard let delegate else {
return
}
session.beginConfiguration()
defer { session.commitConfiguration() }
if delegate.isMuted {
for input in session.inputs {
if let deviceInput = input as? AVCaptureDeviceInput {
if deviceInput.device.hasMediaType(.audio) {
session.removeInput(input)
return
}
}
}
}
if !delegate.isMuted && delegate.mode == .video {
if let audioCapturedevice = AVCaptureDevice.default(for: .audio) {
do {
let audioDeviceInput = try AVCaptureDeviceInput(device: audioCapturedevice)
if session.canAddInput(audioDeviceInput) {
session.addInput(audioDeviceInput)
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
}
}
}
func enableTorch() {
guard let delegate, let device = captureDeviceInput?.device, device.hasTorch else {
return
}
do {
try device.lockForConfiguration()
if device.hasTorch && device.isTorchModeSupported(.on) {
device.torchMode = delegate.torchEnabled ? .on : .off
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
device.unlockForConfiguration()
}
func setFocusMode() {
guard let device = captureDeviceInput?.device, let delegate else {
return
}
do {
try device.lockForConfiguration()
if device.isFocusModeSupported(delegate.autoFocus), device.focusMode != delegate.autoFocus {
device.focusMode = delegate.autoFocus
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
return
}
device.unlockForConfiguration()
}
func updateZoom() {
guard let device = captureDeviceInput?.device, let delegate else {
return
}
do {
try device.lockForConfiguration()
let minZoom = 1.0
device.videoZoomFactor = minZoom * pow(device.activeFormat.videoMaxZoomFactor / minZoom, delegate.zoom)
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
device.unlockForConfiguration()
}
func getAvailableLenses() -> [String] {
guard let delegate else {
return []
}
let availableLenses = delegate.presetCamera == AVCaptureDevice.Position.back
? deviceDiscovery.backCameraLenses
: deviceDiscovery.frontCameraLenses
// Lens ordering can be varied which causes problems if you keep the result in react state.
// We sort them to provide a stable ordering
return availableLenses.map { $0.localizedName }.sorted {
$0 < $1
}
}
func setupMovieFileCapture() {
let output = AVCaptureMovieFileOutput()
if session.canAddOutput(output) {
session.addOutput(output)
videoFileOutput = output
}
}
func cleanupMovieFileCapture() {
if let videoFileOutput {
if session.outputs.contains(videoFileOutput) {
session.removeOutput(videoFileOutput)
self.videoFileOutput = nil
}
}
}
func stopSession() {
#if targetEnvironment(simulator)
return
#else
session.beginConfiguration()
for input in self.session.inputs {
session.removeInput(input)
}
for output in session.outputs {
session.removeOutput(output)
}
session.commitConfiguration()
if session.isRunning {
session.stopRunning()
}
#endif
}
func addErrorNotification() {
guard let delegate else {
return
}
Task {
let errors = NotificationCenter.default.notifications(named: .AVCaptureSessionRuntimeError, object: self.session)
.compactMap({ $0.userInfo?[AVCaptureSessionErrorKey] as? AVError })
for await error in errors where error.code == .mediaServicesWereReset {
if !session.isRunning {
session.startRunning()
}
delegate.sessionQueue.async {
self.updateSessionAudioIsMuted()
}
delegate.onMountError(["message": "Camera session was reset"])
}
}
}
var currentPhotoOutput: AVCapturePhotoOutput? {
return photoOutput
}
var currentVideoFileOutput: AVCaptureMovieFileOutput? {
return videoFileOutput
}
var currentDevice: AVCaptureDevice? {
return captureDeviceInput?.device
}
private func addDevice(_ device: AVCaptureDevice) {
guard let delegate else {
return
}
session.beginConfiguration()
defer {
session.commitConfiguration()
delegate.emitAvailableLenses()
}
if let captureDeviceInput {
session.removeInput(captureDeviceInput)
}
do {
let deviceInput = try AVCaptureDeviceInput(device: device)
if session.canAddInput(deviceInput) {
session.addInput(deviceInput)
captureDeviceInput = deviceInput
updateZoom()
}
} catch {
delegate.onMountError(["message": "Camera could not be started - \(error.localizedDescription)"])
}
}
private func startSession() {
#if targetEnvironment(simulator)
return
#else
guard let delegate else {
return
}
guard let manager = delegate.permissionsManager else {
log.info("Permissions module not found.")
return
}
if !manager.hasGrantedPermission(usingRequesterClass: CameraOnlyPermissionRequester.self) {
delegate.onMountError(["message": "Camera permissions not granted - component could not be rendered."])
return
}
let photoOutput = AVCapturePhotoOutput()
photoOutput.isLivePhotoCaptureEnabled = false
session.beginConfiguration()
if session.canAddOutput(photoOutput) {
session.addOutput(photoOutput)
self.photoOutput = photoOutput
}
session.sessionPreset = delegate.mode == .video
? delegate.videoQuality.toPreset()
: delegate.pictureSize.toCapturePreset()
session.commitConfiguration()
addErrorNotification()
delegate.changePreviewOrientation()
delegate.barcodeScanner?.maybeStartBarcodeScanning()
updateCameraIsActive()
DispatchQueue.main.async { [weak delegate] in
delegate?.onCameraReady()
}
enableTorch()
#endif
}
}