feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,307 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import AVFoundation
import ExpoModulesCore
let cameraLegacyEvents = ["onCameraReady", "onMountError", "onPictureSaved", "onBarCodeScanned", "onFacesDetected", "onResponsiveOrientationChanged"]
public final class CameraViewLegacyModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoCameraLegacy")
OnCreate {
let permissionsManager = self.appContext?.permissions
EXPermissionsMethodsDelegate.register(
[
CameraPermissionRequester(),
CameraOnlyPermissionRequester(),
CameraMicrophonePermissionRequester()
],
withPermissionsManager: permissionsManager
)
}
Constants([
"Type": [
"front": CameraTypeLegacy.front.rawValue,
"back": CameraTypeLegacy.back.rawValue
],
"FlashMode": [
"off": FlashModeLegacy.off.rawValue,
"on": FlashModeLegacy.on.rawValue,
"auto": FlashModeLegacy.auto.rawValue,
"torch": FlashModeLegacy.torch.rawValue
],
"AutoFocus": [
"on": AutoFocus.on.rawValue,
"off": AutoFocus.off.rawValue
],
"WhiteBalance": [
"auto": WhiteBalance.auto.rawValue,
"sunny": WhiteBalance.sunny.rawValue,
"cloudy": WhiteBalance.cloudy.rawValue,
"shadow": WhiteBalance.shadow.rawValue,
"incandescent": WhiteBalance.incandescent.rawValue,
"fluorescent": WhiteBalance.fluorescent.rawValue
],
"VideoQuality": [
"2160p": VideoQuality.video2160p.rawValue,
"1080p": VideoQuality.video1080p.rawValue,
"720p": VideoQuality.video720p.rawValue,
"480p": VideoQuality.video4x3.rawValue,
"4:3": VideoQuality.video4x3.rawValue
],
"VideoStabilization": [
"off": VideoStabilizationMode.off.rawValue,
"standard": VideoStabilizationMode.standard.rawValue,
"cinematic": VideoStabilizationMode.cinematic.rawValue,
"auto": VideoStabilizationMode.auto.rawValue
],
"VideoCodec": [
"H264": VideoCodecLegacy.h264.rawValue,
"HEVC": VideoCodecLegacy.hevc.rawValue,
"JPEG": VideoCodecLegacy.jpeg.rawValue,
"AppleProRes422": VideoCodecLegacy.appleProRes422.rawValue,
"AppleProRes4444": VideoCodecLegacy.appleProRes4444.rawValue
]
])
// swiftlint:disable:next closure_body_length
View(CameraViewLegacy.self) {
Events(cameraLegacyEvents)
Prop("type") { (view, type: CameraTypeLegacy) in
if view.presetCamera.rawValue != type.rawValue {
view.presetCamera = type.toPosition()
}
}
Prop("flashMode") { (view, flashMode: FlashModeLegacy) in
if view.flashMode.rawValue != flashMode.rawValue {
view.flashMode = flashMode
}
}
Prop("faceDetectorSettings") { (view, settings: [String: Any]) in
view.updateFaceDetectorSettings(settings: settings)
}
Prop("barCodeScannerSettings") { (view, settings: [String: Any]) in
view.setBarCodeScannerSettings(settings: settings)
}
Prop("autoFocus") { (view, autoFocus: AutoFocus) in
if view.autoFocus.rawValue != autoFocus.rawValue {
view.autoFocus = autoFocus.toAvAutoFocus()
}
}
Prop("focusDepth") { (view, focusDepth: Float) in
if fabsf(view.focusDepth - focusDepth) > Float.ulpOfOne {
view.focusDepth = focusDepth
}
}
Prop("zoom") { (view, zoom: Double) in
if fabs(view.zoom - zoom) > Double.ulpOfOne {
view.zoom = zoom
}
}
Prop("whiteBalance") { (view, whiteBalance: WhiteBalance) in
if view.whiteBalance.rawValue != whiteBalance.rawValue {
view.whiteBalance = whiteBalance
}
}
Prop("pictureSize") { (view, pictureSize: String) in
if let size = pictureSizesDict[pictureSize] {
view.pictureSize = size
}
}
Prop("faceDetectorEnabled") { (view, detectFaces: Bool?) in
if view.isDetectingFaces != detectFaces {
view.isDetectingFaces = detectFaces ?? false
}
}
Prop("barCodeScannerEnabled") { (view, scanBarCodes: Bool?) in
if view.isScanningBarCodes != scanBarCodes {
view.isScanningBarCodes = scanBarCodes ?? false
}
}
Prop("responsiveOrientationWhenOrientationLocked") { (view, responsiveOrientation: Bool) in
if view.responsiveWhenOrientationLocked != responsiveOrientation {
view.responsiveWhenOrientationLocked = responsiveOrientation
}
}
}
AsyncFunction("takePicture") { (options: TakePictureOptions, viewTag: Int, promise: Promise) in
guard let view = self.appContext?.findView(withTag: viewTag, ofType: CameraViewLegacy.self) else {
throw Exceptions.ViewNotFound((tag: viewTag, type: CameraView.self))
}
#if targetEnvironment(simulator)
try takePictureForSimulator(self.appContext, view, options, promise)
#else // simulator
view.takePicture(options: options, promise: promise)
#endif // not simulator
}
.runOnQueue(.main)
AsyncFunction("record") { (options: CameraRecordingOptionsLegacy, viewTag: Int, promise: Promise) in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
guard let view = self.appContext?.findView(withTag: viewTag, ofType: CameraViewLegacy.self) else {
throw Exceptions.ViewNotFound((tag: viewTag, type: CameraViewLegacy.self))
}
view.record(options: options, promise: promise)
#endif
}
.runOnQueue(.main)
AsyncFunction("stopRecording") { (viewTag: Int) in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
guard let view = self.appContext?.findView(withTag: viewTag, ofType: CameraViewLegacy.self) else {
throw Exceptions.ViewNotFound((tag: viewTag, type: CameraViewLegacy.self))
}
view.stopRecording()
#endif
}
.runOnQueue(.main)
AsyncFunction("resumePreview") { (viewTag: Int) in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
guard let view = self.appContext?.findView(withTag: viewTag, ofType: CameraViewLegacy.self) else {
throw Exceptions.ViewNotFound((tag: viewTag, type: CameraViewLegacy.self))
}
view.resumePreview()
#endif
}
.runOnQueue(.main)
AsyncFunction("pausePreview") { (viewTag: Int) in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
guard let view = self.appContext?.findView(withTag: viewTag, ofType: CameraViewLegacy.self) else {
throw Exceptions.ViewNotFound((tag: viewTag, type: CameraViewLegacy.self))
}
view.pausePreview()
#endif
}
.runOnQueue(.main)
AsyncFunction("getAvailablePictureSizes") { (_: String?, _: Int) in
// Argument types must be compatible with Android which receives the ratio and view tag.
return pictureSizesDict.map { k, _ in
k
}
}
AsyncFunction("getAvailableVideoCodecsAsync") { () -> [String] in
return getAvailableVideoCodecs()
}
AsyncFunction("getPermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
self.appContext?.permissions,
withRequester: CameraPermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("requestPermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.askForPermission(
withPermissionsManager: self.appContext?.permissions,
withRequester: CameraPermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("getCameraPermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
self.appContext?.permissions,
withRequester: CameraOnlyPermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("requestCameraPermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.askForPermission(
withPermissionsManager: self.appContext?.permissions,
withRequester: CameraOnlyPermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("getMicrophonePermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
self.appContext?.permissions,
withRequester: CameraMicrophonePermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("requestMicrophonePermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.askForPermission(
withPermissionsManager: self.appContext?.permissions,
withRequester: CameraMicrophonePermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
}
}
private func getAvailableVideoCodecs() -> [String] {
let session = AVCaptureSession()
session.beginConfiguration()
guard let captureDevice = ExpoCameraUtils.device(
with: AVMediaType.video,
preferring: AVCaptureDevice.Position.front) else {
return []
}
guard let deviceInput = try? AVCaptureDeviceInput(device: captureDevice) else {
return []
}
if session.canAddInput(deviceInput) {
session.addInput(deviceInput)
}
session.commitConfiguration()
let movieFileOutput = AVCaptureMovieFileOutput()
if session.canAddOutput(movieFileOutput) {
session.addOutput(movieFileOutput)
}
return movieFileOutput.availableVideoCodecTypes.map { $0.rawValue }
}
private let pictureSizesDict = [
"3840x2160": AVCaptureSession.Preset.hd4K3840x2160,
"1920x1080": AVCaptureSession.Preset.hd1920x1080,
"1280x720": AVCaptureSession.Preset.hd1280x720,
"640x480": AVCaptureSession.Preset.vga640x480,
"352x288": AVCaptureSession.Preset.cif352x288,
"Photo": AVCaptureSession.Preset.photo,
"High": AVCaptureSession.Preset.high,
"Medium": AVCaptureSession.Preset.medium,
"Low": AVCaptureSession.Preset.low
]

View File

@@ -0,0 +1,324 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import AVFoundation
import ExpoModulesCore
import VisionKit
let cameraEvents = ["onCameraReady", "onMountError", "onPictureSaved", "onBarcodeScanned", "onResponsiveOrientationChanged"]
struct ScannerContext {
var controller: Any?
var delegate: Any?
}
public final class CameraViewModule: Module, ScannerResultHandler {
private var scannerContext: ScannerContext?
public func definition() -> ModuleDefinition {
Name("ExpoCamera")
Events("onModernBarcodeScanned")
OnCreate {
let permissionsManager = self.appContext?.permissions
EXPermissionsMethodsDelegate.register(
[
CameraOnlyPermissionRequester(),
CameraMicrophonePermissionRequester()
],
withPermissionsManager: permissionsManager
)
}
Property("isModernBarcodeScannerAvailable") { () -> Bool in
if #available(iOS 16.0, *) {
return true
}
return false
}
AsyncFunction("scanFromURLAsync") { (url: URL, _: [BarcodeType], promise: Promise) in
guard let imageLoader = appContext?.imageLoader else {
throw ImageLoaderNotFound()
}
imageLoader.loadImage(for: url) { error, image in
if error != nil {
promise.reject(FailedToLoadImage())
return
}
guard let cgImage = image?.cgImage else {
promise.reject(FailedToLoadImage())
return
}
guard let detector = CIDetector(
ofType: CIDetectorTypeQRCode,
context: nil,
options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]
) else {
promise.reject(InitScannerFailed())
return
}
let ciImage = CIImage(cgImage: cgImage)
let features = detector.features(in: ciImage)
promise.resolve(BarcodeUtils.getResultFrom(features))
}
}
// swiftlint:disable:next closure_body_length
View(CameraView.self) {
Events(cameraEvents)
Prop("facing") { (view, type: CameraType?) in
if let type, view.presetCamera != type.toPosition() {
view.presetCamera = type.toPosition()
}
}
Prop("flashMode") { (view, flashMode: FlashMode?) in
if let flashMode, view.flashMode != flashMode {
view.flashMode = flashMode
}
}
Prop("enableTorch") { (view, enabled: Bool?) in
view.torchEnabled = enabled ?? false
}
Prop("pictureSize") { (view, pictureSize: PictureSize?) in
if let pictureSize, view.pictureSize != pictureSize {
view.pictureSize = pictureSize
}
}
Prop("zoom") { (view, zoom: Double?) in
if let zoom, fabs(view.zoom - zoom) > Double.ulpOfOne {
view.zoom = zoom
}
}
Prop("mode") { (view, mode: CameraMode?) in
if let mode, view.mode != mode {
view.mode = mode
}
}
Prop("barcodeScannerEnabled") { (view, scanBarcodes: Bool?) in
if let scanBarcodes, view.isScanningBarcodes != scanBarcodes {
view.isScanningBarcodes = scanBarcodes
}
}
Prop("barcodeScannerSettings") { (view, settings: BarcodeSettings?) in
if let settings {
view.setBarcodeScannerSettings(settings: settings)
}
}
Prop("mute") { (view, muted: Bool?) in
view.isMuted = muted ?? false
}
Prop("animateShutter") { (view, animate: Bool?) in
view.animateShutter = animate ?? true
}
Prop("videoQuality") { (view, quality: VideoQuality?) in
if let quality, view.videoQuality != quality {
view.videoQuality = quality
}
}
Prop("autoFocus") { (view, focusMode: FocusMode?) in
view.autoFocus = focusMode?.toAVCaptureFocusMode() ?? .continuousAutoFocus
}
Prop("responsiveOrientationWhenOrientationLocked") { (view, responsiveOrientation: Bool?) in
if let responsiveOrientation, view.responsiveWhenOrientationLocked != responsiveOrientation {
view.responsiveWhenOrientationLocked = responsiveOrientation
}
}
Prop("mirror") { (view, mirror: Bool?) in
if let mirror {
view.mirror = mirror
return
}
view.mirror = false
}
Prop("active") { (view, active: Bool?) in
if let active {
view.active = active
return
}
view.active = true
}
OnViewDidUpdateProps { view in
view.initCamera()
}
AsyncFunction("resumePreview") { view in
view.resumePreview()
}
AsyncFunction("pausePreview") { view in
view.pausePreview()
}
AsyncFunction("getAvailablePictureSizes") { (_: String?) in
return PictureSize.allCases.map {
$0.rawValue
}
}
AsyncFunction("takePicture") { (view, options: TakePictureOptions, promise: Promise) in
#if targetEnvironment(simulator) // simulator
try takePictureForSimulator(self.appContext, view, options, promise)
#else // not simulator
view.takePicture(options: options, promise: promise)
#endif
}.runOnQueue(.main)
AsyncFunction("record") { (view, options: CameraRecordingOptions, promise: Promise) in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
view.record(options: options, promise: promise)
#endif
}.runOnQueue(.main)
AsyncFunction("stopRecording") { view in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
view.stopRecording()
#endif
}.runOnQueue(.main)
}
AsyncFunction("launchScanner") { (options: VisionScannerOptions?) in
if #available(iOS 16.0, *) {
await MainActor.run {
let delegate = VisionScannerDelegate(handler: self)
scannerContext = ScannerContext(delegate: delegate)
launchScanner(with: options)
}
}
}
AsyncFunction("dismissScanner") {
if #available(iOS 16.0, *) {
await MainActor.run {
dismissScanner()
}
}
}
AsyncFunction("getCameraPermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
self.appContext?.permissions,
withRequester: CameraOnlyPermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("requestCameraPermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.askForPermission(
withPermissionsManager: self.appContext?.permissions,
withRequester: CameraOnlyPermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("getMicrophonePermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.getPermissionWithPermissionsManager(
self.appContext?.permissions,
withRequester: CameraMicrophonePermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("requestMicrophonePermissionsAsync") { (promise: Promise) in
EXPermissionsMethodsDelegate.askForPermission(
withPermissionsManager: self.appContext?.permissions,
withRequester: CameraMicrophonePermissionRequester.self,
resolve: promise.resolver,
reject: promise.legacyRejecter
)
}
AsyncFunction("getAvailableVideoCodecsAsync") { () -> [String] in
return getAvailableVideoCodecs()
}
}
@available(iOS 16.0, *)
@MainActor
private func launchScanner(with options: VisionScannerOptions?) {
let symbologies = options?.toSymbology() ?? [.qr]
let controller = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: symbologies)],
isPinchToZoomEnabled: options?.isPinchToZoomEnabled ?? true,
isGuidanceEnabled: options?.isGuidanceEnabled ?? true,
isHighlightingEnabled: options?.isHighlightingEnabled ?? false
)
scannerContext?.controller = controller
if let delegate = scannerContext?.delegate as? VisionScannerDelegate {
controller.delegate = delegate
}
appContext?.utilities?.currentViewController()?.present(controller, animated: true) {
try? controller.startScanning()
}
}
@available(iOS 16.0, *)
@MainActor
private func dismissScanner() {
guard let controller = scannerContext?.controller as? DataScannerViewController else {
return
}
controller.stopScanning()
controller.dismiss(animated: true)
}
func onItemScanned(result: [String: Any]) {
sendEvent("onModernBarcodeScanned", result)
}
private func getAvailableVideoCodecs() -> [String] {
let session = AVCaptureSession()
session.beginConfiguration()
guard let captureDevice = ExpoCameraUtils.device(
with: AVMediaType.video,
preferring: AVCaptureDevice.Position.front) else {
return []
}
guard let deviceInput = try? AVCaptureDeviceInput(device: captureDevice) else {
return []
}
if session.canAddInput(deviceInput) {
session.addInput(deviceInput)
}
session.commitConfiguration()
let movieFileOutput = AVCaptureMovieFileOutput()
if session.canAddOutput(movieFileOutput) {
session.addOutput(movieFileOutput)
}
return movieFileOutput.availableVideoCodecTypes.map { $0.rawValue }
}
}

