clyne의 개발 기록

[AVFoundation] Camera프리뷰 및 사진찍기 (기본 예제) 본문

iOS/Swift

[AVFoundation] Camera프리뷰 및 사진찍기 (기본 예제)

clyne_dev 2021. 4. 27. 22:24

 

안녕하세요!!

주말동안 잘 쉬셨나요?

iOS개발자 CNOO 입니다!!

 

오늘은, Apple에서 제공하는 AVFoundation 모듈을 활용하여, Camera기능을 구현해보도록 하겠습니다.

 

오늘 구현해볼 것은

1. 카메라 프리뷰

2. 촬영버튼을 통한 촬영

크게 이렇게 두 가지 입니다.

 

렛터디에서  출석 사진을 등록하는 화면을 구현한 코드를 예시로 소개할 것입니다.

먼저, 구현된 화면을 보실게요~

 

 

렛터디에서 출석사진 등록을 진행하는 화면입니다.

 

 

 

 


1. 카메라 접근 권한 요청

카메라 기능을 활용하기 위해서는, 사용자에게 카메라 접근 권한을 획득해야겠죠?

카메라 접근 권한과 관련해서는 제가 이전에 포스팅한 글을 참고해주세요!

2021.04.26 - [iOS/Swift] - [Swift] 카메라/사진 권한 요청

 

[Swift] 카메라/사진 권한 요청

안녕하세요. iOS 개발자 CNOO 입니다. 오늘은 Swift에서 카메라 접근, 사진 접근 권한을 얻는 방법에 대해서 알아보려고 합니다. 1. 사진 접근 권한 기기에 있는 사진을 가지고 온다거나 할 때는 사진

cnoo.tistory.com

 

 

2. 변수 선언

기본적으로 필요한 변수들 부터 선언해줍시다.


let JPEG_QUALITY:CGFloat    = 0.8       // jpeg 압축 품질 (0.0 ~ 1.0)
let RESOLUTION_MAX:CGFloat  = 960.0     // jpg 이미지 (가로/세로) 리사이징 최대값
lazy var captureSession = AVCaptureSession()
var captureOutput: AVCaptureOutput?
var captureOn   = false   // 프리뷰 캡쳐 (사진촬영)
var currentOrientation:AVCaptureVideoOrientation = .portrait
var previewLayer: AVCaptureVideoPreviewLayer?
private let avDevice = AVCaptureDevice.default(for: AVMediaType.video)!

기본적으로 카메라는 현재 카메라가 보는 화면을  연속적으로 캡쳐해서  AVCapture세션이라는 것으로 연결하여 부드럽게 보여줍니다.

그것을 위해 AVCapture는 미리 객체를 생성해서 선언해주기로 합니다.

그 외에 CaptureOn 변수는 Session에서 계속해서 보내주는 화면을 찍어서 UIImage로 보여주는 플래그값입니다.

 

 

2. 캡쳐세션 세팅 + 방향 조정

setupCaptureSession()  이라는 메서드를 시작으로 캡쳐 세션을 열어봅시다.


override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    self.setupCameraOrientation()
}

저는 개인적으로  위와같이  viewDidLayoutSubviews 에서 셋업하는 것을 좋아합니다. 뷰가 다 그려지고 나서 카메라를 여는 것이 더 안정적입니다.

 

 


func setupCaptureSession() {
    
    if let currentDevice = self.setupDevice() {
    
        do {
            let videoDeviceInput = try AVCaptureDeviceInput(device: currentDevice)
            
            captureSession.beginConfiguration()
            if captureSession.canAddInput(videoDeviceInput) {
                captureSession.addInput(videoDeviceInput)
            }
            
            captureSession.sessionPreset = .high
            let videoDataOutput = AVCaptureVideoDataOutput()
            let sampleBufferQueue = DispatchQueue(label: "sampleBufferQueue")
            videoDataOutput.setSampleBufferDelegate(self, queue: sampleBufferQueue)
            
            if captureSession.canAddOutput(videoDataOutput) {
                captureSession.addOutput(videoDataOutput)
            }
            
            videoDataOutput.connection(with: .video)? .isEnabled = true
            
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.videoGravity = .resizeAspectFill
            
            self.previewLayer = previewLayer
            self.captureOutput = videoDataOutput
            
            DispatchQueue.main.async {
                previewLayer.frame = self.previewView.bounds
                self.previewView.layer.addSublayer(previewLayer)
            }
            
            captureSession.commitConfiguration()
            captureSession.startRunning()
            
        }
        catch {
            print(error)
        }
    }
}



func setupDevice() -> AVCaptureDevice? {
    var devices: [AVCaptureDevice]
    let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position:
AVCaptureDevice.Position.unspecified)
    devices = deviceDiscoverySession.devices
    
    for device in devices {
        if device.position == AVCaptureDevice.Position.back {
            return device
        }
    }
    
    return nil
}


private func updatePreviewLayer(layer: AVCaptureConnection, orientation: AVCaptureVideoOrientation) {
    layer.videoOrientation = orientation
    self.previewLayer?.frame = self.previewView.bounds
}


