- 카메라 줌인, 줌아웃 기능
- 촬영 기능
2가지 구현해봤습니다.
애플 공식 문서 : https://developer.apple.com/documentation/avfoundation
AVFoundation
AV 는 Audio Visual 입니다. 제가 이해한 AVFoundation 은 그냥 아이폰의 시청각 관련 하드웨어를 컨트롤 할 수 있게끔 돕는 Framework 입니다.
UIImagePickerController vs AVFoundation
커스텀 카메라를 구현하고 싶어서 공식문서를 좀 오랫동안 뒤져봤는데 UIImagePickerController 와 AVFoundation 의 두 가지 선택지가 있었습니다.
UIImagePickerController 는 아이폰의 system camera (기본 카메라) 를 고스란히 불러오는 거고, AVFoundation 은 조금 더 깊게 low level 로 파고 들어가서 Camera Device 와 소통하는 느낌입니다.
AVFoundation 이 돌아가는 플로우 이해하기
공식 문서 : https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture
커스텀 카메라를 구현할 거기 때문에 AVFoundation 의 AVCaptureSession 을 사용해야 됩니다.
먼저 크게 AVCaptureDeviceInput, AVCaptureSession, AVCaptureOutput 이 3가지가 어떻게 상호 작용하는지 이해해야 합니다.
AVCaptureDevice 는 말그대로 내 아이폰의 카메라고, AVCaptureDeviceInput 은 그 카메라 기기로부터 프로그램에 들어오는 사진이나 동영상 데이터입니다. AVCaptureOutput 은 사진을 찍어서 나온 결과가 될겁니다.
Session 은 Input 과 Output 을 연결해주는 파이프 역할이라고 이해할 수 있었고, Capture 의 디테일 설정을 관리하기도 합니다.
startRunning() 이라는 메서드로 실질적인 플로우가 시작됩니다. 그리고 이 메서드는 blocking call 이 될 수 있기 때문에 UI 를 처리하는 메인 쓰레드와 다른 쓰레드에서 처리해줘야 한다는 점을 주의해야합니다. 저는 DispatchQueue 를 사용해서 처리해줬습니다. 그리고 세션의 일이 끝났을 땐 stopRunning() 처리도 잊지 말아야됩니다.
// AVFoundation camera setting func settingCamera() { print("setting Camera") guard let captureDevice = getDefaultCamera() else { return } do { captureSession = AVCaptureSession() captureSession?.sessionPreset = .photo input = try AVCaptureDeviceInput(device: captureDevice) output = AVCapturePhotoOutput() setting = AVCapturePhotoSettings() guard let input = input, let output = output else {return} captureSession?.addInput(input) captureSession?.addOutput(output) guard let session = captureSession else {return} previewLayer = AVCaptureVideoPreviewLayer(session: session) guard let previewLayer = previewLayer else { return } // startRunning 은 UI 쓰레드를 방해할 수 있기 때문에 다른 쓰레드에 담아줌 globalDispatchQueue.async { session.startRunning() } mainDispatchQueue.async { previewLayer.frame = self.cameraView.frame self.cameraView.layer.addSublayer(previewLayer) } } catch { print("setting Camera Error") } }
카메라 확대 축소 기능을 구현하는 과정에서 꽤 많이 고생했습니다..
아무리 구글링을 많이 해봐도 결국엔 애플 공식 문서가 최고인 것 같습니다.먼저 카메라 확대 축소는 AVCaptureDevice 의 videoZoomFactor 를 사용해서 구현할 수 있습니다. https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624611-videozoomfactor
근데 이 device 의 videoZoomFactor 를 접근하기 위해서는 lockForConfiguration() 메서드를 호출해서 허락받아야 합니다. 그리고 볼일이 끝났으면 unlockForConfiguration() 을 호출해야 합니다. https://developer.apple.com/documentation/avfoundation/avcapturedevice
@objc func handlePinchCamera(_ pinch: UIPinchGestureRecognizer) { guard let device = getDefaultCamera() else {return} var initialScale: CGFloat = device.videoZoomFactor let minAvailableZoomScale = 1.0 let maxAvailableZoomScale = device.maxAvailableVideoZoomFactor do { try device.lockForConfiguration() if(pinch.state == UIPinchGestureRecognizer.State.began){ initialScale = device.videoZoomFactor } else { if(initialScale*(pinch.scale) < minAvailableZoomScale){ device.videoZoomFactor = minAvailableZoomScale } else if(initialScale*(pinch.scale) > maxAvailableZoomScale){ device.videoZoomFactor = maxAvailableZoomScale } else { device.videoZoomFactor = initialScale * (pinch.scale) } } pinch.scale = 1.0 } catch { return } device.unlockForConfiguration() }
카메라 촬영은 AVCapturePhotoOutput 의 capturePhoto 메서드로 구현할 수 있습니다. 결과 출력은 AVCapturePhotoCaptureDelegate 을 채택했을 때 delegate 메서드인 photoOutput 의 didFinishProcessingPhoto 에 구현했습니다.
// photoCapture proecess 가 끝날 떄 호출되는 delegate 메서드 func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { print("photoOutput") guard let imageData = photo.fileDataRepresentation() else { print("imageData Error") return} let outputImage = UIImage(data: imageData) // globalQueue 에서 Session stop globalDispatchQueue.async { guard let session = self.captureSession else { print("session error at photoOut func") return } session.stopRunning() } // mainQueue 쓰레드에서 UI 작업 mainDispatchQueue.async { //self.cameraView.layer.removeFromSuperlayer() print("cameraView to outputImage") self.cameraView.layer.contents = outputImage } } // 촬영 버튼 클릭 이벤트 @IBAction func tapCameraShootButton(_ sender: UIButton) { print("tapCameraShootButton") guard let setting = setting else {return} output?.capturePhoto(with: setting, delegate: self) }
카메라 촬영 후에도 결과를 보여주지 않고 계속 움직이길래 이건 또 왜그럴까.. 정말 고민을 많이 해봤습니다.
원인은 DispatchQueue.global 을 여러 개 선언했기 때문이었습니다. globalDispatchQueue 라는 이름으로 한 개만 선언하고, 여기서 startRunning 과 stopRunning 을 모두 관리해준 뒤, mainQueue 에 따로 결과를 보내주었더니 제대로 동작하게 됐습니다.