View File

@@ -0,0 +1,19 @@
import ExpoModulesCore
internal class ImageLoaderNotFound: Exception {
override var reason: String {
"Image Loader module not found"
}
}
internal class FailedToLoadImage: Exception {
override var reason: String {
"Could not get the image"
}
}
internal class InitScannerFailed: Exception {
override var reason: String {
"Could not initialize the barcode scanner"
}
}

View File

@@ -0,0 +1,70 @@
import AVFoundation
struct BarcodeUtils {
static func getResultFrom(_ features: [CIFeature]) -> [[AnyHashable: Any]?] {
var result = [[AnyHashable: Any]?]()
for feature in features {
if let qrCodeFeature = feature as? CIQRCodeFeature {
let item = ciQRCodeFeature(
codeFeature: qrCodeFeature
)
result.append(item)
}
}
return result
}
static func ciQRCodeFeature(codeFeature: CIQRCodeFeature) -> [String: Any] {
var result: [String: Any] = [:]
result["type"] = "qr"
result["data"] = codeFeature.messageString
if !codeFeature.bounds.isEmpty {
result["cornerPoints"] = [
codeFeature.topLeft,
codeFeature.topRight,
codeFeature.bottomRight,
codeFeature.bottomLeft
].map { point in
[
"x": point.x,
"y": point.y
]
}
let origin = codeFeature.bounds.origin
let size = codeFeature.bounds.size
result["bounds"] = [
"origin": [
"x": origin.x,
"y": origin.y
],
"size": [
"width": size.width,
"height": size.height
]
]
} else {
addEmptyCornerPoints(to: &result)
}
return result
}
static func addEmptyCornerPoints(to result: inout [String: Any]) {
result["cornerPoints"] = []
result["bounds"] = [
"origin": [
"x": 0,
"y": 0
],
"size": [
"width": 0,
"height": 0
]
]
}
}

View File

@@ -0,0 +1,55 @@
import ExpoModulesCore
internal class CameraUnmountedException: Exception {
override var reason: String {
"Camera unmounted during taking photo process"
}
}
internal class CameraNotReadyException: Exception {
override var reason: String {
"Camera unmounted during taking photo process"
}
}
internal class CameraOutputNotReadyException: Exception {
override var reason: String {
"Camera is not ready yet. Wait for 'onCameraReady' callback"
}
}
internal class CameraImageCaptureException: Exception {
override var reason: String {
"Image could not be captured"
}
}
internal class CameraSavingImageException: Exception {
override var reason: String {
"Could not save the image"
}
}
internal class CameraRecordingException: GenericException<String?> {
override var reason: String {
"Video Codec '\(param)' is not supported on this device"
}
}
internal class CameraRecordingFailedException: Exception {
override var reason: String {
"An error occurred while recording a video"
}
}
internal class CameraMetadataDecodingException: Exception {
override var reason: String {
"Could not decode image metadata"
}
}
internal class CameraInvalidPhotoData: Exception {
override var reason: String {
"An error occured while generating photo data"
}
}

View File

@@ -0,0 +1,125 @@
import ExpoModulesCore
import AVFoundation
let cameraKey = "NSCameraUsageDescription"
let microphoneKey = "NSMicrophoneUsageDescription"
protocol BaseCameraRequester {
var mediaType: AVMediaType { get }
func permissionWith(status systemStatus: AVAuthorizationStatus) -> [AnyHashable: Any]
func permissions(for key: String, service: String) -> [AnyHashable: Any]
func requestAccess(handler: @escaping (Bool) -> Void)
}
extension BaseCameraRequester {
func permissions(for key: String, service: String) -> [AnyHashable: Any] {
var systemStatus: AVAuthorizationStatus
let description = Bundle.main.infoDictionary?[key] as? String
if let description {
systemStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
} else {
EXFatal(EXErrorWithMessage("""
This app is missing \(key),
so \(service) services will fail. Add this entry to your bundle's Info.plist.
"""))
systemStatus = .denied
}
return permissionWith(status: systemStatus)
}
func permissionWith(status systemStatus: AVAuthorizationStatus) -> [AnyHashable: Any] {
var status: EXPermissionStatus
switch systemStatus {
case .authorized:
status = EXPermissionStatusGranted
case .denied, .restricted:
status = EXPermissionStatusDenied
case .notDetermined:
fallthrough
@unknown default:
status = EXPermissionStatusUndetermined
}
return [
"status": status.rawValue
]
}
func requestAccess(handler: @escaping (Bool) -> Void) {
AVCaptureDevice.requestAccess(for: mediaType, completionHandler: handler)
}
}
class CameraOnlyPermissionRequester: NSObject, EXPermissionsRequester, BaseCameraRequester {
let mediaType: AVMediaType = .video
static func permissionType() -> String {
"camera"
}
func getPermissions() -> [AnyHashable: Any] {
return permissions(for: cameraKey, service: "video")
}
func requestPermissions(resolver resolve: @escaping EXPromiseResolveBlock, rejecter reject: EXPromiseRejectBlock) {
requestAccess { [weak self] _ in
resolve(self?.getPermissions())
}
}
}
class CameraPermissionRequester: NSObject, EXPermissionsRequester, BaseCameraRequester {
let mediaType: AVMediaType = .video
static func permissionType() -> String {
"camera"
}
func getPermissions() -> [AnyHashable: Any] {
var systemStatus: AVAuthorizationStatus
var status: EXPermissionStatus
let cameraUsuageDescription = Bundle.main.infoDictionary?[cameraKey] as? String
let microphoneUsuageDescription = Bundle.main.infoDictionary?[microphoneKey] as? String
if let cameraUsuageDescription, let microphoneUsuageDescription {
systemStatus = AVCaptureDevice.authorizationStatus(for: mediaType)
} else {
EXFatal(EXErrorWithMessage("""
This app is missing either NSCameraUsageDescription or NSMicrophoneUsageDescription,
so audio/video services will fail. Add one of these entries to
your bundle's Info.plist
"""))
systemStatus = .denied
}
return permissionWith(status: systemStatus)
}
func requestPermissions(resolver resolve: @escaping EXPromiseResolveBlock, rejecter reject: EXPromiseRejectBlock) {
requestAccess { [weak self] _ in
resolve(self?.getPermissions())
}
}
}
class CameraMicrophonePermissionRequester: NSObject, EXPermissionsRequester, BaseCameraRequester {
let mediaType: AVMediaType = .audio
static func permissionType() -> String {
"microphone"
}
func getPermissions() -> [AnyHashable: Any] {
return permissions(for: microphoneKey, service: "audio")
}
func requestPermissions(resolver resolve: @escaping EXPromiseResolveBlock, rejecter reject: EXPromiseRejectBlock) {
requestAccess { [weak self] _ in
resolve(self?.getPermissions())
}
}
}

View File

