Created
December 12, 2018 06:41
-
-
Save yusuke024/3b5a89835deab5b9027efea794b80a45 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CameraController.swift | |
// | |
import AVFoundation | |
import Photos | |
import UIKit | |
class CameraController: UIViewController { | |
enum Camera { | |
case front, back | |
} | |
/// The current active camera. | |
private(set) var camera = Camera.back | |
private lazy var session = AVCaptureSession() | |
private lazy var photoOutput = AVCapturePhotoOutput() | |
private var activeCamera: AVCaptureDevice? | |
private lazy var frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) | |
private lazy var backCamera: AVCaptureDevice? = { | |
return AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInWideAngleCamera], mediaType: .video, position: .back).devices.first | |
}() | |
private lazy var previewView = UIView() | |
private lazy var previewLayer = AVCaptureVideoPreviewLayer(session: session) | |
private let currentDevice = UIDevice.current // Used to determine the image orientation. | |
/// A range to determine minimum and maximum zoom factor. | |
/// | |
/// This value will be intersected with `AVCaptureDevice.minAvailableVideoZoomFactor` and `AVCaptureDevice.maxAvailableVideoZoomFactor` before being applied. | |
var zoomScaleRange: ClosedRange<CGFloat> = 1...10 | |
private let sessionQueue = DispatchQueue(label: "Session Queue") | |
private var sessionSetupSucceeds = false | |
private var captureProcessors: [Int64: PhotoCaptureProcessor] = [:] | |
private var videoOrientation: AVCaptureVideoOrientation = .portrait | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// `AVCaptureVideoPreviewLayer` must be set up before we configure `AVCaptureSession` in a different queue. | |
previewLayer.videoGravity = .resizeAspectFill | |
previewView.frame = view.bounds | |
previewView.layer.addSublayer(previewLayer) | |
view.insertSubview(previewView, at: 0) | |
switch AVCaptureDevice.authorizationStatus(for: .video) { | |
case .authorized: | |
sessionQueue.async { [unowned self] in | |
self.configureSession() | |
} | |
case .notDetermined: | |
AVCaptureDevice.requestAccess(for: .video) { [unowned self] granted in | |
if granted { | |
self.sessionQueue.async { | |
self.configureSession() | |
} | |
} | |
} | |
default: | |
break | |
} | |
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:))) | |
previewView.addGestureRecognizer(pinch) | |
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) | |
previewView.addGestureRecognizer(tap) | |
setVideoOrientationForDeviceOrientation(currentDevice.orientation) | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(deviceOrientationChanged(_:)), | |
name: UIDevice.orientationDidChangeNotification, | |
object: nil) | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(didInterrupted), | |
name: .AVCaptureSessionWasInterrupted, | |
object: session) | |
} | |
@objc | |
func didInterrupted() { | |
print(session.isInterrupted) | |
} | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
sessionQueue.async { [unowned self] in | |
if self.sessionSetupSucceeds { | |
self.session.startRunning() | |
} | |
} | |
} | |
override func viewWillDisappear(_ animated: Bool) { | |
super.viewWillDisappear(animated) | |
sessionQueue.async { [unowned self] in | |
if self.sessionSetupSucceeds { | |
self.session.stopRunning() | |
} | |
} | |
} | |
override func viewWillLayoutSubviews() { | |
super.viewWillLayoutSubviews() | |
previewLayer.frame = previewView.layer.bounds | |
} | |
func setCamera(_ camera: Camera) { | |
guard sessionSetupSucceeds else { return } | |
if camera == self.camera { return } | |
sessionQueue.async { [unowned self] in | |
self.session.beginConfiguration() | |
self._setCamera(camera) | |
self.session.commitConfiguration() | |
} | |
} | |
func takePhoto(_ completion: ((Photo) -> Void)? = nil) { | |
guard sessionSetupSucceeds else { return } | |
let settings: AVCapturePhotoSettings | |
if self.photoOutput.availablePhotoCodecTypes.contains(.hevc) { | |
settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc]) | |
} else { | |
settings = AVCapturePhotoSettings() | |
} | |
let orientation = videoOrientation | |
// Update the photo output's connection to match current device's orientation | |
photoOutput.connection(with: .video)?.videoOrientation = orientation | |
let processor = PhotoCaptureProcessor() | |
processor.orientation = orientation | |
processor.completion = { [unowned self] processor in | |
if let photo = processor.photo { | |
PHPhotoLibrary.shared().performChanges({ | |
// Add the captured photo's file data as the main resource for the Photos asset. | |
let creationRequest = PHAssetCreationRequest.forAsset() | |
creationRequest.addResource(with: .photo, data: photo.fileDataRepresentation()!, options: nil) | |
}, completionHandler: nil) | |
completion?(Photo(photo: photo, orientation: processor.orientation)) | |
} | |
if let settings = processor.settings { | |
let id = settings.uniqueID | |
self.captureProcessors.removeValue(forKey: id) | |
} | |
} | |
captureProcessors[settings.uniqueID] = processor | |
sessionQueue.async { [unowned self] in | |
self.photoOutput.capturePhoto(with: settings, delegate: processor) | |
} | |
} | |
// Call this on the `sessionQueue`. | |
private func configureSession() { | |
self.session.beginConfiguration() | |
self.session.sessionPreset = .photo | |
if backCamera != nil { | |
_setCamera(.back) | |
} else if frontCamera != nil { | |
_setCamera(.front) | |
} else { | |
return | |
} | |
self.photoOutput.isHighResolutionCaptureEnabled = true | |
guard self.session.canAddOutput(self.photoOutput) else { | |
return | |
} | |
self.session.addOutput(self.photoOutput) | |
self.session.commitConfiguration() | |
sessionSetupSucceeds = true | |
} | |
/// You should nest the call to this function with `beginConfiguration()` and `commitConfiguration()`. | |
private func _setCamera(_ camera: Camera) { | |
let newDevice: AVCaptureDevice? | |
switch camera { | |
case .front: | |
newDevice = frontCamera | |
case .back: | |
newDevice = backCamera | |
} | |
if let _currentInput = session.inputs.first { | |
session.removeInput(_currentInput) | |
} | |
guard | |
let device = newDevice, | |
let input = try? AVCaptureDeviceInput(device: device), | |
session.canAddInput(input) else { return } | |
session.addInput(input) | |
self.camera = camera | |
activeCamera = device | |
} | |
private func configCamera(_ camera: AVCaptureDevice?, _ config: @escaping (AVCaptureDevice) -> ()) { | |
guard let device = camera else { return } | |
sessionQueue.async { [device] in | |
do { | |
try device.lockForConfiguration() | |
} catch { | |
return | |
} | |
config(device) | |
device.unlockForConfiguration() | |
} | |
} | |
private var initialScale: CGFloat = 0 | |
@objc | |
private func handlePinch(_ pinch: UIPinchGestureRecognizer) { | |
guard sessionSetupSucceeds, let device = activeCamera else { return } | |
switch pinch.state { | |
case .began: | |
initialScale = device.videoZoomFactor | |
case .changed: | |
let minAvailableZoomScale = device.minAvailableVideoZoomFactor | |
let maxAvailableZoomScale = device.maxAvailableVideoZoomFactor | |
let availableZoomScaleRange = minAvailableZoomScale...maxAvailableZoomScale | |
let resolvedZoomScaleRange = zoomScaleRange.clamped(to: availableZoomScaleRange) | |
let resolvedScale = max(resolvedZoomScaleRange.lowerBound, min(pinch.scale * initialScale, resolvedZoomScaleRange.upperBound)) | |
configCamera(device) { device in | |
device.videoZoomFactor = resolvedScale | |
} | |
default: | |
return | |
} | |
} | |
@objc | |
private func handleTap(_ tap: UITapGestureRecognizer) { | |
guard sessionSetupSucceeds else { return } | |
let point = tap.location(in: previewView) | |
let devicePoint = previewLayer.captureDevicePointConverted(fromLayerPoint: point) | |
configCamera(activeCamera) { device in | |
let focusMode = AVCaptureDevice.FocusMode.autoFocus | |
if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) { | |
device.focusPointOfInterest = devicePoint | |
device.focusMode = focusMode | |
} | |
let exposureMode = AVCaptureDevice.ExposureMode.autoExpose | |
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) { | |
device.exposurePointOfInterest = devicePoint | |
device.exposureMode = exposureMode | |
} | |
} | |
// Animate focusing | |
} | |
@objc | |
private func deviceOrientationChanged(_ note: Notification) { | |
setVideoOrientationForDeviceOrientation(currentDevice.orientation) | |
} | |
private func setVideoOrientationForDeviceOrientation(_ deviceOrientation: UIDeviceOrientation) { | |
switch deviceOrientation { | |
case .portrait: | |
videoOrientation = .portrait | |
case .portraitUpsideDown: | |
videoOrientation = .portraitUpsideDown | |
case .landscapeLeft: | |
videoOrientation = .landscapeRight | |
case .landscapeRight: | |
videoOrientation = .landscapeLeft | |
default: | |
break | |
} | |
} | |
} | |
// MARK: - CameraController.Photo | |
extension CameraController { | |
/// An object to represent the result photo taken with the camera | |
class Photo { | |
private let photo: AVCapturePhoto | |
private let orientation: AVCaptureVideoOrientation | |
fileprivate init(photo: AVCapturePhoto, orientation: AVCaptureVideoOrientation) { | |
self.photo = photo | |
self.orientation = orientation | |
} | |
func image() -> UIImage? { | |
guard let cgImage = photo.cgImageRepresentation()?.takeUnretainedValue() else { return nil } | |
let imageOrientation: UIImage.Orientation | |
switch orientation { | |
case .portrait: | |
imageOrientation = .right | |
case .portraitUpsideDown: | |
imageOrientation = .left | |
case .landscapeRight: | |
imageOrientation = .up | |
case .landscapeLeft: | |
imageOrientation = .down | |
} | |
return UIImage(cgImage: cgImage, scale: 1, orientation: imageOrientation) | |
} | |
} | |
} | |
// MARK: - PhotoCaptureProcessor | |
private class PhotoCaptureProcessor: NSObject { | |
var photo: AVCapturePhoto? | |
var completion: ((PhotoCaptureProcessor) -> Void)? | |
var settings: AVCaptureResolvedPhotoSettings? | |
var orientation: AVCaptureVideoOrientation = .portrait | |
} | |
extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate { | |
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { | |
guard error == nil else { | |
return | |
} | |
self.photo = photo | |
} | |
func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) { | |
guard error == nil else { | |
return | |
} | |
self.settings = resolvedSettings | |
completion?(self) | |
} | |
} |
@yusuke024 Is there a way to zoom in to a specific location? videoZoomFactor seems to always zoom in at the center, I would like to zoom in to a different position.
Thanks
Add please this since some changes:
extension AVCapturePhoto {
func stupidOSChangePreviewCGImageRepresentation() -> CGImage? {
#if compiler(>=5.5)
return self.previewCGImageRepresentation()
#else
return self.previewCGImageRepresentation()?.takeUnretainedValue()
#endif
}
}
And there is some problems with device orientation. Add this to handle correct orientation changing:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if let videoPreviewLayerConnection = previewView.videoPreviewLayer.connection {
let deviceOrientation = UIDevice.current.orientation
guard let newVideoOrientation = AVCaptureVideoOrientation(deviceOrientation: deviceOrientation),
deviceOrientation.isPortrait || deviceOrientation.isLandscape else {
return
}
videoPreviewLayerConnection.videoOrientation = newVideoOrientation
}
}
camera is not launching after permission granted, this line must needed.
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [unowned self] granted in
if granted {
self.sessionQueue.async {
self.configureSession()
self.session.startRunning() // this line must be called to start session after camera permission granted for the first time
}
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is great, thank you!