func setupCameraOrientation() {
    guard
        let connection =  self.previewLayer?.connection
        else {return}
    
    let currentDevice: UIDevice = UIDevice.current
    let previewLayerConnection : AVCaptureConnection = connection
    
    if currentDevice.userInterfaceIdiom == .phone {
        updatePreviewLayer(layer: previewLayerConnection, orientation: .portrait)
    }
    else {
        if previewLayerConnection.isVideoOrientationSupported {
            var orientation: UIInterfaceOrientation = .portrait
              if #available(iOS 13.0, *) {
                let windowScene = UIApplication.shared.connectedScenes.filter {
                    $0.activationState == .foregroundActive
                }.first
                if let windowScene = windowScene as? UIWindowScene {
                    orientation = windowScene.interfaceOrientation
                }
              } else {
                  orientation = UIApplication.shared.statusBarOrientation
              }
            switch (orientation) {
                case .portrait: currentOrientation = .portrait
                case .landscapeRight: currentOrientation = .landscapeRight
                case .landscapeLeft: currentOrientation = .landscapeLeft
                case .portraitUpsideDown: currentOrientation = .portraitUpsideDown
                default: break;
            }
            updatePreviewLayer(layer: previewLayerConnection, orientation: currentOrientation)
        }
    }
}

코드가 너무 길어서 좀 짤리네요..

 

여기서 CaptureOuput 함수는

AVCaptureVideoDataOutputSampleBufferDelegate 프로토콜에 정의되어있는 함수입니다.

이 것을 구현한 것이구요,  avDevice 얻어오는 과정에서, 후면 카메라를 쓸지, 전면 카메라를 쓸지 결정하고,

사진을 찍었을 때,  가로방향인지, 세로방향인지는 setupCameraOrientation 메서드에서 세팅해주게 됩니다. ( 이 메서드는 꼭 메인 스레드에서 동작시켜주셔야 합니다.)

 

여기까지 진행하셨으면, 카메라 화면이 동작하고 있는 것을 보실 수 있으실텐데요,

여기서 이제 마지막으로  촬영 버튼을 눌렀을 때 사진 촬영이 되는 것 까지 구현해 주어야겠죠???

 

 

 

3. 사진 촬영

CaptureOuput 메서드쪽에 로그를 찍어보시면, 사진이 카메라 세션에 의해 계속해서 들어오는 것을 알 수 있습니다.

이중에서, 사람이 촬영 버튼을 눌렀을 때 (takePicture함수 호출)   1번에서 선언한 captureOn 플래그값을  변경시켜줌으로써, 순간적으로 1개의 세션 캡쳐이미지를 가지고 오는 것입니다.

 


private func takePicture() {
    if !captureOn {
        LTFeedbackGenerator().impact(style: .light)  // 진동
        captureOn = true
    }
}




public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    if self.captureOn {
        if let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
            
            var image = CIImage(cvImageBuffer: imageBuffer)
            
            // 방향 조정
            var orientation: CGImagePropertyOrientation = .up
            switch currentOrientation {
            case .portrait:
                orientation = .right
            case .portraitUpsideDown:
                orientation = .left
            case .landscapeLeft:
                orientation = .up
            case .landscapeRight:
                orientation = .down
            default: break;
            }
            image = image.oriented(forExifOrientation: Int32(orientation.rawValue))
            // 리사이징
            let scale = RESOLUTION_MAX / max(image.extent.width, image.extent.height)
            if let filter = CIFilter(name: "CILanczosScaleTransform",
                                     parameters: ["inputImage"     : image,
                                                  "inputScale"     : scale]) {
                if let outputImage = filter.outputImage {
                    image = outputImage
                }
            }
            
            let ciContext = CIContext()
            
            if let data = ciContext.jpegRepresentation(of: image,
                                    colorSpace: CGColorSpaceCreateDeviceRGB(),
                                    options: [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption : JPEG_QUALITY]) {
                self.sendImage(data: data)
            }
        }
        self.captureOn = false
    }
}

 

그리고 sendImage(data: Data) 라는 메서드로   사진을 촬영해서 보내는데요,  (위에 적혀있는 captureOutput함수 안에서 저는 적어두었네요..!!

이쪽은  얻으신 Data를 가지고 

UIImage(data: data)  로 변환하셔서,   Image값을 원하는대로 처리하시면 되겠습니다!!

 

 

 

 


 

카메라를 실행해서 사진을 촬영하는 것에 대해서 알아보았습니다!!

 

사실 AVFoundation을 제대로 쓰려면, 애플 개발자 문서를 참고하시는 것이 좋습니다.

기능이 어마어마하게 많거든요...

 

그래도 처음 구현해보시는 여러분들께 도움이 되었으면 좋겠네요.!

 

 

다음 시간에는  디바이스 기능 중 카메라와 연관이 있는  Flash 기능을 사용해보도록 하겠습니다!! (또는 Torch 기능이요 ㅎㅎ)