본문 바로가기

프로그래밍/iOS,macOS

카메라 데이터 수신을 위한 AVCaptureSession

AVCaptureSession 과 AVCaptureVideoDataOutput은 생성시 별도 설정이 없으므로, 미리 생성해 둔다.
Input의 경우 device 객체가 필요하므로, device 생성 이후에 설정.
output의 경우 device에 따라 픽셀 포맷이 변경되므로, 관련 프로퍼티 추가.

var captureSession: AVCaptureSession = .init()
var videoDataOutput: AVcaptureVideoDataOutput = .init()
var currentDevice: AVCaptureDevice?
var preview: AVCaptureVideoPreviewLayer?


var deviceFormat: AVCaptureDevice.Format?
var preferredOutputPixelFormat: FourCharCode = 0


var sampleQueue: DispatchQueue = DispatchQueue.global(qos: .userInteractive)

 

 

세션 설정

캡처 세션의 기본 설정은 preset 으로 제공되는데, AVCaptureSession.Preset 참조
동적으로 입출력 포맷을 설정하기를 원하는 경우 .inputPriority 프리셋 사용
비디오만 처리할 것이라 usesApplicationAudioSession 은 false로 선언했는데.. ios7 이후에는 차이가 없으므로 굳이 설정할 필요가 없을 듯 싶다.

captureSession.beginConfiguration()

captureSession.sessionPreset = .inputPriority
captureSession.usesApplicationAudioSession = false



// input 설정 및 추가
// output 설정 및 추가

captureSession.commitConfiguration()

 

디바이스 설정

디바이스에 따라 AVCaptureDevice.Format 이 변경되므로, format 에 따른 해상도, fps 설정을 진행한다.

참고) 카메라 전환 시 디바이스 설정부터 다시 수행해야 한다.

let position = AVCaptureDevice.Position.front
let currentDevice: AVCaptureDevice?
currentDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: position)


guard let device = currentDevice else { return }

// width, height
let formats: [AVCaptureDevice.Format] = device.formats
let targetWidth = 640
let targetHeight = 480

var selectedFormat: AVCaptureDevice.Format?
var currentDiff = INT_MAX

for format in formats {
   let dimension: CMVideoDimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
   let pixelFormat: FourCharCode = CMFormatDescriptionGetMediaSubType(format.formatDescription)
   let diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height)
   if diff < currentDiff {
       selectedFormat = format
       currentDiff = diff
   } else if diff == currentDiff && pixelFormat == preferredOutputPixelFormat {
       selectedFormat = format
   }
}
self.deviceFormat = selectedFormat


// fps 설정
let maxFrameRate: Float64 = 0.0
for fpsRange in  selectedFormat.videoSupportedFrameRateRanges {
   maxFrameRate = fmax(maxFrameRate, fpsRange.maxFrameRate)
}

let fps = Int(maxFrameRate)

do {
   try device.lockForConfiguration()
   device.activeFormat = selectedFormat
   device.activeVideoMinFrameDuration = CMTimeMake(value: 1, timescale: Int32(fps))
   device.unlockForConfiguration()
} catch {
   
}

 

입력

디바이스에 해당하는 AVCaptureDeviceInput 객체를 생성하고, 캡처세션에 추가한다.

guard let input: AVCaptureDeviceInput = try? AVCaptureDeviceInput(device: device) else {
   return
}

let inputs = captureSession.inputs
for old in inputs {
  captureSession.removeInput(old)
}

if captureSession.canAddInput(input) {
  captureSession.addInput(input)
} else {
}

 

출력 데이터 포맷, delegate 설정

픽셀포맷을 설정하고, 캡처세션에 추가한다.

Pixel Format Identifiers | Apple Developer Documentation

 

Apple Developer Documentation

 

developer.apple.com

 

주요 픽셀 포맷 값

CV420YpCbCr8BiPlanarVideoRange : 875704438
CV420YpCbCr8BiPlanarFullRange : 875704422
CV32BGRA : 1111970369
CV32ARGB : 32

 

 

VideoDataOutput의 경우 i420과 BGRA만 지원하고, ARGB는 지원하지 않음.

// 사용할 픽셀포맷
let pixelFormats: Set<OSType> = Set([kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
                                                        kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
                                                        kCVPixelFormatType_32BGRA,
                                                        kCVPixelFormatType_32ARGB
                                                       ])
// 출력에서 지원하는 픽셀포맷
let availablePixelFormats = NSMutableOrderedSet(array: videoDataOutput.availableVideoPixelFormatTypes)
availablePixelFormats.intersectSet(pixelFormats)

let pixelFormat = availablePixelFormats.firstObject as? OSType ?? 0

// 우선순위 설정
self.preferredOutputPixelFormat = pixelFormat

// 디바이스 설정시 선택한 포맷과 비교
if let format = self.deviceFormat {
    var mediaSubType: FourCharCode = CMFormatDescriptionGetMediaSubType(format.formatDescription)
    if supportedPixelFormats.contains(mediaSubType) {
        if mediaSubType != preferredOutputPixelFormat {
            self.preferredOutputPixelFormat = mediaSubType
        }
    } else {
        mediaSubType = self.preferredOutputPixelFormat
    }
}


// 출력 설정
let settings: [String: Any] = [
    String(kCVPixelBufferMetalCompatibilityKey): true,
    String(kCVPixelBufferPixelFormatTypeKey): NSNumber(value: self.preferredOutputPixelFormat),
]
videoDataOutput.videoSettings = settings
videoDataOutput.alwaysDiscardsLateVideoFrames = true
videoDataOutput.setSampleBufferDelegate(self, queue: sampleQueue)

// 출력 등록
if captureSession.canAddOutput(videoDataOutput) {
    captureSession.addOutput(videoDataOutput)
}

 

만약 출력된 데이터를 별도로 수정하거나 다른 타입으로 전환하는 등의 픽셀버퍼를 핸들링하는 경우 plane 타입의 픽셀포맷 보다는 단순 바이트 배열인 BGRA 포맷을 사용하는게 좋다.

 

 

설정 변경 및 캡처 시작

// 출력 미러모드 등 변경이 필요한 경우
captureSession.beginConfiguration()

if let connector = videoDataOutput.connection(with: .video) {
   if connector.isVideoMirrorinigSupported {
      if !isFrontCamera {
          connector.videoOrientation = .landscapeRight
          connector.isVideoMirrored = false
      } else {
   
          connector.videoOrientation = .landscapeLeft
   
          if isMirror {
              connectior.isVideoMirrored = true
              connector.videoOrientation = .landscapeRight
          } else {
              connector.isVideoMirrored = false
          }
      }
   }
}

captureSession.commitConfiguration()

// 캡처 시작
captureSession.startRunning()

 

캡처 중지

captureSession.stopRunning()

 

AVCaptureVideoDataOutputSampleBufferDelegate

func captureOutput(_ output: AVCaptureOutput, 
                   didOutput sampleBuffer: CMSampleBuffer,
                   from connection: AVCaptureConnection) {
                   
    if CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
      !CMSampleBufferIsValid(sampleBuffer) ||
      !CMSampleBufferDataIsReady(sampleBuffer) {
      
      return
    }
    
    let cvPixelBuffer: CVPixelBuffer? = CMSampleBufferGetImageBuffer(sampleBuffer)
    guard let pixelBuffer = cvPixelBuffer else {
       return
    }
    
    let time = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
    let nanoTime = time * 1_000_000_000
}