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

355 lines
9.8 KiB
Swift

import UIKit
@preconcurrency import ExpoModulesCore
import CoreMotion
public class CameraView: ExpoView, EXAppLifecycleListener, EXCameraInterface, CameraEvent,
CameraSessionManagerDelegate, CameraPhotoCaptureDelegate, CameraVideoRecordingDelegate {
public var sessionQueue = DispatchQueue(label: "captureSessionQueue")
// Needed to satisfy EXCameraInterface
// swiftlint:disable implicitly_unwrapped_optional
public var session: AVCaptureSession!
private var sessionManager: CameraSessionManager!
private var photoCapture: CameraPhotoCapture!
private var videoRecording: CameraVideoRecording!
// swiftlint:enable implicitly_unwrapped_optional
private var lifecycleManager: EXAppLifecycleService?
internal var permissionsManager: EXPermissionsInterface?
internal var barcodeScanner: BarcodeScanner?
internal lazy var previewLayer = AVCaptureVideoPreviewLayer(session: self.session)
internal var physicalOrientation: UIDeviceOrientation = .unknown
private var motionManager: CMMotionManager = {
let mm = CMMotionManager()
mm.accelerometerUpdateInterval = 0.2
mm.gyroUpdateInterval = 0.2
return mm
}()
private var isSessionPaused = false
// MARK: Property Observers
var responsiveWhenOrientationLocked = false {
didSet {
updateResponsiveOrientation()
}
}
var videoQuality: VideoQuality = .video1080p {
didSet {
sessionQueue.async {
if self.mode == .video {
self.sessionManager.updateSessionPreset(preset: self.videoQuality.toPreset())
}
}
}
}
var isScanningBarcodes = false {
didSet {
sessionQueue.async { [weak self] in
guard let self else {
return
}
barcodeScanner?.setIsEnabled(isScanningBarcodes)
}
}
}
internal var videoBitrate: Int?
var presetCamera = AVCaptureDevice.Position.back {
didSet {
sessionQueue.async {
self.sessionManager.updateDevice()
}
}
}
var flashMode = FlashMode.auto
var torchEnabled = false {
didSet {
sessionManager.enableTorch()
}
}
var autoFocus = AVCaptureDevice.FocusMode.continuousAutoFocus {
didSet {
sessionManager.setFocusMode()
}
}
var pictureSize = PictureSize.high {
didSet {
updatePictureSize()
}
}
var mode = CameraMode.picture {
didSet {
sessionQueue.async {
self.sessionManager.setCameraMode()
let preset = self.mode == .video ? self.videoQuality.toPreset() : self.pictureSize.toCapturePreset()
self.sessionManager.updateSessionPreset(preset: preset)
}
}
}
var isMuted = false {
didSet {
sessionQueue.async {
self.sessionManager.updateSessionAudioIsMuted()
}
}
}
var active = true {
didSet {
sessionQueue.async {
self.sessionManager.updateCameraIsActive()
}
}
}
var selectedLens: String? {
didSet {
sessionQueue.async {
self.sessionManager.updateDevice()
}
}
}
var animateShutter = true
var mirror = false
var zoom: CGFloat = 0 {
didSet {
sessionManager.updateZoom()
}
}
// MARK: - Events
let onCameraReady = EventDispatcher()
let onMountError = EventDispatcher()
let onPictureSaved = EventDispatcher()
let onBarcodeScanned = EventDispatcher()
let onResponsiveOrientationChanged = EventDispatcher()
let onAvailableLensesChanged = EventDispatcher()
internal var deviceOrientation: UIInterfaceOrientation {
UIApplication.shared.connectedScenes.compactMap {
$0 as? UIWindowScene
}.first?.interfaceOrientation ?? .unknown
}
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
lifecycleManager = appContext?.legacyModule(implementing: EXAppLifecycleService.self)
permissionsManager = appContext?.legacyModule(implementing: EXPermissionsInterface.self)
sessionManager = CameraSessionManager(delegate: self)
photoCapture = CameraPhotoCapture(delegate: self)
videoRecording = CameraVideoRecording(delegate: self)
session = sessionManager.session
#if !targetEnvironment(simulator)
setupPreview()
#endif
barcodeScanner = createBarcodeScanner()
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(
self,
selector: #selector(orientationChanged),
name: UIDevice.orientationDidChangeNotification,
object: nil)
lifecycleManager?.register(self)
sessionManager.initializeCaptureSessionInput()
}
private func setupPreview() {
previewLayer = AVCaptureVideoPreviewLayer(session: sessionManager.session)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.needsDisplayOnBoundsChange = true
}
public func onAppForegrounded() {
sessionQueue.async { [weak self] in
guard let self else {
return
}
if !session.isRunning && isSessionPaused {
isSessionPaused = false
session.startRunning()
if torchEnabled {
sessionManager.enableTorch()
}
}
}
}
public func onAppBackgrounded() {
sessionQueue.async { [weak self] in
guard let self else {
return
}
if session.isRunning && !isSessionPaused {
isSessionPaused = true
session.stopRunning()
}
}
}
private func updatePictureSize() {
#if !targetEnvironment(simulator)
sessionQueue.async {
if self.mode == .picture {
let preset = self.pictureSize.toCapturePreset()
self.sessionManager.updateSessionPreset(preset: preset)
}
}
#endif
}
func setBarcodeScannerSettings(settings: BarcodeSettings) {
sessionQueue.async {
self.barcodeScanner?.setSettings([BARCODE_TYPES_KEY: settings.toMetadataObjectType()])
}
}
func emitAvailableLenses() {
DispatchQueue.main.async { [weak self] in
guard let self else {
return
}
self.onAvailableLensesChanged([
"lenses": self.sessionManager.getAvailableLenses()
])
}
}
func getAvailableLenses() -> [String] {
return sessionManager.getAvailableLenses()
}
func updateResponsiveOrientation() {
if responsiveWhenOrientationLocked {
motionManager.startAccelerometerUpdates(to: OperationQueue()) { [weak self] _, error in
if error != nil {
return
}
guard let self, let accelerometerData = self.motionManager.accelerometerData else {
return
}
let deviceOrientation = ExpoCameraUtils.deviceOrientation(
for: accelerometerData,
default: self.physicalOrientation)
if deviceOrientation != self.physicalOrientation {
self.physicalOrientation = deviceOrientation
self.onResponsiveOrientationChanged(["orientation": ExpoCameraUtils.toOrientationString(orientation: deviceOrientation)])
}
}
} else {
motionManager.stopAccelerometerUpdates()
}
}
func takePictureRef(options: TakePictureOptions) async throws -> PictureRef {
guard let photoOutput = sessionManager.currentPhotoOutput else {
throw CameraOutputNotReadyException()
}
return try await photoCapture.takePictureRef(options: options, photoOutput: photoOutput)
}
func takePicturePromise(options: TakePictureOptions) async throws -> [String: Any] {
guard let photoOutput = sessionManager.currentPhotoOutput else {
throw CameraOutputNotReadyException()
}
return try await photoCapture.takePicturePromise(options: options, photoOutput: photoOutput)
}
func record(options: CameraRecordingOptions, promise: Promise) async {
guard let videoFileOutput = sessionManager.currentVideoFileOutput else {
promise.reject(CameraOutputNotReadyException())
return
}
await videoRecording.record(options: options, videoFileOutput: videoFileOutput, promise: promise)
}
@available(iOS 18.0, *)
func toggleRecording() {
guard let videoFileOutput = sessionManager.currentVideoFileOutput else {
return
}
videoRecording.toggleRecording(videoFileOutput: videoFileOutput)
}
public override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = .black
previewLayer.frame = self.bounds
self.layer.insertSublayer(previewLayer, at: 0)
}
public override func removeFromSuperview() {
super.removeFromSuperview()
sessionQueue.async {
self.sessionManager.stopSession()
}
lifecycleManager?.unregisterAppLifecycleListener(self)
UIDevice.current.endGeneratingDeviceOrientationNotifications()
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}
func stopRecording() {
videoRecording.stopRecording(videoFileOutput: sessionManager.currentVideoFileOutput)
}
func resumePreview() {
previewLayer.connection?.isEnabled = true
}
func pausePreview() {
previewLayer.connection?.isEnabled = false
}
@objc func orientationChanged() {
changePreviewOrientation()
}
func changePreviewOrientation() {
// We shouldn't access the device orientation anywhere but on the main thread
Task { @MainActor in
let videoOrientation = ExpoCameraUtils.videoOrientation(for: deviceOrientation)
if (previewLayer.connection?.isVideoOrientationSupported) == true {
physicalOrientation = ExpoCameraUtils.physicalOrientation(for: deviceOrientation)
previewLayer.connection?.videoOrientation = videoOrientation
}
}
}
private func createBarcodeScanner() -> BarcodeScanner {
let scanner = BarcodeScanner(session: session, sessionQueue: sessionQueue)
scanner.setPreviewLayer(layer: previewLayer)
scanner.setOnBarcodeScanned { [weak self] body in
guard let self else {
return
}
if let body {
self.onBarcodeScanned(body)
}
}
return scanner
}
deinit {
photoCapture.cleanup()
videoRecording.cleanup()
}
}