Extract AVCaptureSession
and AVAudioSession
setup to extensions
This commit is contained in:
parent
56c67f25c7
commit
9404b93dc3
@ -279,7 +279,7 @@ PODS:
|
||||
- react-native-video/Video (= 5.1.1)
|
||||
- react-native-video/Video (5.1.1):
|
||||
- React-Core
|
||||
- react-native-vision-camera (1.0.3):
|
||||
- react-native-vision-camera (1.0.4):
|
||||
- React-Core
|
||||
- React-perflogger (0.64.0)
|
||||
- React-RCTActionSheet (0.64.0):
|
||||
@ -554,9 +554,9 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
|
||||
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
|
||||
DoubleConversion: cde416483dac037923206447da6e1454df403714
|
||||
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
|
||||
FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5
|
||||
FBReactNativeSpec: e800dc469340da7e8e47f45145f69d75a7b06874
|
||||
FBReactNativeSpec: 06c29ba6920affcab9cda6154497386d21f43410
|
||||
Flipper: d3da1aa199aad94455ae725e9f3aa43f3ec17021
|
||||
Flipper-DoubleConversion: 38631e41ef4f9b12861c67d17cb5518d06badc41
|
||||
Flipper-Folly: f7a3caafbd74bda4827954fd7a6e000e36355489
|
||||
@ -564,7 +564,7 @@ SPEC CHECKSUMS:
|
||||
Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9
|
||||
Flipper-RSocket: 602921fee03edacf18f5d6f3d3594ba477f456e5
|
||||
FlipperKit: 8a20b5c5fcf9436cac58551dc049867247f64b00
|
||||
glog: 40a13f7840415b9a77023fbcae0f1e6f43192af3
|
||||
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
|
||||
hermes-engine: 7d97ba46a1e29bacf3e3c61ecb2804a5ddd02d4f
|
||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||
OpenSSL-Universal: 1aa4f6a6ee7256b83db99ec1ccdaa80d10f9af9b
|
||||
@ -583,7 +583,7 @@ SPEC CHECKSUMS:
|
||||
react-native-cameraroll: 1965db75c851b15e77a22ca0ac78e32af6b571ae
|
||||
react-native-slider: e99fc201cefe81270fc9d81714a7a0f5e566b168
|
||||
react-native-video: 0bb76b6d6b77da3009611586c7dbf817b947f30e
|
||||
react-native-vision-camera: 83bc97de3bc01be3a99037dd4cf6c672aef632b7
|
||||
react-native-vision-camera: d0d6fdd334f1536d016b3ca92064f25a6312e09c
|
||||
React-perflogger: 9c547d8f06b9bf00cb447f2b75e8d7f19b7e02af
|
||||
React-RCTActionSheet: 3080b6e12e0e1a5b313c8c0050699b5c794a1b11
|
||||
React-RCTAnimation: 3f96f21a497ae7dabf4d2f150ee43f906aaf516f
|
||||
|
75
ios/CameraView+AVAudioSession.swift
Normal file
75
ios/CameraView+AVAudioSession.swift
Normal file
@ -0,0 +1,75 @@
|
||||
//
|
||||
// CameraView+AVAudioSession.swift
|
||||
// VisionCamera
|
||||
//
|
||||
// Created by Marc Rousavy on 26.03.21.
|
||||
// Copyright © 2021 Facebook. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
/**
|
||||
Extension for CameraView that sets up the AVAudioSession.
|
||||
*/
|
||||
extension CameraView {
|
||||
@objc
|
||||
func audioSessionInterrupted(notification: Notification) {
|
||||
ReactLogger.log(level: .error, message: "The Audio Session was interrupted!")
|
||||
guard let userInfo = notification.userInfo,
|
||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .began:
|
||||
// TODO: Should we also disable the camera here? I think it will throw a runtime error
|
||||
// disable audio session
|
||||
try? AVAudioSession.sharedInstance().setActive(false)
|
||||
break
|
||||
case .ended:
|
||||
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
||||
if options.contains(.shouldResume) {
|
||||
// restart audio session because interruption is over
|
||||
configureAudioSession()
|
||||
} else {
|
||||
ReactLogger.log(level: .error, message: "Cannot resume interrupted Audio Session!")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private final func setAutomaticallyConfiguresAudioSession(_ automaticallyConfiguresAudioSession: Bool) {
|
||||
if captureSession.automaticallyConfiguresApplicationAudioSession != automaticallyConfiguresAudioSession {
|
||||
captureSession.beginConfiguration()
|
||||
captureSession.automaticallyConfiguresApplicationAudioSession = automaticallyConfiguresAudioSession
|
||||
captureSession.commitConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Configures the Audio session to allow background-music playback while recording.
|
||||
*/
|
||||
internal final func configureAudioSession() {
|
||||
let start = DispatchTime.now()
|
||||
do {
|
||||
setAutomaticallyConfiguresAudioSession(false)
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
if audioSession.category != .playAndRecord {
|
||||
// allow background music playback
|
||||
try audioSession.setCategory(AVAudioSession.Category.playAndRecord, options: [.mixWithOthers, .allowBluetoothA2DP, .defaultToSpeaker])
|
||||
}
|
||||
// TODO: Use https://developer.apple.com/documentation/avfaudio/avaudiosession/3726094-setprefersnointerruptionsfromsys
|
||||
audioSession.trySetAllowHaptics(true)
|
||||
// activate current audio session because camera is active
|
||||
try audioSession.setActive(true)
|
||||
} catch let error as NSError {
|
||||
self.invokeOnError(.session(.audioSessionSetupFailed(reason: error.description)), cause: error)
|
||||
setAutomaticallyConfiguresAudioSession(true)
|
||||
}
|
||||
let end = DispatchTime.now()
|
||||
let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds
|
||||
ReactLogger.log(level: .info, message: "Configured Audio session in \(Double(nanoTime) / 1_000_000)ms!")
|
||||
}
|
||||
}
|
261
ios/CameraView+AVCaptureSession.swift
Normal file
261
ios/CameraView+AVCaptureSession.swift
Normal file
@ -0,0 +1,261 @@
|
||||
//
|
||||
// CameraView+AVCaptureSession.swift
|
||||
// VisionCamera
|
||||
//
|
||||
// Created by Marc Rousavy on 26.03.21.
|
||||
// Copyright © 2021 Facebook. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
/**
|
||||
Extension for CameraView that sets up the AVCaptureSession, Device and Format.
|
||||
*/
|
||||
extension CameraView {
|
||||
@objc
|
||||
func sessionRuntimeError(notification: Notification) {
|
||||
ReactLogger.log(level: .error, message: "Unexpected Camera Runtime Error occured!")
|
||||
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else {
|
||||
return
|
||||
}
|
||||
|
||||
invokeOnError(.unknown(message: error._nsError.description), cause: error._nsError)
|
||||
|
||||
if isActive {
|
||||
// restart capture session after an error occured
|
||||
queue.async {
|
||||
self.captureSession.startRunning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Configures the Capture Session.
|
||||
*/
|
||||
internal final func configureCaptureSession() {
|
||||
ReactLogger.logJS(level: .info, message: "Configuring Session...")
|
||||
isReady = false
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
return invokeOnError(.device(.notAvailableOnSimulator))
|
||||
#endif
|
||||
|
||||
guard cameraId != nil else {
|
||||
return invokeOnError(.device(.noDevice))
|
||||
}
|
||||
let cameraId = self.cameraId! as String
|
||||
|
||||
ReactLogger.log(level: .info, message: "Initializing Camera with device \(cameraId)...")
|
||||
captureSession.beginConfiguration()
|
||||
defer {
|
||||
captureSession.commitConfiguration()
|
||||
}
|
||||
|
||||
if let preset = self.preset {
|
||||
var sessionPreset: AVCaptureSession.Preset?
|
||||
do {
|
||||
sessionPreset = try AVCaptureSession.Preset(withString: preset)
|
||||
} catch let EnumParserError.unsupportedOS(supportedOnOS: os) {
|
||||
return invokeOnError(.parameter(.unsupportedOS(unionName: "Preset", receivedValue: preset, supportedOnOs: os)))
|
||||
} catch {
|
||||
return invokeOnError(.parameter(.invalid(unionName: "Preset", receivedValue: preset)))
|
||||
}
|
||||
if sessionPreset != nil {
|
||||
if captureSession.canSetSessionPreset(sessionPreset!) {
|
||||
captureSession.sessionPreset = sessionPreset!
|
||||
} else {
|
||||
// non-fatal error, so continue with configuration
|
||||
invokeOnError(.format(.invalidPreset(preset: preset)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// INPUTS
|
||||
// Video Input
|
||||
do {
|
||||
if let videoDeviceInput = self.videoDeviceInput {
|
||||
captureSession.removeInput(videoDeviceInput)
|
||||
}
|
||||
guard let videoDevice = AVCaptureDevice(uniqueID: cameraId) else {
|
||||
return invokeOnError(.device(.invalid))
|
||||
}
|
||||
zoom = NSNumber(value: Double(videoDevice.neutralZoomPercent))
|
||||
videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
||||
guard captureSession.canAddInput(videoDeviceInput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "video-input")))
|
||||
}
|
||||
captureSession.addInput(videoDeviceInput!)
|
||||
} catch {
|
||||
return invokeOnError(.device(.invalid))
|
||||
}
|
||||
|
||||
// Microphone (Audio Input)
|
||||
do {
|
||||
if let audioDeviceInput = self.audioDeviceInput {
|
||||
captureSession.removeInput(audioDeviceInput)
|
||||
}
|
||||
guard let audioDevice = AVCaptureDevice.default(for: .audio) else {
|
||||
return invokeOnError(.device(.microphoneUnavailable))
|
||||
}
|
||||
|
||||
audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
|
||||
guard captureSession.canAddInput(audioDeviceInput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "audio-input")))
|
||||
}
|
||||
captureSession.addInput(audioDeviceInput!)
|
||||
} catch {
|
||||
return invokeOnError(.device(.invalid))
|
||||
}
|
||||
|
||||
// OUTPUTS
|
||||
if let photoOutput = self.photoOutput {
|
||||
captureSession.removeOutput(photoOutput)
|
||||
}
|
||||
// Photo Output
|
||||
photoOutput = AVCapturePhotoOutput()
|
||||
photoOutput!.isDepthDataDeliveryEnabled = photoOutput!.isDepthDataDeliverySupported && enableDepthData
|
||||
if let enableHighResolutionCapture = self.enableHighResolutionCapture?.boolValue {
|
||||
photoOutput!.isHighResolutionCaptureEnabled = enableHighResolutionCapture
|
||||
}
|
||||
if #available(iOS 12.0, *) {
|
||||
photoOutput!.isPortraitEffectsMatteDeliveryEnabled = photoOutput!.isPortraitEffectsMatteDeliverySupported && self.enablePortraitEffectsMatteDelivery
|
||||
}
|
||||
guard captureSession.canAddOutput(photoOutput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "photo-output")))
|
||||
}
|
||||
captureSession.addOutput(photoOutput!)
|
||||
if videoDeviceInput!.device.position == .front {
|
||||
photoOutput!.mirror()
|
||||
}
|
||||
|
||||
// Video Output
|
||||
if let movieOutput = self.movieOutput {
|
||||
captureSession.removeOutput(movieOutput)
|
||||
}
|
||||
movieOutput = AVCaptureMovieFileOutput()
|
||||
guard captureSession.canAddOutput(movieOutput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "movie-output")))
|
||||
}
|
||||
captureSession.addOutput(movieOutput!)
|
||||
if videoDeviceInput!.device.position == .front {
|
||||
movieOutput!.mirror()
|
||||
}
|
||||
|
||||
// Barcode Scanning
|
||||
if let metadataOutput = self.metadataOutput {
|
||||
captureSession.removeOutput(metadataOutput)
|
||||
}
|
||||
if let scannableCodes = self.scannableCodes {
|
||||
// scannableCodes prop is not nil, so enable barcode scanning.
|
||||
guard onCodeScanned != nil else {
|
||||
return invokeOnError(.parameter(.invalidCombination(provided: "scannableCodes", missing: "onCodeScanned")))
|
||||
}
|
||||
metadataOutput = AVCaptureMetadataOutput()
|
||||
guard captureSession.canAddOutput(metadataOutput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "metadata-output")))
|
||||
}
|
||||
captureSession.addOutput(metadataOutput!)
|
||||
metadataOutput!.setMetadataObjectsDelegate(self, queue: queue)
|
||||
var objectTypes: [AVMetadataObject.ObjectType] = []
|
||||
scannableCodes.forEach { code in
|
||||
do {
|
||||
objectTypes.append(try AVMetadataObject.ObjectType(withString: code))
|
||||
} catch let EnumParserError.unsupportedOS(supportedOnOS: os) {
|
||||
invokeOnError(.parameter(.unsupportedOS(unionName: "CodeType", receivedValue: code, supportedOnOs: os)))
|
||||
} catch {
|
||||
invokeOnError(.parameter(.invalid(unionName: "CodeType", receivedValue: code)))
|
||||
}
|
||||
}
|
||||
metadataOutput!.metadataObjectTypes = objectTypes
|
||||
}
|
||||
|
||||
invokeOnInitialized()
|
||||
isReady = true
|
||||
ReactLogger.logJS(level: .info, message: "Session successfully configured!")
|
||||
}
|
||||
|
||||
/**
|
||||
Configures the Video Device to find the best matching Format.
|
||||
*/
|
||||
internal final func configureFormat() {
|
||||
ReactLogger.logJS(level: .info, message: "Configuring Format...")
|
||||
guard let filter = self.format else {
|
||||
// Format Filter was null. Ignore it.
|
||||
return
|
||||
}
|
||||
guard let device = videoDeviceInput?.device else {
|
||||
return invokeOnError(.session(.cameraNotReady))
|
||||
}
|
||||
|
||||
if device.activeFormat.matchesFilter(filter) {
|
||||
ReactLogger.log(level: .info, message: "Active format already matches filter.")
|
||||
return
|
||||
}
|
||||
|
||||
// get matching format
|
||||
let matchingFormats = device.formats.filter { $0.matchesFilter(filter) }.sorted { $0.isBetterThan($1) }
|
||||
guard let format = matchingFormats.first else {
|
||||
return invokeOnError(.format(.invalidFormat))
|
||||
}
|
||||
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
device.activeFormat = format
|
||||
device.unlockForConfiguration()
|
||||
ReactLogger.logJS(level: .info, message: "Format successfully configured!")
|
||||
} catch let error as NSError {
|
||||
return invokeOnError(.device(.configureError), cause: error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Configures the Video Device with the given FPS, HDR and ColorSpace.
|
||||
*/
|
||||
internal final func configureDevice() {
|
||||
ReactLogger.logJS(level: .info, message: "Configuring Device...")
|
||||
guard let device = videoDeviceInput?.device else {
|
||||
return invokeOnError(.session(.cameraNotReady))
|
||||
}
|
||||
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
|
||||
if let fps = self.fps?.int32Value {
|
||||
let duration = CMTimeMake(value: 1, timescale: fps)
|
||||
device.activeVideoMinFrameDuration = duration
|
||||
device.activeVideoMaxFrameDuration = duration
|
||||
} else {
|
||||
device.activeVideoMinFrameDuration = CMTime.invalid
|
||||
device.activeVideoMaxFrameDuration = CMTime.invalid
|
||||
}
|
||||
if hdr != nil {
|
||||
if hdr == true && !device.activeFormat.isVideoHDRSupported {
|
||||
return invokeOnError(.format(.invalidHdr))
|
||||
}
|
||||
if !device.automaticallyAdjustsVideoHDREnabled {
|
||||
if device.isVideoHDREnabled != hdr!.boolValue {
|
||||
device.isVideoHDREnabled = hdr!.boolValue
|
||||
}
|
||||
}
|
||||
}
|
||||
if lowLightBoost != nil {
|
||||
if lowLightBoost == true && !device.isLowLightBoostSupported {
|
||||
return invokeOnError(.device(.lowLightBoostNotSupported))
|
||||
}
|
||||
if device.automaticallyEnablesLowLightBoostWhenAvailable != lowLightBoost!.boolValue {
|
||||
device.automaticallyEnablesLowLightBoostWhenAvailable = lowLightBoost!.boolValue
|
||||
}
|
||||
}
|
||||
if colorSpace != nil, let avColorSpace = try? AVCaptureColorSpace(string: String(colorSpace!)) {
|
||||
device.activeColorSpace = avColorSpace
|
||||
}
|
||||
|
||||
device.unlockForConfiguration()
|
||||
ReactLogger.logJS(level: .info, message: "Device successfully configured!")
|
||||
} catch let error as NSError {
|
||||
return invokeOnError(.device(.configureError), cause: error)
|
||||
}
|
||||
}
|
||||
}
|
@ -136,6 +136,8 @@ final class CameraView: UIView {
|
||||
override class var layerClass: AnyClass {
|
||||
return AVCaptureVideoPreviewLayer.self
|
||||
}
|
||||
|
||||
internal let captureSession = AVCaptureSession()
|
||||
|
||||
// pragma MARK: Exported Properties
|
||||
// props that require reconfiguring
|
||||
@ -197,49 +199,6 @@ final class CameraView: UIView {
|
||||
return layer as! AVCaptureVideoPreviewLayer
|
||||
}
|
||||
|
||||
@objc
|
||||
func sessionRuntimeError(notification: Notification) {
|
||||
ReactLogger.log(level: .error, message: "Unexpected Camera Runtime Error occured!")
|
||||
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else {
|
||||
return
|
||||
}
|
||||
|
||||
if isActive {
|
||||
// restart capture session after an error occured
|
||||
queue.async {
|
||||
self.captureSession.startRunning()
|
||||
}
|
||||
}
|
||||
invokeOnError(.unknown(message: error.description), cause: error as NSError)
|
||||
}
|
||||
|
||||
@objc
|
||||
func audioSessionInterrupted(notification: Notification) {
|
||||
ReactLogger.log(level: .error, message: "The Audio Session was interrupted!")
|
||||
guard let userInfo = notification.userInfo,
|
||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .began:
|
||||
// TODO: Should we also disable the camera here? I think it will throw a runtime error
|
||||
// disable audio session
|
||||
try? AVAudioSession.sharedInstance().setActive(false)
|
||||
break
|
||||
case .ended:
|
||||
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
||||
if options.contains(.shouldResume) {
|
||||
// restart audio session because interruption is over
|
||||
configureAudioSession()
|
||||
} else {
|
||||
ReactLogger.log(level: .error, message: "Cannot resume interrupted Audio Session!")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal final func setTorchMode(_ torchMode: String) {
|
||||
guard let device = videoDeviceInput?.device else {
|
||||
return invokeOnError(.session(.cameraNotReady))
|
||||
@ -302,269 +261,4 @@ final class CameraView: UIView {
|
||||
onInitialized([String: Any]())
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let captureSession = AVCaptureSession()
|
||||
|
||||
private final func setAutomaticallyConfiguresAudioSession(_ automaticallyConfiguresAudioSession: Bool) {
|
||||
if captureSession.automaticallyConfiguresApplicationAudioSession != automaticallyConfiguresAudioSession {
|
||||
captureSession.beginConfiguration()
|
||||
captureSession.automaticallyConfiguresApplicationAudioSession = automaticallyConfiguresAudioSession
|
||||
captureSession.commitConfiguration()
|
||||
}
|
||||
}
|
||||
|
||||
// pragma MARK: Session, Device and Format Configuration
|
||||
/**
|
||||
Configures the Audio session to allow background-music playback while recording.
|
||||
*/
|
||||
private final func configureAudioSession() {
|
||||
let start = DispatchTime.now()
|
||||
do {
|
||||
setAutomaticallyConfiguresAudioSession(false)
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
if audioSession.category != .playAndRecord {
|
||||
// allow background music playback
|
||||
try audioSession.setCategory(AVAudioSession.Category.playAndRecord, options: [.mixWithOthers, .allowBluetoothA2DP, .defaultToSpeaker])
|
||||
}
|
||||
// TODO: Use https://developer.apple.com/documentation/avfaudio/avaudiosession/3726094-setprefersnointerruptionsfromsys
|
||||
audioSession.trySetAllowHaptics(true)
|
||||
// activate current audio session because camera is active
|
||||
try audioSession.setActive(true)
|
||||
} catch let error as NSError {
|
||||
self.invokeOnError(.session(.audioSessionSetupFailed(reason: error.description)), cause: error)
|
||||
setAutomaticallyConfiguresAudioSession(true)
|
||||
}
|
||||
let end = DispatchTime.now()
|
||||
let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds
|
||||
ReactLogger.log(level: .info, message: "Configured Audio session in \(Double(nanoTime) / 1_000_000)ms!")
|
||||
}
|
||||
|
||||
/**
|
||||
Configures the Capture Session.
|
||||
*/
|
||||
private final func configureCaptureSession() {
|
||||
ReactLogger.logJS(level: .info, message: "Configuring Session...")
|
||||
isReady = false
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
return invokeOnError(.device(.notAvailableOnSimulator))
|
||||
#endif
|
||||
|
||||
guard cameraId != nil else {
|
||||
return invokeOnError(.device(.noDevice))
|
||||
}
|
||||
let cameraId = self.cameraId! as String
|
||||
|
||||
ReactLogger.log(level: .info, message: "Initializing Camera with device \(cameraId)...")
|
||||
captureSession.beginConfiguration()
|
||||
defer {
|
||||
captureSession.commitConfiguration()
|
||||
}
|
||||
|
||||
if let preset = self.preset {
|
||||
var sessionPreset: AVCaptureSession.Preset?
|
||||
do {
|
||||
sessionPreset = try AVCaptureSession.Preset(withString: preset)
|
||||
} catch let EnumParserError.unsupportedOS(supportedOnOS: os) {
|
||||
return invokeOnError(.parameter(.unsupportedOS(unionName: "Preset", receivedValue: preset, supportedOnOs: os)))
|
||||
} catch {
|
||||
return invokeOnError(.parameter(.invalid(unionName: "Preset", receivedValue: preset)))
|
||||
}
|
||||
if sessionPreset != nil {
|
||||
if captureSession.canSetSessionPreset(sessionPreset!) {
|
||||
captureSession.sessionPreset = sessionPreset!
|
||||
} else {
|
||||
// non-fatal error, so continue with configuration
|
||||
invokeOnError(.format(.invalidPreset(preset: preset)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// INPUTS
|
||||
// Video Input
|
||||
do {
|
||||
if let videoDeviceInput = self.videoDeviceInput {
|
||||
captureSession.removeInput(videoDeviceInput)
|
||||
}
|
||||
guard let videoDevice = AVCaptureDevice(uniqueID: cameraId) else {
|
||||
return invokeOnError(.device(.invalid))
|
||||
}
|
||||
zoom = NSNumber(value: Double(videoDevice.neutralZoomPercent))
|
||||
videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
||||
guard captureSession.canAddInput(videoDeviceInput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "video-input")))
|
||||
}
|
||||
captureSession.addInput(videoDeviceInput!)
|
||||
} catch {
|
||||
return invokeOnError(.device(.invalid))
|
||||
}
|
||||
|
||||
// Microphone (Audio Input)
|
||||
do {
|
||||
if let audioDeviceInput = self.audioDeviceInput {
|
||||
captureSession.removeInput(audioDeviceInput)
|
||||
}
|
||||
guard let audioDevice = AVCaptureDevice.default(for: .audio) else {
|
||||
return invokeOnError(.device(.microphoneUnavailable))
|
||||
}
|
||||
|
||||
audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
|
||||
guard captureSession.canAddInput(audioDeviceInput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedInput(inputDescriptor: "audio-input")))
|
||||
}
|
||||
captureSession.addInput(audioDeviceInput!)
|
||||
} catch {
|
||||
return invokeOnError(.device(.invalid))
|
||||
}
|
||||
|
||||
// OUTPUTS
|
||||
if let photoOutput = self.photoOutput {
|
||||
captureSession.removeOutput(photoOutput)
|
||||
}
|
||||
// Photo Output
|
||||
photoOutput = AVCapturePhotoOutput()
|
||||
photoOutput!.isDepthDataDeliveryEnabled = photoOutput!.isDepthDataDeliverySupported && enableDepthData
|
||||
if let enableHighResolutionCapture = self.enableHighResolutionCapture?.boolValue {
|
||||
photoOutput!.isHighResolutionCaptureEnabled = enableHighResolutionCapture
|
||||
}
|
||||
if #available(iOS 12.0, *) {
|
||||
photoOutput!.isPortraitEffectsMatteDeliveryEnabled = photoOutput!.isPortraitEffectsMatteDeliverySupported && self.enablePortraitEffectsMatteDelivery
|
||||
}
|
||||
guard captureSession.canAddOutput(photoOutput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "photo-output")))
|
||||
}
|
||||
captureSession.addOutput(photoOutput!)
|
||||
if videoDeviceInput!.device.position == .front {
|
||||
photoOutput!.mirror()
|
||||
}
|
||||
|
||||
// Video Output
|
||||
if let movieOutput = self.movieOutput {
|
||||
captureSession.removeOutput(movieOutput)
|
||||
}
|
||||
movieOutput = AVCaptureMovieFileOutput()
|
||||
guard captureSession.canAddOutput(movieOutput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "movie-output")))
|
||||
}
|
||||
captureSession.addOutput(movieOutput!)
|
||||
if videoDeviceInput!.device.position == .front {
|
||||
movieOutput!.mirror()
|
||||
}
|
||||
|
||||
// Barcode Scanning
|
||||
if let metadataOutput = self.metadataOutput {
|
||||
captureSession.removeOutput(metadataOutput)
|
||||
}
|
||||
if let scannableCodes = self.scannableCodes {
|
||||
// scannableCodes prop is not nil, so enable barcode scanning.
|
||||
guard onCodeScanned != nil else {
|
||||
return invokeOnError(.parameter(.invalidCombination(provided: "scannableCodes", missing: "onCodeScanned")))
|
||||
}
|
||||
metadataOutput = AVCaptureMetadataOutput()
|
||||
guard captureSession.canAddOutput(metadataOutput!) else {
|
||||
return invokeOnError(.parameter(.unsupportedOutput(outputDescriptor: "metadata-output")))
|
||||
}
|
||||
captureSession.addOutput(metadataOutput!)
|
||||
metadataOutput!.setMetadataObjectsDelegate(self, queue: queue)
|
||||
var objectTypes: [AVMetadataObject.ObjectType] = []
|
||||
scannableCodes.forEach { code in
|
||||
do {
|
||||
objectTypes.append(try AVMetadataObject.ObjectType(withString: code))
|
||||
} catch let EnumParserError.unsupportedOS(supportedOnOS: os) {
|
||||
invokeOnError(.parameter(.unsupportedOS(unionName: "CodeType", receivedValue: code, supportedOnOs: os)))
|
||||
} catch {
|
||||
invokeOnError(.parameter(.invalid(unionName: "CodeType", receivedValue: code)))
|
||||
}
|
||||
}
|
||||
metadataOutput!.metadataObjectTypes = objectTypes
|
||||
}
|
||||
|
||||
invokeOnInitialized()
|
||||
isReady = true
|
||||
ReactLogger.logJS(level: .info, message: "Session successfully configured!")
|
||||
}
|
||||
|
||||
/**
|
||||
Configures the Video Device to find the best matching Format.
|
||||
*/
|
||||
private final func configureFormat() {
|
||||
ReactLogger.logJS(level: .info, message: "Configuring Format...")
|
||||
guard let filter = self.format else {
|
||||
// Format Filter was null. Ignore it.
|
||||
return
|
||||
}
|
||||
guard let device = videoDeviceInput?.device else {
|
||||
return invokeOnError(.session(.cameraNotReady))
|
||||
}
|
||||
|
||||
if device.activeFormat.matchesFilter(filter) {
|
||||
ReactLogger.log(level: .info, message: "Active format already matches filter.")
|
||||
return
|
||||
}
|
||||
|
||||
// get matching format
|
||||
let matchingFormats = device.formats.filter { $0.matchesFilter(filter) }.sorted { $0.isBetterThan($1) }
|
||||
guard let format = matchingFormats.first else {
|
||||
return invokeOnError(.format(.invalidFormat))
|
||||
}
|
||||
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
device.activeFormat = format
|
||||
device.unlockForConfiguration()
|
||||
ReactLogger.logJS(level: .info, message: "Format successfully configured!")
|
||||
} catch let error as NSError {
|
||||
return invokeOnError(.device(.configureError), cause: error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Configures the Video Device with the given FPS, HDR and ColorSpace.
|
||||
*/
|
||||
private final func configureDevice() {
|
||||
ReactLogger.logJS(level: .info, message: "Configuring Device...")
|
||||
guard let device = videoDeviceInput?.device else {
|
||||
return invokeOnError(.session(.cameraNotReady))
|
||||
}
|
||||
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
|
||||
if let fps = self.fps?.int32Value {
|
||||
let duration = CMTimeMake(value: 1, timescale: fps)
|
||||
device.activeVideoMinFrameDuration = duration
|
||||
device.activeVideoMaxFrameDuration = duration
|
||||
} else {
|
||||
device.activeVideoMinFrameDuration = CMTime.invalid
|
||||
device.activeVideoMaxFrameDuration = CMTime.invalid
|
||||
}
|
||||
if hdr != nil {
|
||||
if hdr == true && !device.activeFormat.isVideoHDRSupported {
|
||||
return invokeOnError(.format(.invalidHdr))
|
||||
}
|
||||
if !device.automaticallyAdjustsVideoHDREnabled {
|
||||
if device.isVideoHDREnabled != hdr!.boolValue {
|
||||
device.isVideoHDREnabled = hdr!.boolValue
|
||||
}
|
||||
}
|
||||
}
|
||||
if lowLightBoost != nil {
|
||||
if lowLightBoost == true && !device.isLowLightBoostSupported {
|
||||
return invokeOnError(.device(.lowLightBoostNotSupported))
|
||||
}
|
||||
if device.automaticallyEnablesLowLightBoostWhenAvailable != lowLightBoost!.boolValue {
|
||||
device.automaticallyEnablesLowLightBoostWhenAvailable = lowLightBoost!.boolValue
|
||||
}
|
||||
}
|
||||
if colorSpace != nil, let avColorSpace = try? AVCaptureColorSpace(string: String(colorSpace!)) {
|
||||
device.activeColorSpace = avColorSpace
|
||||
}
|
||||
|
||||
device.unlockForConfiguration()
|
||||
ReactLogger.logJS(level: .info, message: "Device successfully configured!")
|
||||
} catch let error as NSError {
|
||||
return invokeOnError(.device(.configureError), cause: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ extension AVAudioSession {
|
||||
*/
|
||||
func trySetAllowHaptics(_ allowHaptics: Bool) {
|
||||
if #available(iOS 13.0, *) {
|
||||
if !audioSession.allowHapticsAndSystemSoundsDuringRecording {
|
||||
try? audioSession.setAllowHapticsAndSystemSoundsDuringRecording(true)
|
||||
if !self.allowHapticsAndSystemSoundsDuringRecording {
|
||||
try? self.setAllowHapticsAndSystemSoundsDuringRecording(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
|
||||
defer {
|
||||
delegatesReferences.removeAll(where: { $0 == self })
|
||||
}
|
||||
if let error = error {
|
||||
return promise.reject(error: .capture(.unknown(message: error.description)), cause: error as NSError)
|
||||
if let error = error as NSError? {
|
||||
return promise.reject(error: .capture(.unknown(message: error.description)), cause: error)
|
||||
}
|
||||
|
||||
let error = ErrorPointer(nilLiteral: ())
|
||||
@ -66,8 +66,8 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
|
||||
defer {
|
||||
delegatesReferences.removeAll(where: { $0 == self })
|
||||
}
|
||||
if let error = error {
|
||||
return promise.reject(error: .capture(.unknown(message: error.description)), cause: error as NSError)
|
||||
if let error = error as NSError? {
|
||||
return promise.reject(error: .capture(.unknown(message: error.description)), cause: error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,8 +33,8 @@ class RecordingDelegateWithCallback: NSObject, AVCaptureFileOutputRecordingDeleg
|
||||
self.resetTorchMode()
|
||||
delegateReferences.removeAll(where: { $0 == self })
|
||||
}
|
||||
if let error = error {
|
||||
return callback([NSNull(), makeReactError(.capture(.unknown(message: error.description)), cause: error as NSError)])
|
||||
if let error = error as NSError? {
|
||||
return callback([NSNull(), makeReactError(.capture(.unknown(message: error.description)), cause: error)])
|
||||
}
|
||||
|
||||
let seconds = CMTimeGetSeconds(output.recordedDuration)
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
B86DC971260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */; };
|
||||
B86DC974260E310600FB17B2 /* CameraView+AVAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */; };
|
||||
B86DC977260E315100FB17B2 /* CameraView+AVCaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */; };
|
||||
B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */; };
|
||||
B887518625E0102000DB86D6 /* CameraView+RecordVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */; };
|
||||
B887518725E0102000DB86D6 /* CameraViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B887515F25E0102000DB86D6 /* CameraViewManager.m */; };
|
||||
@ -62,6 +64,8 @@
|
||||
/* Begin PBXFileReference section */
|
||||
134814201AA4EA6300B7C361 /* libVisionCamera.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVisionCamera.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioSession+trySetAllowHaptics.swift"; sourceTree = "<group>"; };
|
||||
B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+AVAudioSession.swift"; sourceTree = "<group>"; };
|
||||
B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+AVCaptureSession.swift"; sourceTree = "<group>"; };
|
||||
B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDelegate.swift; sourceTree = "<group>"; };
|
||||
B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraView+RecordVideo.swift"; sourceTree = "<group>"; };
|
||||
B887515E25E0102000DB86D6 /* CameraBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CameraBridge.h; sourceTree = "<group>"; };
|
||||
@ -127,6 +131,8 @@
|
||||
B887515E25E0102000DB86D6 /* CameraBridge.h */,
|
||||
B887518325E0102000DB86D6 /* CameraError.swift */,
|
||||
B887518425E0102000DB86D6 /* CameraView.swift */,
|
||||
B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */,
|
||||
B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */,
|
||||
B887516C25E0102000DB86D6 /* CameraView+CodeScanning.swift */,
|
||||
B887518025E0102000DB86D6 /* CameraView+Focus.swift */,
|
||||
B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */,
|
||||
@ -289,6 +295,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B86DC974260E310600FB17B2 /* CameraView+AVAudioSession.swift in Sources */,
|
||||
B887518625E0102000DB86D6 /* CameraView+RecordVideo.swift in Sources */,
|
||||
B88751A225E0102000DB86D6 /* AVCaptureColorSpace+descriptor.swift in Sources */,
|
||||
B887518925E0102000DB86D6 /* Collection+safe.swift in Sources */,
|
||||
@ -310,6 +317,7 @@
|
||||
B887518B25E0102000DB86D6 /* AVCaptureDevice.Format+isBetterThan.swift in Sources */,
|
||||
B887518E25E0102000DB86D6 /* AVFrameRateRange+includes.swift in Sources */,
|
||||
B88751A125E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift in Sources */,
|
||||
B86DC977260E315100FB17B2 /* CameraView+AVCaptureSession.swift in Sources */,
|
||||
B887518A25E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift in Sources */,
|
||||
B88751A325E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */,
|
||||
B887519A25E0102000DB86D6 /* AVVideoCodecType+descriptor.swift in Sources */,
|
||||
|
Loading…
Reference in New Issue
Block a user