@@ -0,0 +1,227 @@
import AVFoundation
import CoreMotion
struct ExpoCameraUtils {
static func device(with mediaType: AVMediaType, preferring position: AVCaptureDevice.Position) -> AVCaptureDevice? {
return AVCaptureDevice.default(.builtInWideAngleCamera, for: mediaType, position: position)
}
static func deviceOrientation(
for accelerometerData: CMAccelerometerData,
default orientation: UIDeviceOrientation
) -> UIDeviceOrientation {
if accelerometerData.acceleration.x >= 0.75 {
return .landscapeRight
}
if accelerometerData.acceleration.x <= -0.75 {
return .landscapeLeft
}
if accelerometerData.acceleration.y <= -0.75 {
return .portrait
}
if accelerometerData.acceleration.y >= 0.75 {
return .portraitUpsideDown
}
return orientation
}
// .landscapeRight and .landscapeLeft of UIInterfaceOrientation are reversed when mapped to UIDeviceOrientation
static func physicalOrientation(
for orientation: UIInterfaceOrientation
) -> UIDeviceOrientation {
switch orientation {
case .portrait:
return .portrait
case .landscapeLeft:
return .landscapeRight
case .landscapeRight:
return .landscapeLeft
case .portraitUpsideDown:
return .portraitUpsideDown
case .unknown:
return .unknown
default:
return .unknown
}
}
static func videoOrientation(for interfaceOrientation: UIInterfaceOrientation) -> AVCaptureVideoOrientation {
switch interfaceOrientation {
case .portrait:
return .portrait
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
case .portraitUpsideDown:
return .portraitUpsideDown
default:
return .portrait
}
}
// .landscapeRight and .landscapeLeft need to be reversed when mapped back to AVCaptureVideoOrientation
static func videoOrientation(for deviceOrientation: UIDeviceOrientation) -> AVCaptureVideoOrientation {
switch deviceOrientation {
case .portrait:
return .portrait
case .portraitUpsideDown:
return .portraitUpsideDown
case .landscapeLeft:
return .landscapeRight
case .landscapeRight:
return .landscapeLeft
default:
return .portrait
}
}
static func toOrientationString(orientation: UIDeviceOrientation) -> String {
switch orientation {
case .portrait:
return "portrait"
case .landscapeLeft:
return "landscapeLeft"
case .landscapeRight:
return "landscapeRight"
case .portraitUpsideDown:
return "portraitUpsideDown"
case .faceDown:
return "faceDown"
case .faceUp:
return "faceUp"
case .unknown:
return "unknown"
@unknown default:
return "unknown"
}
}
static func toExifOrientation(orientation: UIImage.Orientation) -> Int {
switch orientation {
case .up:
return 1
case .down:
return 3
case .left:
return 8
case .right:
return 6
case .upMirrored:
return 2
case .downMirrored:
return 4
case .leftMirrored:
return 5
case .rightMirrored:
return 7
@unknown default:
return 1
}
}
static func exportImage(orientation: UIImage.Orientation) -> Int {
switch orientation {
case .left:
return 90
case .right:
return -90
case .down:
return 180
default:
return 0
}
}
static func generatePhoto(of size: CGSize) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { ctx in
UIColor.black.setFill()
ctx.fill(rect)
let currentDate = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd.MM.YY HH:mm:ss"
let text = dateFormatter.string(from: currentDate)
text.draw(
with: CGRect(
x: size.width * 0.1,
y: size.height * 0.9,
width: size.width,
height: size.height
),
attributes: [.font: UIFont.systemFont(ofSize: 18), .foregroundColor: UIColor.orange],
context: nil
)
}
}
static func crop(image: UIImage, to rect: CGRect) -> UIImage {
let cgImage = image.cgImage
guard let croppedCgImage = cgImage?.cropping(to: rect) else {
return image
}
return UIImage(cgImage: croppedCgImage, scale: image.scale, orientation: image.imageOrientation)
}
static func write(data: Data, to path: String) -> String? {
let url = URL(fileURLWithPath: path)
do {
try data.write(to: url, options: .atomic)
return url.absoluteString
} catch {
return nil
}
}
static func data(from image: UIImage, with metadata: [String: Any], quality: Float) -> Data? {
guard let sourceCGImageRef = image.cgImage,
let sourceData = image.jpegData(compressionQuality: 1.0) as CFData?,
let sourceCGImageSourceRef = CGImageSourceCreateWithData(sourceData, nil),
let sourceMetadata = CGImageSourceCopyPropertiesAtIndex(sourceCGImageSourceRef, 0, nil) as? NSDictionary else {
return nil
}
let updatedMetadata = NSMutableDictionary(dictionary: sourceMetadata)
for (key, value) in metadata {
updatedMetadata[key] = value
}
updatedMetadata.setObject(NSNumber(value: quality), forKey: kCGImageDestinationLossyCompressionQuality as NSString)
let processedImageData = NSMutableData()
guard let sourceType = CGImageSourceGetType(sourceCGImageSourceRef) else {
return nil
}
guard let destinationCGImageRef =
CGImageDestinationCreateWithData(processedImageData, sourceType, 1, nil) else {
return nil
}
CGImageDestinationAddImage(destinationCGImageRef, sourceCGImageRef, updatedMetadata)
if CGImageDestinationFinalize(destinationCGImageRef) {
return processedImageData as Data
}
CGImageDestinationAddImage(destinationCGImageRef, sourceCGImageRef, updatedMetadata as CFDictionary)
return CGImageDestinationFinalize(destinationCGImageRef) ? processedImageData as Data : nil
}
static func updateExif(metadata: NSDictionary, with additionalData: [String: Any]) -> NSMutableDictionary {
let mutableMetadata = NSMutableDictionary(dictionary: metadata)
mutableMetadata.addEntries(from: additionalData)
if let gps = mutableMetadata[kCGImagePropertyGPSDictionary as String] as? [String: Any] {
for (gpsKey, gpsValue) in gps {
mutableMetadata["GPS" + gpsKey] = gpsValue
}
}
return mutableMetadata
}
}

View File

@@ -0,0 +1,24 @@
import ExpoModulesCore
struct TakePictureOptions: Record {
@Field
var id: Int = 0
@Field
var quality: Double = 0
@Field
var base64: Bool = false
@Field
var exif: Bool = false
@Field
var mirror: Bool = false
@Field
var fastMode: Bool = false
@Field
var additionalExif: [String: Any]?
}

View File

@@ -0,0 +1,160 @@
import ExpoModulesCore
import Vision
struct BarcodeSettings: Record {
@Field var barcodeTypes: [BarcodeType]
func toMetadataObjectType() -> [AVMetadataObject.ObjectType] {
barcodeTypes.map {
$0.toMetadataObjectType()
}
}
}
enum BarcodeType: String, Enumerable {
case aztec
case ean13
case ean8
case qr
case pdf417
case upc_e
case datamatrix
case code39
case code93
case itf14
case codabar
case code128
case upc_a
func toMetadataObjectType() -> AVMetadataObject.ObjectType {
if #available(iOS 15.4, *) {
if self == .codabar {
return .codabar
}
}
switch self {
case .aztec:
return .aztec
case .qr:
return .qr
case .ean13:
return .ean13
case .ean8:
return .ean8
case .pdf417:
return .pdf417
case .itf14:
return .itf14
case .upc_a:
return .ean13
case .upc_e:
return .upce
case .code39:
return .code39
case .code93:
return .code93
case .datamatrix:
return .dataMatrix
case .code128:
return .code128
default:
return .aztec
}
}
static func toBarcodeType(type: AVMetadataObject.ObjectType) -> BarcodeType {
if #available(iOS 15.4, *) {
if type == .codabar {
return .codabar
}
}
switch type {
case .aztec:
return .aztec
case .qr:
return .qr
case .ean13:
return .ean13
case .ean8:
return .ean8
case .pdf417:
return .pdf417
case .itf14:
return .itf14
case .upce:
return .upc_e
case .code39:
return .code39
case .code93:
return .code93
case .dataMatrix:
return .datamatrix
case .code128:
return .code128
default:
return .aztec
}
}
}
enum VNBarcodeType: String, Enumerable {
case aztec
case ean13
case ean8
case qr
case pdf417
case upc_e
case datamatrix
case code39
case code93
case itf14
case codabar
case code128
case upc_a
@available(iOS 16.0, *)
func toSymbology() -> VNBarcodeSymbology {
switch self {
case .aztec:
return .aztec
case .codabar:
return .codabar
case .qr:
return .qr
case .ean13:
return .ean13
case .ean8:
return .ean8
case .pdf417:
return .pdf417
case .itf14:
return .itf14
case .upc_a:
return .ean13
case .upc_e:
return .upce
case .code39:
return .code39
case .code93:
return .code93
case .datamatrix:
return .dataMatrix
case .code128:
return .code128
}
}
}
struct VisionScannerOptions: Record {
@Field var barcodeTypes: [VNBarcodeType] = []
@Field var isPinchToZoomEnabled: Bool = false
@Field var isGuidanceEnabled: Bool = true
@Field var isHighlightingEnabled: Bool = false
@available(iOS 16.0, *)
func toSymbology() -> [VNBarcodeSymbology] {
barcodeTypes.map {
$0.toSymbology()
}
}
}

View File

@@ -0,0 +1,234 @@
import ZXingObjC
import AVFoundation
let BARCODE_TYPES_KEY = "barcodeTypes"
class BarcodeScanner: NSObject, AVCaptureMetadataOutputObjectsDelegate, AVCaptureVideoDataOutputSampleBufferDelegate {
var onBarcodeScanned: (([String: Any]?) -> Void)?
var isScanningBarcodes = false
// MARK: - Properties
private let session: AVCaptureSession
private let sessionQueue: DispatchQueue
private let zxingCaptureQueue = DispatchQueue(label: "com.zxing.captureQueue")
private var metadataOutput: AVCaptureMetadataOutput?
private var videoDataOutput: AVCaptureVideoDataOutput?
private var settings = BarcodeScannerUtils.getDefaultSettings()
private var zxingBarcodeReaders: [AVMetadataObject.ObjectType: ZXReader] = [
AVMetadataObject.ObjectType.pdf417: ZXPDF417Reader(),
AVMetadataObject.ObjectType.code39: ZXCode39Reader()
]
private var previewLayer: AVCaptureVideoPreviewLayer?
private var zxingFPSProcessed = 6.0
private var zxingEnabled = true
init(session: AVCaptureSession, sessionQueue: DispatchQueue) {
self.session = session
self.sessionQueue = sessionQueue
if #available(iOS 15.4, *) {
zxingBarcodeReaders[AVMetadataObject.ObjectType.codabar] = ZXCodaBarReader()
}
}
func setSettings(_ newSettings: [String: [AVMetadataObject.ObjectType]]) {
for (key, value) in newSettings where key == BARCODE_TYPES_KEY {
let previousTypes = Set(settings[BARCODE_TYPES_KEY] ?? [])
let newTypes = Set(value)
if previousTypes != newTypes {
settings[BARCODE_TYPES_KEY] = value
let zxingCoveredTypes = Set(zxingBarcodeReaders.keys)
zxingEnabled = !zxingCoveredTypes.isDisjoint(with: newTypes)
sessionQueue.async {
self.maybeStartBarcodeScanning()
}
}
}
}
func setPreviewLayer(layer: AVCaptureVideoPreviewLayer) {
self.previewLayer = layer
}
func setIsEnabled(_ enabled: Bool) {
guard isScanningBarcodes != enabled else {
return
}
isScanningBarcodes = enabled
sessionQueue.async {
if self.isScanningBarcodes {
if self.metadataOutput != nil {
self.setConnection(enabled: true)
} else {
self.maybeStartBarcodeScanning()
}
} else {
self.setConnection(enabled: false)
}
}
}
func setConnection(enabled: Bool) {
metadataOutput?.connections.forEach {
$0.isEnabled = enabled
}
}
func maybeStartBarcodeScanning() {
guard isScanningBarcodes else {
return
}
if metadataOutput == nil || videoDataOutput == nil {
addOutputs()
if metadataOutput == nil {
return
}
}
let availableObjectTypes: [AVMetadataObject.ObjectType] = metadataOutput?.availableMetadataObjectTypes ?? []
let requestedTypes = (settings[BARCODE_TYPES_KEY] ?? []).filter {
availableObjectTypes.contains($0)
}
metadataOutput?.metadataObjectTypes = requestedTypes
}
func stopBarcodeScanning() {
removeOutputs()
if isScanningBarcodes {
onBarcodeScanned?(nil)
}
}
func scanBarcodes(from image: CGImage, completion: @escaping (ZXResult) -> Void) {
let source = ZXCGImageLuminanceSource(cgImage: image)
let binarizer = ZXHybridBinarizer(source: source)
let bitmap = ZXBinaryBitmap(binarizer: binarizer)
var result: ZXResult?
for reader in zxingBarcodeReaders.values {
result = try? reader.decode(bitmap, hints: nil)
if result != nil {
break
}
}
if result == nil && bitmap?.rotateSupported == true {
if let rotatedBitmap = bitmap?.rotateCounterClockwise() {
for reader in zxingBarcodeReaders.values {
result = try? reader.decode(rotatedBitmap, hints: nil)
if result != nil {
break
}
}
}
}
if let result {
completion(result)
}
}
private func addOutputs() {
session.beginConfiguration()
if metadataOutput == nil {
let output = AVCaptureMetadataOutput()
output.setMetadataObjectsDelegate(self, queue: sessionQueue)
if session.canAddOutput(output) {
session.addOutput(output)
metadataOutput = output
}
}
if videoDataOutput == nil {
let output = AVCaptureVideoDataOutput()
output.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
output.alwaysDiscardsLateVideoFrames = true
output.setSampleBufferDelegate(self, queue: zxingCaptureQueue)
if session.canAddOutput(output) {
session.addOutput(output)
videoDataOutput = output
}
}
session.commitConfiguration()
}
private func removeOutputs() {
session.beginConfiguration()
if let metadataOutput {
if session.outputs.contains(metadataOutput) {
session.removeOutput(metadataOutput)
self.metadataOutput = nil
}
}
if let videoDataOutput {
if session.outputs.contains(videoDataOutput) {
session.removeOutput(videoDataOutput)
self.videoDataOutput = nil
}
}
session.commitConfiguration()
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
guard let settings = settings[BARCODE_TYPES_KEY], let metadataOutput else {
return
}
for metadata in metadataObjects {
var codeMetadata = metadata as? AVMetadataMachineReadableCodeObject
if let previewLayer {
codeMetadata = previewLayer.transformedMetadataObject(for: metadata) as? AVMetadataMachineReadableCodeObject
}
for barcodeType in settings {
if zxingBarcodeReaders[barcodeType] != nil {
continue
}
if let codeMetadata {
if codeMetadata.stringValue != nil && codeMetadata.type == barcodeType {
onBarcodeScanned?(BarcodeScannerUtils.avMetadataCodeObjectToDictionary(codeMetadata))
}
}
}
}
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let barcodeTypes = settings[BARCODE_TYPES_KEY],
let metadataOutput,
zxingEnabled else {
return
}
let kMinMargin = 1.0 / zxingFPSProcessed
let presentTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
var curFrameTimeStamp = 0.0
var lastFrameTimeStamp = 0.0
curFrameTimeStamp = Double(presentTimeStamp.value) / Double(presentTimeStamp.timescale)
if curFrameTimeStamp - lastFrameTimeStamp > Double(kMinMargin) {
lastFrameTimeStamp = curFrameTimeStamp
if let videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer),
let videoFrameImage = ZXCGImageLuminanceSource.createImage(from: videoFrame) {
self.scanBarcodes(from: videoFrameImage) { barcodeScannerResult in
self.onBarcodeScanned?(BarcodeScannerUtils.zxResultToDictionary(barcodeScannerResult))
}
}
}
}
}

