Skip to content

Instantly share code, notes, and snippets.

@niaeashes
Last active January 18, 2022 08:58
Show Gist options
  • Save niaeashes/548f0274c720e4d54f81a37e61c0a591 to your computer and use it in GitHub Desktop.
Save niaeashes/548f0274c720e4d54f81a37e61c0a591 to your computer and use it in GitHub Desktop.
iOS Camera Implementation
//
// Camera.swift
// CameraTest
//
import UIKit
import CoreImage
import VideoToolbox
import AVFoundation
import CoreMotion
import CoreLocation
extension CGImage {
static func create(pixelBuffer: CVPixelBuffer) -> CGImage? {
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
return cgImage
}
}
struct CapturePhoto {
var image: UIImage
var location: CLLocationCoordinate2D?
var timestamp: Date?
}
final class Camera: NSObject, ObservableObject {
enum CameraState: String {
case suspended
case setup
case active
case accessDenied
case accessRestricted
case enableDeviceNotFound
}
private let session = AVCaptureSession()
private let location = CLLocationManager()
private let motion = CMMotionManager()
private var currentLocation: CLLocation?
private var currentOrientation: UIDeviceOrientation? {
willSet {
if newValue != currentOrientation {
objectWillChange.send()
}
}
}
@Published var state: CameraState = .suspended
@Published var isEnableFlash = false
@Published var flashMode: AVCaptureDevice.FlashMode = .auto {
didSet {
output?.setPreparedPhotoSettingsArray([currentPreviewSetting])
}
}
@Published var resolution: CGSize = .zero
@Published var previewLayer: AVCaptureVideoPreviewLayer? = nil
private var input: AVCaptureInput? = nil {
willSet {
guard let input = input else { return }
session.removeInput(input)
}
didSet {
guard let input = input, session.canAddInput(input) else { return assertionFailure() }
session.addInput(input)
}
}
private var output: AVCapturePhotoOutput? = nil {
willSet {
guard let output = output else { return }
session.removeOutput(output)
}
didSet {
guard let output = output, session.canAddOutput(output) else { return assertionFailure() }
session.addOutput(output)
}
}
var device: AVCaptureDevice? {
(input as? AVCaptureDeviceInput)?.device
}
// Life-cycle methods: setup -> start -> stop -> suspend
func setup() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
state = .setup
setupSession()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
DispatchQueue.main.async {
self.state = .setup
self.setupSession()
}
} else {
DispatchQueue.main.async {
self.state = .accessDenied
}
}
}
case .denied:
state = .accessDenied
case .restricted:
state = .accessRestricted
@unknown default:
break
}
}
func suspend() {
stop()
output = nil
input = nil
previewLayer = nil
state = .suspended
}
func start() {
guard state == .active else { return }
DispatchQueue.main.async {
self.session.startRunning()
self.startUpdateingAcceleration()
self.startUpdatingLocation()
}
}
func stop() {
guard session.isRunning else { return }
session.stopRunning()
motion.stopAccelerometerUpdates()
location.stopUpdatingLocation()
}
var targetDevice: AVCaptureDevice? {
if let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
return device
}
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
return device
}
assertionFailure("Target capture device is not exists.")
return nil
}
var captureCallback: (CapturePhoto) -> Void = { _ in }
func capturePhoto(handler: @escaping (CapturePhoto) -> Void) {
guard let output = output else {
return assertionFailure("current output is none. (Camera.output == nil)")
}
output.capturePhoto(with: currentCaptureSetting, delegate: self)
captureCallback = handler
}
override var debugDescription: String {
"Orientation: \((currentOrientation ?? .unknown).debugDescription)"
}
}
// MARK: Start Steps.
extension Camera {
private func startUpdateingAcceleration() {
guard motion.isAccelerometerAvailable else {
#if DEBUG
print("[DEBUG] CoreMotion Accelerometer is not available.")
#endif
return
}
motion.startAccelerometerUpdates(to: .main) { data, error in
if let error = error {
assertionFailure(error.localizedDescription)
}
if let acceleration = data?.acceleration {
self.updateOrientation(acceleration: acceleration)
}
}
}
private func startUpdatingLocation() {
if CLLocationManager.locationServicesEnabled() {
location.startUpdatingLocation()
} else {
location.stopUpdatingLocation()
}
}
}
// MARK: Setup Steps.
extension Camera {
private func setupMotionManager() {
motion.accelerometerUpdateInterval = 1 / 10
}
private func setupLocationManager() {
location.delegate = self
}
// Call from setup open method after get permission to use device camera.
private func setupSession() {
guard let device = targetDevice else {
state = .enableDeviceNotFound
return
}
// Start request using location service when in use authorization.
location.requestWhenInUseAuthorization()
session.beginConfiguration()
session.sessionPreset = .photo
do {
input = try AVCaptureDeviceInput(device: device)
} catch {
print(error)
}
output = AVCapturePhotoOutput()
output?.isHighResolutionCaptureEnabled = true
output?.setPreparedPhotoSettingsArray([currentPreviewSetting])
previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer?.videoGravity = .resizeAspect
previewLayer?.connection?.videoOrientation = .portrait
do {
let formatDescription = device.activeFormat.formatDescription
let dimensions = CMVideoFormatDescriptionGetDimensions(formatDescription)
// [!] dimensions is always landscape, but use as portrait.
resolution = CGSize(width: CGFloat(dimensions.height), height: CGFloat(dimensions.width))
}
session.commitConfiguration()
state = .active
}
}
// MARK: - Private utilities.
extension Camera {
private var currentPreviewSetting: AVCapturePhotoSettings {
let settings = AVCapturePhotoSettings()
settings.livePhotoVideoCodecType = .jpeg
if device?.hasFlash == true {
settings.flashMode = flashMode
}
return settings
}
private var currentCaptureSetting: AVCapturePhotoSettings {
let settings = AVCapturePhotoSettings()
settings.isHighResolutionPhotoEnabled = true
if let quality = output?.maxPhotoQualityPrioritization {
settings.photoQualityPrioritization = quality
}
if device?.hasFlash == true {
settings.flashMode = flashMode
}
return settings
}
private func updateOrientation(acceleration: CMAcceleration) {
let threashold = 0.5
if acceleration.x > threashold {
currentOrientation = .landscapeRight
return
}
if acceleration.x < -threashold {
currentOrientation = .landscapeLeft
return
}
if acceleration.y > threashold {
currentOrientation = .portraitUpsideDown
return
}
if acceleration.y < -threashold {
currentOrientation = .portrait
return
}
if acceleration.z > threashold {
currentOrientation = .faceUp
return
}
if acceleration.z < -threashold {
currentOrientation = .faceDown
return
}
currentOrientation = .unknown
}
}
// MARK: - Camera as Delegates.
extension Camera: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
currentLocation = location
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
assertionFailure(error.localizedDescription)
}
}
extension Camera: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let cgImage = photo.cgImageRepresentation() else { fatalError() }
/// https://stackoverflow.com/questions/46852521/how-to-generate-an-uiimage-from-avcapturephoto-with-correct-orientation
var imageOrientation: UIImage.Orientation = .up
switch (photo[kCGImagePropertyOrientation] as? UInt32).map({ CGImagePropertyOrientation(rawValue: $0) }) ?? .up {
case .up:
imageOrientation = .up
case .upMirrored:
imageOrientation = .upMirrored
case .down:
imageOrientation = .down
case .downMirrored:
imageOrientation = .downMirrored
case .right:
imageOrientation = .right
case .rightMirrored:
imageOrientation = .rightMirrored
case .left:
imageOrientation = .left
case .leftMirrored:
imageOrientation = .leftMirrored
case .none:
break
}
let isFront = device?.position == .front
switch currentOrientation {
case .some(.landscapeLeft):
imageOrientation = isFront ? imageOrientation.rotateRight() : imageOrientation.rotateLeft()
case .some(.landscapeRight):
imageOrientation = isFront ? imageOrientation.rotateLeft() : imageOrientation.rotateRight()
case .some(.portraitUpsideDown):
imageOrientation = imageOrientation.rotate180()
default:
break
}
let image = UIImage(cgImage: cgImage, scale: 1.0, orientation: imageOrientation)
var capturePhoto = CapturePhoto(image: image, location: nil, timestamp: Date())
if let location = currentLocation, location.timestamp.timeIntervalSinceNow < 180 {
capturePhoto.location = location.coordinate
}
captureCallback(capturePhoto)
}
}
// MARK: - Utilities.
extension AVCapturePhoto {
subscript(_ metakey: CFString) -> Any? {
metadata[String(metakey)]
}
}
extension UIImage.Orientation {
var isMirror: Bool { rawValue >= 4 }
func rotateLeft() -> Self {
guard let orientation = Self(rawValue: rawValue % 4) else { fatalError() }
switch orientation {
case .up:
return Self(rawValue: 2 + (isMirror ? 4 : 0))! // 2 = left
case .down:
return Self(rawValue: 3 + (isMirror ? 4 : 0))! // 3 = right
case .left:
return Self(rawValue: 1 + (isMirror ? 4 : 0))! // 1 = down
case .right:
return Self(rawValue: 0 + (isMirror ? 4 : 0))! // 0 = up
default:
fatalError()
}
}
func rotateRight() -> Self {
rotateLeft().rotateLeft().rotateLeft()
}
func rotate180() -> Self {
rotateLeft().rotateLeft()
}
}
extension UIDeviceOrientation: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .portrait:
return "portrait"
case .portraitUpsideDown:
return "portraitUpsideDown"
case .landscapeLeft:
return "landscapeLeft"
case .landscapeRight:
return "landscapeRight"
case .faceUp:
return "faceUp"
case .faceDown:
return "faceDown"
case .unknown:
return "unknown"
@unknown default:
return "unknown"
}
}
}
extension AVCaptureDevice.FlashMode: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .auto:
return "auto"
case .on:
return "on"
case .off:
return "off"
@unknown default:
return "unknown"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment