[iOS] AVFoundation 으로 커스텀 카메라 구현

김상우·2022년 1월 11일
0

구현 화면 gif

  • 카메라 줌인, 줌아웃 기능
  • 촬영 기능

2가지 구현해봤습니다.


AVFoundation

애플 공식 문서 : https://developer.apple.com/documentation/avfoundation

AVFoundation

AV 는 Audio Visual 입니다. 제가 이해한 AVFoundation 은 그냥 아이폰의 시청각 관련 하드웨어를 컨트롤 할 수 있게끔 돕는 Framework 입니다.


UIImagePickerController vs AVFoundation

커스텀 카메라를 구현하고 싶어서 공식문서를 좀 오랫동안 뒤져봤는데 UIImagePickerControllerAVFoundation 의 두 가지 선택지가 있었습니다.

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 에 따로 결과를 보내주었더니 제대로 동작하게 됐습니다.


profile
안녕하세요, iOS 와 알고리즘에 대한 글을 씁니다.

0개의 댓글