View File

@@ -0,0 +1,137 @@
import AVFoundation
import ZXingObjC
import VisionKit
import Vision
class BarcodeScannerUtils {
static func getDefaultSettings() -> [String: [AVMetadataObject.ObjectType]] {
var validTypes = [
"upc_e": AVMetadataObject.ObjectType.upce,
"upc_a": AVMetadataObject.ObjectType.ean13,
"code39": AVMetadataObject.ObjectType.code39,
"code39mod43": AVMetadataObject.ObjectType.code39Mod43,
"ean13": AVMetadataObject.ObjectType.ean13,
"ean8": AVMetadataObject.ObjectType.ean8,
"code93": AVMetadataObject.ObjectType.code93,
"code128": AVMetadataObject.ObjectType.code128,
"pdf417": AVMetadataObject.ObjectType.pdf417,
"qr": AVMetadataObject.ObjectType.qr,
"aztec": AVMetadataObject.ObjectType.aztec,
"interleaved2of5": AVMetadataObject.ObjectType.interleaved2of5,
"itf14": AVMetadataObject.ObjectType.itf14,
"datamatrix": AVMetadataObject.ObjectType.dataMatrix
]
if #available(iOS 15.4, *) {
validTypes["codabar"] = AVMetadataObject.ObjectType.codabar
}
return [BARCODE_TYPES_KEY: Array(validTypes.values)]
}
static func avMetadataCodeObjectToDictionary(_ barcodeScannerResult: AVMetadataMachineReadableCodeObject) -> [String: Any] {
var result = [String: Any]()
result["type"] = BarcodeType.toBarcodeType(type: barcodeScannerResult.type).rawValue
result["data"] = barcodeScannerResult.stringValue
// iOS converts upc_a to ean13 and appends a leading 0
if barcodeScannerResult.type == AVMetadataObject.ObjectType.ean13 {
let value = barcodeScannerResult.stringValue ?? ""
if !value.isEmpty && value.hasPrefix("0") {
result["data"] = value.dropFirst()
}
}
if !barcodeScannerResult.corners.isEmpty {
var cornerPointsResult = [[String: Any]]()
for point in barcodeScannerResult.corners {
cornerPointsResult.append(["x": point.x, "y": point.y])
}
result["cornerPoints"] = cornerPointsResult
result["bounds"] = [
"origin": [
"x": barcodeScannerResult.bounds.origin.x,
"y": barcodeScannerResult.bounds.origin.y
],
"size": [
"width": barcodeScannerResult.bounds.size.width,
"height": barcodeScannerResult.bounds.size.height
]
]
} else {
addEmptyCornerPoints(to: &result)
}
return result
}
@available(iOS 16.0, *)
static func visionDataScannerObjectToDictionary(item: RecognizedItem.Barcode) -> [String: Any] {
var result = [String: Any]()
result["type"] = item.observation.symbology.rawValue
result["data"] = item.payloadStringValue
// iOS converts upc_a to ean13 and appends a leading 0
if item.observation.symbology == VNBarcodeSymbology.ean13 {
let value = item.payloadStringValue ?? ""
if !value.isEmpty && value.hasPrefix("0") {
result["data"] = value.dropFirst()
}
} else {
result["data"] = item.payloadStringValue
}
let bounds = item.bounds
let cornerPoints: [[String: Any]] = [bounds.bottomLeft, bounds.bottomRight, bounds.topLeft, bounds.topRight].map { point in
["x": point.x, "y": point.y]
}
result["cornerPoints"] = cornerPoints
return result
}
static func addEmptyCornerPoints(to result: inout [String: Any]) {
result["cornerPoints"] = []
result["bounds"] = [
"origin": [
"x": 0,
"y": 0
],
"size": [
"width": 0,
"height": 0
]
]
}
static func zxResultToDictionary(_ barcodeScannerResult: ZXResult) -> [String: Any] {
var result = [String: Any]()
result["type"] = BarcodeScannerUtils.zxingFormatToString(barcodeScannerResult.barcodeFormat)
var data = ""
for i in 0..<barcodeScannerResult.text.count {
let character = barcodeScannerResult.text[barcodeScannerResult.text.index(barcodeScannerResult.text.startIndex, offsetBy: i)]
if character != "\0" {
data.append(character)
}
}
result["data"] = data
return result
}
static func zxingFormatToString(_ format: ZXBarcodeFormat) -> String {
switch format {
case kBarcodeFormatPDF417:
return AVMetadataObject.ObjectType.pdf417.rawValue
case kBarcodeFormatCode39:
return AVMetadataObject.ObjectType.code39.rawValue
case kBarcodeFormatCodabar:
if #available(iOS 15.4, *) {
return AVMetadataObject.ObjectType.codabar.rawValue
}
return "unknown"
default:
return "unknown"
}
}
}

View File

@@ -0,0 +1,54 @@
import AVFoundation
import ExpoModulesCore
enum CameraType: String, Enumerable {
case front
case back
func toPosition() -> AVCaptureDevice.Position {
switch self {
case .front:
return .front
case .back:
return .back
}
}
}
enum FlashMode: String, Enumerable {
case off
case on
case auto
func toDeviceFlashMode() -> AVCaptureDevice.FlashMode {
switch self {
case .off:
return .off
case .on:
return .on
case .auto:
return .auto
}
}
}
enum CameraMode: String, Enumerable {
case picture
case video
}
enum FocusMode: String, Enumerable {
case on
case off
func toAVCaptureFocusMode() -> AVCaptureDevice.FocusMode {
switch self {
case .on:
return .autoFocus
case .off:
return .continuousAutoFocus
default:
return .continuousAutoFocus
}
}
}

View File

@@ -0,0 +1,90 @@
import ExpoModulesCore
struct CameraRecordingOptions: Record {
@Field var maxDuration: Double?
@Field var maxFileSize: Double?
@Field var quality: VideoQuality?
@Field var mirror: Bool = false
@Field var codec: VideoCodec?
}
enum VideoQuality: String, Enumerable {
case video2160p = "2160p"
case video1080p = "1080p"
case video720p = "720p"
case video480p = "480p"
case video4x3 = "4:3"
func toPreset() -> AVCaptureSession.Preset {
switch self {
case .video2160p:
return .hd4K3840x2160
case .video1080p:
return .hd1920x1080
case .video720p:
return .hd1280x720
case .video480p:
return .vga640x480
default:
return .high
}
}
}
enum VideoCodec: String, Enumerable {
case h264 = "avc1"
case hevc = "hvc1"
case jpeg = "jpeg"
case appleProRes422 = "apcn"
case appleProRes4444 = "ap4h"
func codecType() -> AVVideoCodecType {
switch self {
case .h264:
return .h264
case .hevc:
return .hevc
case .jpeg:
return .jpeg
case .appleProRes422:
return .proRes422
case .appleProRes4444:
return .proRes4444
}
}
}
enum PictureSize: String, Enumerable {
case hd4k = "3840x2160"
case hd1920 = "1920x1080"
case hd720 = "1280x720"
case vga = "640x480"
case cif = "352x288"
case photo = "Photo"
case high = "High"
case medium = "Medium"
case low = "Low"
func toCapturePreset() -> AVCaptureSession.Preset {
switch self {
case .hd4k:
return .hd4K3840x2160
case .hd1920:
return .hd1920x1080
case .hd720:
return .hd1280x720
case .vga:
return .vga640x480
case .cif:
return .cif352x288
case .photo:
return .photo
case .high:
return .high
case .medium:
return .medium
case .low:
return .low
}
}
}

View File

