222 lines
7.3 KiB
Swift
222 lines
7.3 KiB
Swift
//
|
|
// SimulatorCamera.swift
|
|
// ReactNativeCameraKit
|
|
//
|
|
|
|
import AVFoundation
|
|
import React
|
|
import UIKit
|
|
|
|
/*
|
|
* Fake camera implementation to be used on simulator
|
|
*/
|
|
class SimulatorCamera: CameraProtocol {
|
|
private var onOrientationChange: RCTDirectEventBlock?
|
|
private var onZoom: RCTDirectEventBlock?
|
|
private var videoDeviceZoomFactor: Double = 1.0
|
|
private var videoDeviceMaxAvailableVideoZoomFactor: Double = 150.0
|
|
private var wideAngleZoomFactor: Double = 2.0
|
|
private var zoom: Double?
|
|
private var maxZoom: Double?
|
|
private var resizeMode: ResizeMode = .contain
|
|
private var barcodeFrameSize: CGSize?
|
|
|
|
var previewView: UIView { mockPreview }
|
|
|
|
private var fakeFocusFinishedTimer: Timer?
|
|
|
|
// Create mock camera layer. When a photo is taken, we capture this layer and save it in place of a hardware input.
|
|
private let mockPreview = SimulatorPreviewView(frame: .zero)
|
|
|
|
// MARK: - Public
|
|
|
|
func setup(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) {
|
|
DispatchQueue.main.async {
|
|
self.mockPreview.cameraTypeLabel.text = "Camera type: \(cameraType)"
|
|
}
|
|
|
|
// Listen to orientation changes
|
|
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
|
NotificationCenter.default.addObserver(
|
|
forName: UIDevice.orientationDidChangeNotification,
|
|
object: UIDevice.current,
|
|
queue: nil,
|
|
using: { [weak self] notification in
|
|
self?.orientationChanged(notification: notification)
|
|
})
|
|
|
|
}
|
|
|
|
private func orientationChanged(notification: Notification) {
|
|
guard let device = notification.object as? UIDevice,
|
|
let orientation = Orientation(from: device.orientation)
|
|
else {
|
|
return
|
|
}
|
|
|
|
self.onOrientationChange?(["orientation": orientation.rawValue])
|
|
}
|
|
|
|
func cameraRemovedFromSuperview() {
|
|
NotificationCenter.default.removeObserver(
|
|
self, name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
|
|
|
|
}
|
|
|
|
func update(onOrientationChange: RCTDirectEventBlock?) {
|
|
self.onOrientationChange = onOrientationChange
|
|
}
|
|
|
|
func update(onZoom: RCTDirectEventBlock?) {
|
|
self.onZoom = onZoom
|
|
}
|
|
|
|
func update(iOsDeferredStartEnabled: Bool?) {
|
|
// Not applicable on simulator; deferred start only matters for real capture outputs.
|
|
}
|
|
|
|
func setVideoDevice(zoomFactor: Double) {
|
|
self.videoDeviceZoomFactor = zoomFactor
|
|
self.mockPreview.zoomLabel.text = "Zoom: \(zoomFactor)"
|
|
}
|
|
|
|
private var zoomStartedAt: Double = 1.0
|
|
func zoomPinchStart() {
|
|
DispatchQueue.main.async {
|
|
self.zoomStartedAt = self.videoDeviceZoomFactor
|
|
self.mockPreview.zoomLabel.text = "Zoom start"
|
|
}
|
|
}
|
|
|
|
func zoomPinchChange(pinchScale: CGFloat) {
|
|
guard !pinchScale.isNaN else { return }
|
|
|
|
DispatchQueue.main.async {
|
|
let desiredZoomFactor = self.zoomStartedAt * pinchScale
|
|
var maxZoomFactor = self.videoDeviceMaxAvailableVideoZoomFactor
|
|
if let maxZoom = self.maxZoom {
|
|
maxZoomFactor = min(maxZoom, maxZoomFactor)
|
|
}
|
|
let zoomForDevice = max(1.0, min(desiredZoomFactor, maxZoomFactor))
|
|
|
|
if zoomForDevice != self.videoDeviceZoomFactor {
|
|
// Only trigger zoom changes if it's an uncontrolled component (zoom isn't manually set)
|
|
// otherwise it's likely to cause issues inf. loops
|
|
if self.zoom == nil {
|
|
self.setVideoDevice(zoomFactor: zoomForDevice)
|
|
}
|
|
self.onZoom?(["zoom": zoomForDevice])
|
|
}
|
|
}
|
|
}
|
|
|
|
func focus(at: CGPoint, focusBehavior: FocusBehavior) {
|
|
DispatchQueue.main.async {
|
|
self.mockPreview.focusAtLabel.text =
|
|
"Focus at: (\(Int(at.x)), \(Int(at.y))), focusMode: \(focusBehavior.avFocusMode)"
|
|
}
|
|
|
|
// Fake focus finish after a second
|
|
fakeFocusFinishedTimer?.invalidate()
|
|
if case .customFocus(_, _, let focusFinished) = focusBehavior {
|
|
fakeFocusFinishedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) {
|
|
_ in
|
|
focusFinished()
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(torchMode: TorchMode) {
|
|
DispatchQueue.main.async {
|
|
self.mockPreview.torchModeLabel.text = "Torch mode: \(torchMode)"
|
|
}
|
|
}
|
|
|
|
func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
|
|
}
|
|
|
|
func update(flashMode: FlashMode) {
|
|
DispatchQueue.main.async {
|
|
self.mockPreview.flashModeLabel.text = "Flash mode: \(flashMode)"
|
|
}
|
|
}
|
|
|
|
func update(cameraType: CameraType) {
|
|
DispatchQueue.main.async {
|
|
self.mockPreview.cameraTypeLabel.text = "Camera type: \(cameraType)"
|
|
self.mockPreview.randomize()
|
|
}
|
|
}
|
|
|
|
func update(maxZoom: Double?) {
|
|
self.maxZoom = maxZoom
|
|
}
|
|
|
|
func update(zoom: Double?) {
|
|
self.zoom = zoom
|
|
|
|
DispatchQueue.main.async {
|
|
var zoomOrDefault = zoom ?? 0
|
|
// -1 will reset to zoom default (which is not 1 on modern cameras)
|
|
if zoomOrDefault == 0 {
|
|
zoomOrDefault = self.wideAngleZoomFactor
|
|
}
|
|
|
|
var maxZoomFactor = self.videoDeviceMaxAvailableVideoZoomFactor
|
|
if let maxZoom = self.maxZoom {
|
|
maxZoomFactor = min(maxZoom, maxZoomFactor)
|
|
}
|
|
let zoomForDevice = max(1.0, min(zoomOrDefault, maxZoomFactor))
|
|
self.setVideoDevice(zoomFactor: zoomForDevice)
|
|
|
|
// If they wanted to reset, tell them what the default zoom turned out to be
|
|
// regardless if it's controlled
|
|
if self.zoom == nil || zoom == 0 {
|
|
self.onZoom?(["zoom": zoomForDevice])
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(resizeMode: ResizeMode) {
|
|
DispatchQueue.main.async {
|
|
self.mockPreview.resizeModeLabel.text = "Resize mode: \(resizeMode)"
|
|
self.mockPreview.randomize()
|
|
}
|
|
}
|
|
|
|
func isBarcodeScannerEnabled(
|
|
_ isEnabled: Bool,
|
|
supportedBarcodeTypes: [CodeFormat],
|
|
onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?
|
|
) {}
|
|
func update(scannerFrameSize: CGRect?) {}
|
|
|
|
func capturePicture(
|
|
onWillCapture: @escaping () -> Void,
|
|
onSuccess:
|
|
@escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions)
|
|
-> Void,
|
|
onError: @escaping (_ message: String) -> Void
|
|
) {
|
|
onWillCapture()
|
|
|
|
DispatchQueue.main.async {
|
|
// Generate snapshot from main UI thread
|
|
let previewSnapshot = self.mockPreview.snapshot(withTimestamp: true)
|
|
|
|
// Then switch to background thread
|
|
DispatchQueue.global(qos: .default).async {
|
|
if let imageData = previewSnapshot?.jpegData(compressionQuality: 0.85) {
|
|
onSuccess(imageData, nil, CMVideoDimensions(width: 480, height: 640))
|
|
} else {
|
|
onError("Failed to convert snapshot to JPEG data")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(barcodeFrameSize: CGSize?) {
|
|
self.barcodeFrameSize = barcodeFrameSize
|
|
}
|
|
}
|