Files
Schnappix/node_modules/expo-camera/ios/CameraViewModule.swift
T

441 lines
13 KiB
Swift

// Copyright 2022-present 650 Industries. All rights reserved.
import AVFoundation
import ExpoModulesCore
import VisionKit
let cameraEvents = ["onCameraReady", "onMountError", "onPictureSaved", "onBarcodeScanned", "onResponsiveOrientationChanged", "onAvailableLensesChanged"]
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
}
Property("toggleRecordingAsyncAvailable") { () -> Bool in
if #available(iOS 18.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()
}
// the prop has been removed, reset to default
if type == nil && view.presetCamera != .back {
view.presetCamera = .back
}
}
Prop("flashMode") { (view, flashMode: FlashMode?) in
if let flashMode, view.flashMode != flashMode {
view.flashMode = flashMode
}
if flashMode == nil && view.flashMode != .auto {
view.flashMode = .auto
}
}
Prop("selectedLens") { (view, selectedLens: String?) in
if let selectedLens, view.selectedLens != selectedLens {
view.selectedLens = selectedLens
}
if selectedLens == nil && view.selectedLens != nil {
view.selectedLens = nil
}
}
Prop("enableTorch") { (view, enabled: Bool?) in
if let enabled, view.torchEnabled != enabled {
view.torchEnabled = enabled
return
}
if enabled == nil && view.torchEnabled {
view.torchEnabled = false
}
}
Prop("pictureSize") { (view, pictureSize: PictureSize?) in
if let pictureSize, view.pictureSize != pictureSize {
view.pictureSize = pictureSize
return
}
if pictureSize == nil && view.pictureSize != .high {
view.pictureSize = .high
}
}
Prop("zoom") { (view, zoom: Double?) in
if let zoom, fabs(view.zoom - zoom) > Double.ulpOfOne {
view.zoom = zoom
return
}
if zoom == nil && view.zoom != 0 {
view.zoom = 0
}
}
Prop("mode") { (view, mode: CameraMode?) in
if let mode, view.mode != mode {
view.mode = mode
}
if mode == nil && view.mode != .picture {
view.mode = .picture
}
}
Prop("barcodeScannerEnabled") { (view, scanBarcodes: Bool?) in
if let scanBarcodes, view.isScanningBarcodes != scanBarcodes {
view.isScanningBarcodes = scanBarcodes
return
}
if scanBarcodes == nil && view.isScanningBarcodes != false {
view.isScanningBarcodes = false
}
}
Prop("barcodeScannerSettings") { (view, settings: BarcodeSettings?) in
if let settings {
view.setBarcodeScannerSettings(settings: settings)
}
}
Prop("mute") { (view, muted: Bool?) in
if let muted, view.isMuted != muted {
view.isMuted = muted
return
}
if muted == nil && view.isMuted != false {
view.isMuted = false
}
}
Prop("animateShutter") { (view, animate: Bool?) in
// Does not call anything that immediately causes any configuration changes
// so it's fine to just set it
view.animateShutter = animate ?? true
}
Prop("videoQuality") { (view, quality: VideoQuality?) in
if let quality, view.videoQuality != quality {
view.videoQuality = quality
return
}
if quality == nil && view.videoQuality != .video1080p {
view.videoQuality = .video1080p
}
}
Prop("autoFocus") { (view, focusMode: FocusMode?) in
if let focusMode, view.autoFocus != focusMode.toAVCaptureFocusMode() {
view.autoFocus = focusMode.toAVCaptureFocusMode()
return
}
if focusMode == nil && view.autoFocus != .continuousAutoFocus {
view.autoFocus = .continuousAutoFocus
}
}
Prop("responsiveOrientationWhenOrientationLocked") { (view, responsiveOrientation: Bool?) in
if let responsiveOrientation, view.responsiveWhenOrientationLocked != responsiveOrientation {
view.responsiveWhenOrientationLocked = responsiveOrientation
return
}
if responsiveOrientation == nil && view.responsiveWhenOrientationLocked != false {
view.responsiveWhenOrientationLocked = false
}
}
Prop("mirror") { (view, mirror: Bool?) in
// Does not call anything that immediately causes any configuration changes
// so it's fine to just set it
view.mirror = mirror ?? false
}
Prop("active") { (view, active: Bool?) in
if let active, view.active != active {
view.active = active
return
}
if active == nil && view.active != true {
view.active = true
}
}
Prop("videoBitrate") { (view, bitrate: Int?) in
// Does not call anything that immediately causes any configuration changes
// so it's fine to just set it
view.videoBitrate = bitrate
}
AsyncFunction("resumePreview") { view in
view.resumePreview()
}
AsyncFunction("pausePreview") { view in
view.pausePreview()
}
AsyncFunction("getAvailablePictureSizes") {
return PictureSize.allCases.map {
$0.rawValue
}
}
AsyncFunction("getAvailableLenses") { view in
view.getAvailableLenses()
}
AsyncFunction("takePictureRef") { (view, options: TakePictureOptions) -> PictureRef in
#if targetEnvironment(simulator)
return try takePictureRefForSimulator(self.appContext, view, options)
#else
return try await view.takePictureRef(options: options)
#endif
}
AsyncFunction("takePicture") { (view, options: TakePictureOptions, promise: Promise) in
#if targetEnvironment(simulator) // simulator
try takePictureForSimulator(self.appContext, view, options, promise)
#else
Task {
do {
let result = try await view.takePicturePromise(options: options)
promise.resolve(result)
} catch {
promise.reject(error)
}
}
#endif
}
AsyncFunction("record") { (view, options: CameraRecordingOptions, promise: Promise) in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
Task {
await view.record(options: options, promise: promise)
}
#endif
}
AsyncFunction("toggleRecording") { view in
if #available(iOS 18.0, *) {
view.toggleRecording()
} else {
throw CameraToggleRecordingException()
}
}
AsyncFunction("stopRecording") { view in
#if targetEnvironment(simulator)
throw Exceptions.SimulatorNotSupported()
#else
view.stopRecording()
#endif
}
}
Class("Picture", PictureRef.self) {
Property("width") { (image: PictureRef) -> Int in
return image.ref.cgImage?.width ?? 0
}
Property("height") { (image: PictureRef) -> Int in
return image.ref.cgImage?.height ?? 0
}
AsyncFunction("savePictureAsync") { (image: PictureRef, options: SavePictureOptions?) -> [String: Any?] in
guard let appContext else {
throw Exceptions.AppContextLost()
}
let options = options ?? SavePictureOptions()
return try ExpoCameraUtils.saveImage(image.ref, options: options, appContext: appContext)
}
}
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() ?? []
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 }
}
}