@@ -0,0 +1,853 @@
import UIKit
import ExpoModulesCore
import CoreMotion
public class CameraView: ExpoView, EXCameraInterface, EXAppLifecycleListener,
AVCaptureFileOutputRecordingDelegate, AVCapturePhotoCaptureDelegate, CameraEvent {
public var session = AVCaptureSession()
public var sessionQueue = DispatchQueue(label: "captureSessionQueue")
// MARK: - Legacy Modules
private var lifecycleManager: EXAppLifecycleService?
private var permissionsManager: EXPermissionsInterface?
// MARK: - Properties
private lazy var barcodeScanner = createBarcodeScanner()
private var previewLayer = PreviewView()
private var isValidVideoOptions = true
private var videoCodecType: AVVideoCodecType?
private var photoCaptureOptions: TakePictureOptions?
private var errorNotification: NSObjectProtocol?
private var physicalOrientation: UIDeviceOrientation = .unknown
private var motionManager: CMMotionManager = {
let mm = CMMotionManager()
mm.accelerometerUpdateInterval = 0.2
mm.gyroUpdateInterval = 0.2
return mm
}()
private var cameraShouldInit = true
private var isSessionPaused = false
// MARK: Property Observers
var responsiveWhenOrientationLocked = false {
didSet {
updateResponsiveOrientation()
}
}
var videoQuality: VideoQuality = .video1080p {
didSet {
if self.session.sessionPreset != videoQuality.toPreset() {
self.sessionQueue.async {
self.updateSessionPreset(preset: self.videoQuality.toPreset())
}
}
}
}
var isScanningBarcodes = false {
didSet {
barcodeScanner.setIsEnabled(isScanningBarcodes)
}
}
var presetCamera = AVCaptureDevice.Position.back {
didSet {
updateType()
}
}
var flashMode = FlashMode.auto
var torchEnabled = false {
didSet {
enableTorch()
}
}
var autoFocus = AVCaptureDevice.FocusMode.continuousAutoFocus {
didSet {
setFocusMode()
}
}
var pictureSize = PictureSize.high {
didSet {
updatePictureSize()
}
}
var mode = CameraMode.picture {
didSet {
setCameraMode()
}
}
var isMuted = false {
didSet {
updateSessionAudioIsMuted()
}
}
var active = true {
didSet {
updateCameraIsActive()
}
}
var animateShutter = true
var mirror = false
var zoom: CGFloat = 0 {
didSet {
updateZoom()
}
}
// MARK: - Session Inputs and Outputs
private var videoFileOutput: AVCaptureMovieFileOutput?
private var photoOutput: AVCapturePhotoOutput?
private var captureDeviceInput: AVCaptureDeviceInput?
// MARK: - Promises
private var photoCapturedPromise: Promise?
private var videoRecordedPromise: Promise?
// MARK: - Events
let onCameraReady = EventDispatcher()
let onMountError = EventDispatcher()
let onPictureSaved = EventDispatcher()
let onBarcodeScanned = EventDispatcher()
let onResponsiveOrientationChanged = EventDispatcher()
private var deviceOrientation: UIInterfaceOrientation {
window?.windowScene?.interfaceOrientation ?? .unknown
}
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
lifecycleManager = appContext?.legacyModule(implementing: EXAppLifecycleService.self)
permissionsManager = appContext?.legacyModule(implementing: EXPermissionsInterface.self)
#if !targetEnvironment(simulator)
setupPreview()
#endif
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(
self,
selector: #selector(orientationChanged(notification:)),
name: UIDevice.orientationDidChangeNotification,
object: nil)
lifecycleManager?.register(self)
}
private func setupPreview() {
DispatchQueue.main.async {
self.previewLayer.videoPreviewLayer.session = self.session
self.previewLayer.videoPreviewLayer.videoGravity = .resizeAspectFill
self.previewLayer.videoPreviewLayer.needsDisplayOnBoundsChange = true
self.addSubview(self.previewLayer)
}
}
func initCamera() {
guard cameraShouldInit else {
return
}
cameraShouldInit = false
self.initializeCaptureSessionInput()
}
private func updateType() {
cameraShouldInit = true
}
public func onAppForegrounded() {
if !session.isRunning && isSessionPaused {
isSessionPaused = false
sessionQueue.async {
self.session.startRunning()
self.enableTorch()
}
}
}
public func onAppBackgrounded() {
if session.isRunning && !isSessionPaused {
isSessionPaused = true
sessionQueue.async {
self.session.stopRunning()
}
}
}
private func updatePictureSize() {
#if !targetEnvironment(simulator)
sessionQueue.async {
self.session.beginConfiguration()
let preset = self.pictureSize.toCapturePreset()
if self.session.canSetSessionPreset(preset) {
self.session.sessionPreset = preset
}
self.session.commitConfiguration()
}
#endif
}
private func enableTorch() {
guard let device = captureDeviceInput?.device, device.hasTorch else {
return
}
do {
try device.lockForConfiguration()
if device.hasTorch && device.isTorchModeSupported(.on) {
device.torchMode = torchEnabled ? .on : .off
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
device.unlockForConfiguration()
}
private func setFocusMode() {
guard let device = captureDeviceInput?.device else {
return
}
do {
try device.lockForConfiguration()
if device.isFocusModeSupported(autoFocus), device.focusMode != autoFocus {
device.focusMode = autoFocus
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
return
}
device.unlockForConfiguration()
}
private func setCameraMode() {
sessionQueue.async {
if self.mode == .video {
if self.videoFileOutput == nil {
self.setupMovieFileCapture()
}
self.updateSessionAudioIsMuted()
} else {
self.cleanupMovieFileCapture()
}
}
}
private func startSession() {
#if targetEnvironment(simulator)
return
#endif
guard let manager = permissionsManager else {
log.info("Permissions module not found.")
return
}
if !manager.hasGrantedPermission(usingRequesterClass: CameraOnlyPermissionRequester.self) {
onMountError(["message": "Camera permissions not granted - component could not be rendered."])
return
}
sessionQueue.async {
let photoOutput = AVCapturePhotoOutput()
photoOutput.isLivePhotoCaptureEnabled = false
if self.session.canAddOutput(photoOutput) {
self.session.addOutput(photoOutput)
self.photoOutput = photoOutput
}
self.session.sessionPreset = self.mode == .video ? self.pictureSize.toCapturePreset() : .photo
self.addErrorNotification()
self.changePreviewOrientation()
}
// Delay starting the scanner
sessionQueue.asyncAfter(deadline: .now() + 0.5) {
self.barcodeScanner.maybeStartBarcodeScanning()
self.session.startRunning()
self.onCameraReady()
self.enableTorch()
}
}
private func updateZoom() {
guard let device = captureDeviceInput?.device else {
return
}
do {
try device.lockForConfiguration()
device.videoZoomFactor = (device.activeFormat.videoMaxZoomFactor - 1.0) * zoom + 1.0
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
device.unlockForConfiguration()
}
private func addErrorNotification() {
if self.errorNotification != nil {
NotificationCenter.default.removeObserver(self.errorNotification as Any)
}
self.errorNotification = NotificationCenter.default.addObserver(
forName: .AVCaptureSessionRuntimeError,
object: self.session,
queue: nil) { [weak self] notification in
guard let self else {
return
}
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else {
return
}
if error.code == .mediaServicesWereReset {
self.session.startRunning()
self.updateSessionAudioIsMuted()
self.onCameraReady()
}
}
}
func setBarcodeScannerSettings(settings: BarcodeSettings) {
barcodeScanner.setSettings([BARCODE_TYPES_KEY: settings.toMetadataObjectType()])
}
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 takePicture(options: TakePictureOptions, promise: Promise) {
if photoCapturedPromise != nil {
promise.reject(CameraNotReadyException())
return
}
guard let photoOutput else {
promise.reject(CameraOutputNotReadyException())
return
}
photoCapturedPromise = promise
photoCaptureOptions = options
sessionQueue.async {
let connection = photoOutput.connection(with: .video)
let orientation = self.responsiveWhenOrientationLocked ? self.physicalOrientation : UIDevice.current.orientation
connection?.videoOrientation = ExpoCameraUtils.videoOrientation(for: orientation)
// options.mirror is deprecated but should continue to work until removed
connection?.isVideoMirrored = self.presetCamera == .front && (self.mirror || options.mirror)
var photoSettings = AVCapturePhotoSettings()
if photoOutput.availablePhotoCodecTypes.contains(AVVideoCodecType.hevc) {
photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
}
var requestedFlashMode = self.flashMode.toDeviceFlashMode()
if photoOutput.supportedFlashModes.contains(requestedFlashMode) {
photoSettings.flashMode = requestedFlashMode
}
if #available(iOS 16.0, *) {
photoSettings.maxPhotoDimensions = photoOutput.maxPhotoDimensions
}
if !photoSettings.availablePreviewPhotoPixelFormatTypes.isEmpty,
let previewFormat = photoSettings.__availablePreviewPhotoPixelFormatTypes.first {
photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewFormat]
}
if photoOutput.isHighResolutionCaptureEnabled {
photoSettings.isHighResolutionPhotoEnabled = true
}
photoSettings.photoQualityPrioritization = .balanced
photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
}
public func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
guard animateShutter else {
return
}
DispatchQueue.main.async {
self.previewLayer.videoPreviewLayer.opacity = 0
UIView.animate(withDuration: 0.25) {
self.previewLayer.videoPreviewLayer.opacity = 1
}
}
}
public func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
guard let promise = photoCapturedPromise, let options = photoCaptureOptions else {
return
}
photoCapturedPromise = nil
photoCaptureOptions = nil
if error != nil {
promise.reject(CameraImageCaptureException())
return
}
let imageData = photo.fileDataRepresentation()
handleCapturedImageData(
imageData: imageData,
metadata: photo.metadata,
options: options,
promise: promise
)
}
func handleCapturedImageData(
imageData: Data?,
metadata: [String: Any],
options: TakePictureOptions,
promise: Promise
) {
guard let imageData, var takenImage = UIImage(data: imageData) else {
return
}
if options.fastMode {
promise.resolve()
}
let previewSize: CGSize = {
return deviceOrientation == .portrait ?
CGSize(width: previewLayer.frame.size.height, height: previewLayer.frame.size.width) :
CGSize(width: previewLayer.frame.size.width, height: previewLayer.frame.size.height)
}()
guard let takenCgImage = takenImage.cgImage else {
return
}
let cropRect = CGRect(x: 0, y: 0, width: takenCgImage.width, height: takenCgImage.height)
let croppedSize = AVMakeRect(aspectRatio: previewSize, insideRect: cropRect)
takenImage = ExpoCameraUtils.crop(image: takenImage, to: croppedSize)
let path = FileSystemUtilities.generatePathInCache(
appContext,
in: "Camera",
extension: ".jpg"
)
let width = takenImage.size.width
let height = takenImage.size.height
var processedImageData: Data?
var response = [String: Any]()
if options.exif {
guard let exifDict = metadata[kCGImagePropertyExifDictionary as String] as? NSDictionary else {
return
}
let updatedExif = ExpoCameraUtils.updateExif(
metadata: exifDict,
with: ["Orientation": ExpoCameraUtils.toExifOrientation(orientation: takenImage.imageOrientation)]
)
updatedExif[kCGImagePropertyExifPixelYDimension] = width
updatedExif[kCGImagePropertyExifPixelXDimension] = height
response["exif"] = updatedExif
var updatedMetadata = metadata
if let additionalExif = options.additionalExif {
updatedExif.addEntries(from: additionalExif)
var gpsDict = [String: Any]()
if let latitude = additionalExif["GPSLatitude"] as? Double {
gpsDict[kCGImagePropertyGPSLatitude as String] = abs(latitude)
gpsDict[kCGImagePropertyGPSLatitudeRef as String] = latitude >= 0 ? "N" : "S"
}
if let longitude = additionalExif["GPSLongitude"] as? Double {
gpsDict[kCGImagePropertyGPSLongitude as String] = abs(longitude)
gpsDict[kCGImagePropertyGPSLongitudeRef as String] = longitude >= 0 ? "E" : "W"
}
if let altitude = additionalExif["GPSAltitude"] as? Double {
gpsDict[kCGImagePropertyGPSAltitude as String] = abs(altitude)
gpsDict[kCGImagePropertyGPSAltitudeRef as String] = altitude >= 0 ? 0 : 1
}
if updatedMetadata[kCGImagePropertyGPSDictionary as String] == nil {
updatedMetadata[kCGImagePropertyGPSDictionary as String] = gpsDict
} else if var existingGpsDict = updatedMetadata[kCGImagePropertyGPSDictionary as String] as? [String: Any] {
existingGpsDict.merge(gpsDict) { _, new in
new
}
updatedMetadata[kCGImagePropertyGPSDictionary as String] = existingGpsDict
}
}
updatedMetadata[kCGImagePropertyExifDictionary as String] = updatedExif
processedImageData = ExpoCameraUtils.data(
from: takenImage,
with: updatedMetadata,
quality: Float(options.quality))
} else {
processedImageData = takenImage.jpegData(compressionQuality: options.quality)
}
guard let processedImageData else {
promise.reject(CameraSavingImageException())
return
}
response["uri"] = ExpoCameraUtils.write(data: processedImageData, to: path)
response["width"] = width
response["height"] = height
if options.base64 {
response["base64"] = processedImageData.base64EncodedString()
}
if options.fastMode {
onPictureSaved(["data": response, "id": options.id])
} else {
promise.resolve(response)
}
}
func record(options: CameraRecordingOptions, promise: Promise) {
sessionQueue.async {
let preset = options.quality?.toPreset()
if let preset {
self.updateSessionPreset(preset: preset)
}
}
sessionQueue.async {
if let videoFileOutput = self.videoFileOutput, !videoFileOutput.isRecording && self.videoRecordedPromise == nil {
if let connection = videoFileOutput.connection(with: .video) {
if connection.isVideoStabilizationSupported {
connection.preferredVideoStabilizationMode = .auto
} else {
log.warn("\(#function): Video Stabilization is not supported on this device.")
}
let orientation = self.responsiveWhenOrientationLocked ? self.physicalOrientation : UIDevice.current.orientation
connection.videoOrientation = ExpoCameraUtils.videoOrientation(for: orientation)
self.setVideoOptions(options: options, for: connection, promise: promise)
if connection.isVideoOrientationSupported && self.mirror {
connection.isVideoMirrored = self.mirror
}
}
if !self.isValidVideoOptions {
return
}
let path = FileSystemUtilities.generatePathInCache(self.appContext, in: "Camera", extension: ".mov")
let fileUrl = URL(fileURLWithPath: path)
self.videoRecordedPromise = promise
videoFileOutput.startRecording(to: fileUrl, recordingDelegate: self)
}
}
}
func setVideoOptions(options: CameraRecordingOptions, for connection: AVCaptureConnection, promise: Promise) {
self.isValidVideoOptions = true
guard let videoFileOutput = self.videoFileOutput else {
return
}
if let maxDuration = options.maxDuration {
videoFileOutput.maxRecordedDuration = CMTime(seconds: maxDuration, preferredTimescale: 1000)
}
if let maxFileSize = options.maxFileSize {
videoFileOutput.maxRecordedFileSize = Int64(maxFileSize)
}
if let codec = options.codec {
let codecType = codec.codecType()
if videoFileOutput.availableVideoCodecTypes.contains(codecType) {
videoFileOutput.setOutputSettings([AVVideoCodecKey: codecType], for: connection)
self.videoCodecType = codecType
} else {
promise.reject(CameraRecordingException(self.videoCodecType?.rawValue))
self.cleanupMovieFileCapture()
self.videoRecordedPromise = nil
self.isValidVideoOptions = false
}
}
}
// Must be called on the sessionQueue
func updateSessionAudioIsMuted() {
sessionQueue.async {
self.session.beginConfiguration()
if self.isMuted {
for input in self.session.inputs {
if let deviceInput = input as? AVCaptureDeviceInput {
if deviceInput.device.hasMediaType(.audio) {
self.session.removeInput(input)
return
}
}
}
}
if !self.isMuted && self.mode == .video {
if let audioCapturedevice = AVCaptureDevice.default(for: .audio) {
do {
let audioDeviceInput = try AVCaptureDeviceInput(device: audioCapturedevice)
if self.session.canAddInput(audioDeviceInput) {
self.session.addInput(audioDeviceInput)
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
return
}
}
}
self.session.commitConfiguration()
}
}
// Must be called on the sessionQueue
func setupMovieFileCapture() {
let output = AVCaptureMovieFileOutput()
if self.session.canAddOutput(output) {
self.session.beginConfiguration()
self.session.addOutput(output)
self.videoFileOutput = output
self.session.commitConfiguration()
}
}
// Must be called on the sessionQueue
func cleanupMovieFileCapture() {
if let videoFileOutput {
if session.outputs.contains(videoFileOutput) {
self.session.beginConfiguration()
session.removeOutput(videoFileOutput)
self.videoFileOutput = nil
self.session.commitConfiguration()
}
}
}
public override func layoutSubviews() {
previewLayer.videoPreviewLayer.frame = self.bounds
self.backgroundColor = .black
self.layer.insertSublayer(previewLayer.videoPreviewLayer, at: 0)
}
public override func removeFromSuperview() {
lifecycleManager?.unregisterAppLifecycleListener(self)
self.stopSession()
UIDevice.current.endGeneratingDeviceOrientationNotifications()
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
super.removeFromSuperview()
}
func updateCameraIsActive() {
if self.session.isRunning == active {
return
}
sessionQueue.async {
if self.active {
self.session.startRunning()
} else {
self.session.stopRunning()
}
}
}
public func fileOutput(
_ output: AVCaptureFileOutput,
didFinishRecordingTo outputFileURL: URL,
from connections: [AVCaptureConnection],
error: Error?
) {
var success = true
if error != nil {
let value = (error as? NSError)?.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as? Bool
success = value == true ? true : false
}
if success && videoRecordedPromise != nil {
videoRecordedPromise?.resolve(["uri": outputFileURL.absoluteString])
} else if videoRecordedPromise != nil {
videoRecordedPromise?.reject(CameraRecordingFailedException())
}
videoRecordedPromise = nil
videoCodecType = nil
}
func setPresetCamera(presetCamera: AVCaptureDevice.Position) {
self.presetCamera = presetCamera
}
func stopRecording() {
sessionQueue.async {
self.videoFileOutput?.stopRecording()
}
}
// Must be called on the sessionQueue
func updateSessionPreset(preset: AVCaptureSession.Preset) {
#if !targetEnvironment(simulator)
if self.session.canSetSessionPreset(preset) {
if self.session.sessionPreset != preset {
self.session.beginConfiguration()
self.session.sessionPreset = preset
self.session.commitConfiguration()
}
}
#endif
}
func initializeCaptureSessionInput() {
if captureDeviceInput?.device.position == presetCamera {
return
}
sessionQueue.async {
self.session.beginConfiguration()
guard let device = ExpoCameraUtils.device(with: .video, preferring: self.presetCamera) else {
return
}
if let videoCaptureDeviceInput = self.captureDeviceInput {
self.session.removeInput(videoCaptureDeviceInput)
}
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: device)
if self.session.canAddInput(captureDeviceInput) {
self.session.addInput(captureDeviceInput)
self.captureDeviceInput = captureDeviceInput
self.updateZoom()
}
} catch {
self.onMountError(["message": "Camera could not be started - \(error.localizedDescription)"])
}
self.session.commitConfiguration()
self.startSession()
}
}
private func stopSession() {
#if targetEnvironment(simulator)
return
#endif
self.previewLayer.videoPreviewLayer.removeFromSuperlayer()
sessionQueue.async {
self.session.beginConfiguration()
for input in self.session.inputs {
self.session.removeInput(input)
}
for output in self.session.outputs {
self.session.removeOutput(output)
}
self.barcodeScanner.stopBarcodeScanning()
self.session.commitConfiguration()
self.motionManager.stopAccelerometerUpdates()
if self.session.isRunning {
self.session.stopRunning()
}
}
}
func resumePreview() {
previewLayer.videoPreviewLayer.connection?.isEnabled = true
}
func pausePreview() {
previewLayer.videoPreviewLayer.connection?.isEnabled = false
}
@objc func orientationChanged(notification: Notification) {
changePreviewOrientation()
}
func changePreviewOrientation() {
EXUtilities.performSynchronously {
// We shouldn't access the device orientation anywhere but on the main thread
let videoOrientation = ExpoCameraUtils.videoOrientation(for: self.deviceOrientation)
if (self.previewLayer.videoPreviewLayer.connection?.isVideoOrientationSupported) == true {
self.physicalOrientation = ExpoCameraUtils.physicalOrientation(for: self.deviceOrientation)
self.previewLayer.videoPreviewLayer.connection?.videoOrientation = videoOrientation
}
}
}
private func createBarcodeScanner() -> BarcodeScanner {
let scanner = BarcodeScanner(session: session, sessionQueue: sessionQueue)
scanner.setPreviewLayer(layer: previewLayer.videoPreviewLayer)
scanner.onBarcodeScanned = { [weak self] body in
guard let self else {
return
}
if let body {
self.onBarcodeScanned(body)
}
}
return scanner
}
deinit {
if let photoCapturedPromise {
photoCapturedPromise.reject(CameraUnmountedException())
}
if let errorNotification {
NotificationCenter.default.removeObserver(errorNotification)
}
}
}

