360 lines
9.2 KiB
Swift
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
|
|
}
|
|
}
|