View File

@@ -0,0 +1,24 @@
import UIKit
import AVFoundation
class PreviewView: UIView {
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
guard let layer = layer as? AVCaptureVideoPreviewLayer else {
fatalError("Expected `AVCaptureVideoPreviewLayer` type for layer. Check PreviewView.layerClass implementation.")
}
return layer
}
var session: AVCaptureSession? {
get {
return videoPreviewLayer.session
}
set {
videoPreviewLayer.session = newValue
}
}
override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}
}

View File

@@ -0,0 +1,25 @@
import VisionKit
protocol ScannerResultHandler {
func onItemScanned(result: [String: Any])
}
@available(iOS 16.0, *)
class VisionScannerDelegate: NSObject, DataScannerViewControllerDelegate {
private let handler: ScannerResultHandler
init(handler: ScannerResultHandler) {
self.handler = handler
}
func dataScanner(_ dataScanner: DataScannerViewController, didUpdate updatedItems: [RecognizedItem], allItems: [RecognizedItem]) {
if let item = updatedItems.first {
switch item {
case .barcode(let code):
handler.onItemScanned(result: BarcodeScannerUtils.visionDataScannerObjectToDictionary(item: code))
case .text(let text):
return
}
}
}
}

View File

@@ -0,0 +1,34 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'ExpoCamera'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platform = :ios, '13.4'
s.source = { :git => "https://github.com/expo/expo.git" }
s.static_framework = true
s.dependency 'ExpoModulesCore'
s.dependency 'ZXingObjC/PDF417'
s.dependency 'ZXingObjC/OneD'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'GCC_PREPROCESSOR_DEFINITIONS' => 'ZXINGOBJC_USE_SUBSPECS ZXINGOBJC_PDF417 ZXINGOBJC_ONED',
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
s.source_files = "**/*.h"
s.vendored_frameworks = "#{s.name}.xcframework"
else
s.source_files = "**/*.{h,m,swift}"
end
end

View File

@@ -0,0 +1,98 @@
import AVFoundation
import ExpoModulesCore
enum WhiteBalance: Int, Enumerable {
case auto = 0
case sunny = 1
case cloudy = 2
case flash = 3
case shadow = 4
case incandescent = 5
case fluorescent = 6
func temperature() -> Float {
switch self {
case .sunny:
return 5200
case .cloudy:
return 6000
case .shadow:
return 7000
case .incandescent:
return 3000
case .fluorescent:
return 4200
default:
return 5200
}
}
}
enum CameraTypeLegacy: Int, Enumerable {
case front = 0
case back = 1
func toPosition() -> AVCaptureDevice.Position {
switch self {
case .front:
return .front
case .back:
return .back
default:
return .back
}
}
}
enum AutoFocus: Int, Enumerable {
case off = 0
case on = 1
func toAvAutoFocus() -> AVCaptureDevice.FocusMode {
switch self {
case .on:
return .autoFocus
case .off:
return .continuousAutoFocus
default:
return .autoFocus
}
}
}
enum FlashModeLegacy: Int, Enumerable {
case off = 0
case on = 1
case auto = 2
case torch = 3
}
enum VideoCodecLegacy: Int, Enumerable {
case h264 = 0
case hevc = 1
case jpeg = 2
case appleProRes422 = 3
case appleProRes4444 = 4
func codecType() -> AVVideoCodecType {
switch self {
case .h264:
return .h264
case .hevc:
return .hevc
case .jpeg:
return .jpeg
case .appleProRes422:
return .proRes422
case .appleProRes4444:
return .proRes4444
}
}
}
enum VideoStabilizationMode: Int {
case off
case standard
case cinematic
case auto
}

View File

@@ -0,0 +1,10 @@
import ExpoModulesCore
struct CameraRecordingOptionsLegacy: Record {
@Field var maxDuration: Double?
@Field var maxFileSize: Double?
@Field var quality: VideoQuality?
@Field var mute: Bool = false
@Field var mirror: Bool = false
@Field var codec: VideoCodecLegacy?
}

View File

@@ -0,0 +1,975 @@
import UIKit
import ExpoModulesCore
import CoreMotion
// swiftlint:disable:next type_body_length
public class CameraViewLegacy: ExpoView, EXCameraInterface, EXAppLifecycleListener,
AVCaptureFileOutputRecordingDelegate, AVCapturePhotoCaptureDelegate, CameraEvent {
public var session = AVCaptureSession()
public var sessionQueue = DispatchQueue(label: "captureSessionQueue")
private var motionManager = CMMotionManager()
private var physicalOrientation: UIDeviceOrientation = .unknown
// MARK: - Legacy Modules
private var faceDetector: EXFaceDetectorManagerInterface?
private var lifecycleManager: EXAppLifecycleService?
private var barCodeScanner: EXBarCodeScannerInterface?
private var permissionsManager: EXPermissionsInterface?
// MARK: - Properties
private var previewLayer: AVCaptureVideoPreviewLayer?
private var isSessionRunning = false
private var isValidVideoOptions = true
private var videoCodecType: AVVideoCodecType?
private var photoCaptureOptions: TakePictureOptions?
private var videoStabilizationMode: AVCaptureVideoStabilizationMode?
private var errorNotification: NSObjectProtocol?
// MARK: Property Observers
var responsiveWhenOrientationLocked = false {
didSet {
updateResponsiveOrientation()
}
}
var pictureSize = AVCaptureSession.Preset.high {
didSet {
updateSessionPreset(preset: pictureSize)
}
}
var isDetectingFaces = false {
didSet {
if let faceDetector {
faceDetector.setIsEnabled(isDetectingFaces)
} else if isDetectingFaces {
log.error("FaceDetector module not found. Make sure `expo-face-detector` is installed and linked correctly.")
}
}
}
var isScanningBarCodes = false {
didSet {
if let barCodeScanner {
barCodeScanner.setIsEnabled(isScanningBarCodes)
} else if isScanningBarCodes {
log.error("BarCodeScanner module not found. Make sure "
+ "`expo-barcode-scanner` is installed and linked correctly.")
}
}
}
var presetCamera = AVCaptureDevice.Position.back {
didSet {
updateType()
}
}
var autoFocus = AVCaptureDevice.FocusMode.autoFocus {
didSet {
updateFocusMode()
}
}
var whiteBalance = WhiteBalance.auto {
didSet {
updateWhiteBalance()
}
}
var flashMode = FlashModeLegacy.auto {
didSet {
updateFlashMode()
}
}
var zoom: CGFloat = 0 {
didSet {
updateZoom()
}
}
var focusDepth: Float = 0 {
didSet {
updateFocusDepth()
}
}
// MARK: - Session Inputs and Outputs
private var videoFileOutput: AVCaptureMovieFileOutput?
private var photoOutput: AVCapturePhotoOutput?
private var captureDeviceInput: AVCaptureDeviceInput?
// MARK: - Promises
private var photoCapturedPromise: Promise?
private var videoRecordedPromise: Promise?
// MARK: - Events
let onCameraReady = EventDispatcher()
let onMountError = EventDispatcher()
let onPictureSaved = EventDispatcher()
let onBarCodeScanned = EventDispatcher()
let onFacesDetected = EventDispatcher()
let onResponsiveOrientationChanged = EventDispatcher()
private var deviceOrientation: UIInterfaceOrientation {
window?.windowScene?.interfaceOrientation ?? .unknown
}
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
faceDetector = createFaceDetectorManager()
barCodeScanner = createBarCodeScanner()
lifecycleManager = appContext?.legacyModule(implementing: EXAppLifecycleService.self)
permissionsManager = appContext?.legacyModule(implementing: EXPermissionsInterface.self)
#if !targetEnvironment(simulator)
previewLayer = AVCaptureVideoPreviewLayer.init(session: session)
previewLayer?.videoGravity = .resizeAspectFill
previewLayer?.needsDisplayOnBoundsChange = true
barCodeScanner?.setPreviewLayer(previewLayer)
#endif
self.initializeCaptureSessionInput()
self.startSession()
NotificationCenter.default.addObserver(
self,
selector: #selector(orientationChanged(notification:)),
name: UIDevice.orientationDidChangeNotification,
object: nil)
lifecycleManager?.register(self)
motionManager.accelerometerUpdateInterval = 0.2
motionManager.gyroUpdateInterval = 0.2
}
private func updateType() {
sessionQueue.async {
self.initializeCaptureSessionInput()
if !self.session.isRunning {
self.startSession()
}
}
}
public func onAppForegrounded() {
if !session.isRunning && isSessionRunning {
isSessionRunning = false
sessionQueue.async {
self.session.startRunning()
self.ensureSessionConfiguration()
}
}
}
public func onAppBackgrounded() {
if session.isRunning && !isSessionRunning {
isSessionRunning = true
sessionQueue.async {
self.session.stopRunning()
}
}
}
private func updateFlashMode() {
guard let device = captureDeviceInput?.device else {
return
}
if flashMode == .torch {
if !device.hasTorch {
return
}
do {
try device.lockForConfiguration()
if device.hasTorch && device.isTorchModeSupported(.on) {
device.torchMode = .on
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
return
}
} else {
if !device.hasFlash {
return
}
do {
try device.lockForConfiguration()
if device.isTorchModeSupported(.off) {
device.torchMode = .off
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
return
}
}
device.unlockForConfiguration()
}
private func startSession() {
#if targetEnvironment(simulator)
return
#endif
guard let manager = permissionsManager else {
log.info("Permissions module not found.")
return
}
if !manager.hasGrantedPermission(usingRequesterClass: CameraOnlyPermissionRequester.self) {
onMountError(["message": "Camera permissions not granted - component could not be rendered."])
return
}
sessionQueue.async {
if self.presetCamera == .unspecified {
return
}
let photoOutput = AVCapturePhotoOutput()
photoOutput.isLivePhotoCaptureEnabled = false
if self.session.canAddOutput(photoOutput) {
self.session.addOutput(photoOutput)
self.photoOutput = photoOutput
}
self.addErrorNotification()
self.changePreviewOrientation()
self.sessionQueue.asyncAfter(deadline: .now() + round(50 / 1_000_000)) {
self.maybeStartFaceDetection(self.presetCamera != .back)
if let barCodeScanner = self.barCodeScanner {
barCodeScanner.maybeStartBarCodeScanning()
}
self.session.startRunning()
self.ensureSessionConfiguration()
self.onCameraReady()
}
}
}
private func updateZoom() {
guard let device = captureDeviceInput?.device else {
return
}
do {
try device.lockForConfiguration()
device.videoZoomFactor = (device.activeFormat.videoMaxZoomFactor - 1.0) * zoom + 1.0
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
device.unlockForConfiguration()
}
private func updateFocusMode() {
guard let device = captureDeviceInput?.device else {
return
}
do {
try device.lockForConfiguration()
if device.isFocusModeSupported(autoFocus) {
device.focusMode = autoFocus
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
device.unlockForConfiguration()
}
private func updateFocusDepth() {
guard let device = captureDeviceInput?.device, device.focusMode == .locked else {
return
}
if device.isLockingFocusWithCustomLensPositionSupported {
do {
try device.lockForConfiguration()
device.setFocusModeLocked(lensPosition: focusDepth) { _ in
device.unlockForConfiguration()
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
return
}
}
log.info("\(#function): Setting focusDepth isn't supported for this camera device")
}
func updateWhiteBalance() {
guard let device = captureDeviceInput?.device else {
return
}
do {
try device.lockForConfiguration()
if whiteBalance == WhiteBalance.auto {
device.whiteBalanceMode = AVCaptureDevice.WhiteBalanceMode.continuousAutoWhiteBalance
device.unlockForConfiguration()
} else {
if device.isLockingWhiteBalanceWithCustomDeviceGainsSupported {
let rgbGains = device.deviceWhiteBalanceGains(
for: AVCaptureDevice.WhiteBalanceTemperatureAndTintValues(
temperature: whiteBalance.temperature(), tint: 0))
device.setWhiteBalanceModeLocked(with: rgbGains) { _ in
device.unlockForConfiguration()
}
}
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
}
device.unlockForConfiguration()
}
private func addErrorNotification() {
if self.errorNotification != nil {
NotificationCenter.default.removeObserver(self.errorNotification)
}
self.errorNotification = NotificationCenter.default.addObserver(
forName: .AVCaptureSessionRuntimeError,
object: self.session,
queue: nil) { [weak self] notification in
guard let self else {
return
}
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else {
return
}
if error.code == .mediaServicesWereReset {
if self.isSessionRunning {
self.session.startRunning()
self.isSessionRunning = self.session.isRunning
self.ensureSessionConfiguration()
self.onCameraReady()
}
}
}
}
func setBarCodeScannerSettings(settings: [String: Any]) {
if let barCodeScanner {
barCodeScanner.setSettings(settings)
}
}
func updateFaceDetectorSettings(settings: [String: Any]) {
if let faceDetector {
faceDetector.updateSettings(settings)
}
}
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": deviceOrientation.rawValue])
}
}
} else {
motionManager.stopAccelerometerUpdates()
}
}
func takePicture(options: TakePictureOptions, promise: Promise) {
if photoCapturedPromise != nil {
promise.reject(CameraNotReadyException())
return
}
guard let photoOutput else {
promise.reject(CameraOutputNotReadyException())
return
}
photoCapturedPromise = promise
photoCaptureOptions = options
sessionQueue.async {
let connection = photoOutput.connection(with: .video)
let orientation = self.responsiveWhenOrientationLocked ? self.physicalOrientation : UIDevice.current.orientation
connection?.videoOrientation = ExpoCameraUtils.videoOrientation(for: orientation)
let photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
var requestedFlashMode = AVCaptureDevice.FlashMode.off
switch self.flashMode {
case .off:
requestedFlashMode = .off
case .auto:
requestedFlashMode = .auto
case .on, .torch:
requestedFlashMode = .on
}
if photoOutput.supportedFlashModes.contains(requestedFlashMode) {
photoSettings.flashMode = requestedFlashMode
}
if photoOutput.isHighResolutionCaptureEnabled {
photoSettings.isHighResolutionPhotoEnabled = true
}
photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
}
public func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingRawPhoto rawSampleBuffer: CMSampleBuffer?,
previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?,
resolvedSettings: AVCaptureResolvedPhotoSettings,
bracketSettings: AVCaptureBracketedStillImageSettings?,
error: Error?
) {
guard let promise = photoCapturedPromise, let options = photoCaptureOptions else {
return
}
photoCapturedPromise = nil
photoCaptureOptions = nil
guard let rawSampleBuffer, error != nil else {
promise.reject(CameraImageCaptureException())
return
}
guard let imageData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(
forJPEGSampleBuffer: rawSampleBuffer,
previewPhotoSampleBuffer: previewPhotoSampleBuffer),
let sourceImage = CGImageSourceCreateWithData(imageData as CFData, nil),
let metadata = CGImageSourceCopyPropertiesAtIndex(sourceImage, 0, nil) as? [String: Any]
else {
promise.reject(CameraMetadataDecodingException())
return
}
self.handleCapturedImageData(imageData: imageData, metadata: metadata, options: options, promise: promise)
}
public func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
guard let promise = photoCapturedPromise, let options = photoCaptureOptions else {
return
}
photoCapturedPromise = nil
photoCaptureOptions = nil
if error != nil {
promise.reject(CameraImageCaptureException())
return
}
let imageData = photo.fileDataRepresentation()
handleCapturedImageData(
imageData: imageData,
metadata: photo.metadata,
options: options,
promise: promise
)
}
func handleCapturedImageData(
imageData: Data?,
metadata: [String: Any],
options: TakePictureOptions,
promise: Promise
) {
guard let imageData, var takenImage = UIImage(data: imageData) else {
return
}
if options.fastMode {
promise.resolve()
}
let previewSize: CGSize = {
return deviceOrientation == .portrait ?
CGSize(width: previewLayer?.frame.size.height ?? 0.0, height: previewLayer?.frame.size.width ?? 0.0) :
CGSize(width: previewLayer?.frame.size.width ?? 0.0, height: previewLayer?.frame.size.height ?? 0.0)
}()
guard let takenCgImage = takenImage.cgImage else {
return
}
let cropRect = CGRect(x: 0, y: 0, width: takenCgImage.width, height: takenCgImage.height)
let croppedSize = AVMakeRect(aspectRatio: previewSize, insideRect: cropRect)
takenImage = ExpoCameraUtils.crop(image: takenImage, to: croppedSize)
let path = FileSystemUtilities.generatePathInCache(
appContext,
in: "Camera",
extension: ".jpg"
)
if path.isEmpty {
return
}
let width = takenImage.size.width
let height = takenImage.size.height
var processedImageData: Data?
var response = [String: Any]()
if options.exif {
guard let exifDict = metadata[kCGImagePropertyExifDictionary as String] as? NSDictionary else {
return
}
var updatedExif = ExpoCameraUtils.updateExif(
metadata: exifDict,
with: ["Orientation": ExpoCameraUtils.toExifOrientation(orientation: takenImage.imageOrientation)]
)
updatedExif[kCGImagePropertyExifPixelYDimension] = width
updatedExif[kCGImagePropertyExifPixelXDimension] = height
response["exif"] = updatedExif
var updatedMetadata = metadata
if let additionalExif = options.additionalExif {
updatedExif.addEntries(from: additionalExif)
var gpsDict = [String: Any]()
let gpsLatitude = additionalExif["GPSLatitude"] as? Double
if let latitude = gpsLatitude {
gpsDict[kCGImagePropertyGPSLatitude as String] = abs(latitude)
gpsDict[kCGImagePropertyGPSLatitudeRef as String] = latitude >= 0 ? "N" : "S"
}
let gpsLongitude = additionalExif["GPSLongitude"] as? Double
if let longitude = gpsLongitude {
gpsDict[kCGImagePropertyGPSLongitude as String] = abs(longitude)
gpsDict[kCGImagePropertyGPSLongitudeRef as String] = longitude >= 0 ? "E" : "W"
}
let gpsAltitude = additionalExif["GPSAltitude"] as? Double
if let altitude = gpsAltitude {
gpsDict[kCGImagePropertyGPSAltitude as String] = abs(altitude)
gpsDict[kCGImagePropertyGPSAltitudeRef as String] = altitude >= 0 ? 0 : 1
}
let metadataGpsDict = updatedMetadata[kCGImagePropertyGPSDictionary as String] as? [String: Any]
if updatedMetadata[kCGImagePropertyGPSDictionary as String] == nil {
updatedMetadata[kCGImagePropertyGPSDictionary as String] = gpsDict
} else {
if var metadataGpsDict = updatedMetadata[kCGImagePropertyGPSDictionary as String] as? NSMutableDictionary {
metadataGpsDict.addEntries(from: gpsDict)
}
}
}
updatedMetadata[kCGImagePropertyExifDictionary as String] = updatedExif
processedImageData = ExpoCameraUtils.data(
from: takenImage,
with: updatedMetadata,
quality: Float(options.quality))
} else {
processedImageData = takenImage.jpegData(compressionQuality: options.quality)
}
guard let processedImageData else {
promise.reject(CameraSavingImageException())
return
}
response["uri"] = ExpoCameraUtils.write(data: processedImageData, to: path)
response["width"] = width
response["height"] = height
if options.base64 {
response["base64"] = processedImageData.base64EncodedString()
}
if options.fastMode {
onPictureSaved(["data": response, "id": options.id])
} else {
promise.resolve(response)
}
}
func record(options: CameraRecordingOptionsLegacy, promise: Promise) {
if videoFileOutput == nil {
if let faceDetector {
faceDetector.stopFaceDetection()
}
setupMovieFileCapture()
}
if let videoFileOutput, !videoFileOutput.isRecording && videoRecordedPromise == nil {
updateSessionAudioIsMuted(options.mute)
if let connection = videoFileOutput.connection(with: .video) {
if !connection.isVideoStabilizationSupported {
log.warn("\(#function): Video Stabilization is not supported on this device.")
} else {
if let videoStabilizationMode {
connection.preferredVideoStabilizationMode = videoStabilizationMode
}
}
let orientation = self.responsiveWhenOrientationLocked ? self.physicalOrientation : UIDevice.current.orientation
connection.videoOrientation = ExpoCameraUtils.videoOrientation(for: orientation)
setVideoOptions(options: options, for: connection, promise: promise)
if connection.isVideoOrientationSupported && options.mirror {
connection.isVideoMirrored = options.mirror
}
}
let preset = options.quality?.toPreset() ?? .high
updateSessionPreset(preset: preset)
if !self.isValidVideoOptions {
return
}
sessionQueue.async {
let path = FileSystemUtilities.generatePathInCache(self.appContext, in: "Camera", extension: ".mov")
let fileUrl = URL(fileURLWithPath: path)
self.videoRecordedPromise = promise
videoFileOutput.startRecording(to: fileUrl, recordingDelegate: self)
}
}
}
func setVideoOptions(options: CameraRecordingOptionsLegacy, for connection: AVCaptureConnection, promise: Promise) {
sessionQueue.async {
self.isValidVideoOptions = true
guard let movieFileOutput = self.videoFileOutput else {
return
}
if let maxDuration = options.maxDuration {
self.videoFileOutput?.maxRecordedDuration = CMTime(seconds: maxDuration, preferredTimescale: 30)
}
if let maxFileSize = options.maxFileSize {
self.videoFileOutput?.maxRecordedFileSize = Int64(maxFileSize)
}
if let codec = options.codec {
let codecType = codec.codecType()
if movieFileOutput.availableVideoCodecTypes.contains(codecType) {
movieFileOutput.setOutputSettings([AVVideoCodecKey: codecType], for: connection)
self.videoCodecType = codecType
} else {
promise.reject(CameraRecordingException(self.videoCodecType?.rawValue))
self.cleanupMovieFileCapture()
self.videoRecordedPromise = nil
self.isValidVideoOptions = false
}
}
}
}
func updateSessionAudioIsMuted(_ isMuted: Bool) {
sessionQueue.async {
self.session.beginConfiguration()
for input in self.session.inputs {
if let deviceInput = input as? AVCaptureDeviceInput {
if deviceInput.device.hasMediaType(.audio) {
if isMuted {
self.session.removeInput(input)
}
return
}
}
}
if !isMuted {
if let audioCapturedevice = AVCaptureDevice.default(for: .audio) {
do {
let audioDeviceInput = try AVCaptureDeviceInput(device: audioCapturedevice)
if self.session.canAddInput(audioDeviceInput) {
self.session.addInput(audioDeviceInput)
}
} catch {
log.info("\(#function): \(error.localizedDescription)")
self.session.commitConfiguration()
return
}
}
}
self.session.commitConfiguration()
}
}
func setupMovieFileCapture() {
let output = AVCaptureMovieFileOutput()
if self.session.canAddOutput(output) {
self.session.addOutput(output)
self.videoFileOutput = output
}
}
func cleanupMovieFileCapture() {
if let videoFileOutput {
if session.outputs.contains(videoFileOutput) {
session.removeOutput(videoFileOutput)
self.videoFileOutput = nil
}
}
}
public override func layoutSubviews() {
previewLayer?.frame = self.bounds
self.backgroundColor = .black
if let previewLayer {
self.layer.insertSublayer(previewLayer, at: 0)
}
}
public override func removeFromSuperview() {
lifecycleManager?.unregisterAppLifecycleListener(self)
self.stopSession()
super.removeFromSuperview()
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}
func ensureSessionConfiguration() {
sessionQueue.async {
self.updateFlashMode()
}
}
public func fileOutput(
_ output: AVCaptureFileOutput,
didFinishRecordingTo outputFileURL: URL,
from connections: [AVCaptureConnection],
error: Error?
) {
var success = true
if error != nil {
let value = (error as? NSError)?.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as? Bool
success = value == true ? true : false
}
if success && videoRecordedPromise != nil {
videoRecordedPromise?.resolve(["uri": outputFileURL.absoluteString])
} else if videoRecordedPromise != nil {
videoRecordedPromise?.reject(CameraRecordingFailedException())
}
videoRecordedPromise = nil
videoCodecType = nil
cleanupMovieFileCapture()
maybeStartFaceDetection(false)
if session.sessionPreset != pictureSize {
updateSessionPreset(preset: pictureSize)
}
}
func maybeStartFaceDetection(_ mirrored: Bool) {
guard let faceDetector else {
return
}
let connection = photoOutput?.connection(with: .video)
connection?.videoOrientation = ExpoCameraUtils.videoOrientation(for: UIDevice.current.orientation)
faceDetector.maybeStartFaceDetection(on: session, with: previewLayer, mirrored: mirrored)
}
func setPresetCamera(presetCamera: AVCaptureDevice.Position) {
self.presetCamera = presetCamera
faceDetector?.updateMirrored(presetCamera != .back)
}
func stopRecording() {
videoFileOutput?.stopRecording()
}
func resumePreview() {
previewLayer?.connection?.isEnabled = true
}
func pausePreview() {
previewLayer?.connection?.isEnabled = false
}
func updateSessionPreset(preset: AVCaptureSession.Preset) {
#if !targetEnvironment(simulator)
sessionQueue.async {
self.session.beginConfiguration()
if self.session.canSetSessionPreset(preset) {
self.session.sessionPreset = preset
}
self.session.commitConfiguration()
}
#endif
}
func initializeCaptureSessionInput() {
if captureDeviceInput?.device.position == presetCamera {
return
}
EXUtilities.performSynchronously {
var orientation: AVCaptureVideoOrientation = .portrait
if self.deviceOrientation != .unknown {
if let videoOrientation = AVCaptureVideoOrientation(rawValue: self.deviceOrientation.rawValue) {
orientation = videoOrientation
}
}
self.previewLayer?.connection?.videoOrientation = orientation
}
sessionQueue.async {
self.session.beginConfiguration()
guard let device = ExpoCameraUtils.device(with: .video, preferring: self.presetCamera) else {
return
}
if let videoCaptureDeviceInput = self.captureDeviceInput {
self.session.removeInput(videoCaptureDeviceInput)
}
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: device)
if self.session.canAddInput(captureDeviceInput) {
self.session.addInput(captureDeviceInput)
self.captureDeviceInput = captureDeviceInput
self.updateZoom()
self.updateFocusMode()
self.updateFocusDepth()
self.updateWhiteBalance()
}
} catch {
self.onMountError(["message": "Camera could not be started - \(error.localizedDescription)"])
}
self.session.commitConfiguration()
}
}
private func stopSession() {
#if targetEnvironment(simulator)
return
#endif
sessionQueue.async {
if let faceDetector = self.faceDetector {
faceDetector.stopFaceDetection()
}
if let barCodeScanner = self.barCodeScanner {
barCodeScanner.stopBarCodeScanning()
}
self.session.beginConfiguration()
self.motionManager.stopAccelerometerUpdates()
self.previewLayer?.removeFromSuperlayer()
for input in self.session.inputs {
self.session.removeInput(input)
}
for output in self.session.outputs {
self.session.removeOutput(output)
}
self.session.commitConfiguration()
self.session.stopRunning()
}
}
@objc func orientationChanged(notification: Notification) {
changePreviewOrientation()
}
func changePreviewOrientation() {
EXUtilities.performSynchronously {
let videoOrientation = ExpoCameraUtils.videoOrientation(for: self.deviceOrientation)
if (self.previewLayer?.connection?.isVideoOrientationSupported) == true {
self.previewLayer?.connection?.videoOrientation = videoOrientation
}
}
}
private func createFaceDetectorManager() -> EXFaceDetectorManagerInterface? {
let provider: EXFaceDetectorManagerProviderInterface? =
appContext?.legacyModule(implementing: EXFaceDetectorManagerProviderInterface.self)
guard let faceDetector = provider?.createFaceDetectorManager() else {
return nil
}
faceDetector.setOnFacesDetected { [weak self] faces in
guard let self else {
return
}
self.onFacesDetected([
"type": "face",
"faces": faces
])
}
faceDetector.setSessionQueue(sessionQueue)
return faceDetector
}
private func createBarCodeScanner() -> EXBarCodeScannerInterface? {
guard let barCodeScnnerProvider: EXBarCodeScannerProviderInterface? =
appContext?.legacyModule(implementing: EXBarCodeScannerProviderInterface.self) else {
return nil
}
guard let scanner = barCodeScnnerProvider?.createBarCodeScanner() else {
return nil
}
scanner.setSession(session)
scanner.setSessionQueue(sessionQueue)
scanner.setOnBarCodeScanned { [weak self] body in
if let body = body as? [String: Any] {
self?.onBarCodeScanned(body)
}
}
return scanner
}
deinit {
if let photoCapturedPromise {
photoCapturedPromise.reject(CameraUnmountedException())
}
if let errorNotification {
NotificationCenter.default.removeObserver(errorNotification)
}
}
}

View File

@@ -0,0 +1,49 @@
import ExpoModulesCore
protocol CameraEvent {
var onPictureSaved: EventDispatcher { get }
}
func takePictureForSimulator(
_ appContext: AppContext?,
_ event: CameraEvent,
_ options: TakePictureOptions,
_ promise: Promise
) throws {
if options.fastMode {
promise.resolve()
}
let result = try generatePictureForSimulator(appContext: appContext, options: options)
if options.fastMode {
event.onPictureSaved([
"data": result,
"id": options.id
])
} else {
promise.resolve(result)
}
}
func generatePictureForSimulator(
appContext: AppContext?,
options: TakePictureOptions
) throws -> [String: Any?] {
let path = FileSystemUtilities.generatePathInCache(
appContext,
in: "Camera",
extension: ".jpg"
)
let generatedPhoto = ExpoCameraUtils.generatePhoto(of: CGSize(width: 200, height: 200))
guard let photoData = generatedPhoto.jpegData(compressionQuality: options.quality) else {
throw CameraInvalidPhotoData()
}
return [
"uri": ExpoCameraUtils.write(data: photoData, to: path),
"width": generatedPhoto.size.width,
"height": generatedPhoto.size.height,
"base64": options.base64 ? photoData.base64EncodedString() : nil
]
}