YunDaeHyeon
← Lab

AVCaptureSession

스터디2025.11.09
  • AVFoundation
  • AVCaptureSession

AVCaptureSession에 대해 학습하기 전, AVFoundation에 대해 짚고 넘어가보자.

AVFoundation이란?

AVFoundation | Apple Developer Documentation

AVFoundation은 Apple의 iOS나 macOS, tvOS 플랫폼 등에서 시청각 미디어를 캡쳐하거나 재생, 편집, 처리하는 프레임워크이다!

우리가 흔히 사용하는 카메라 촬영(입력/출력)부터 비디오, 오디오 파일 관리, 실시간 스트리밍까지 모두 포괄한다.

AVFoundation으로 활용할 수 있는 영역은 다음과 같다.

  1. 미디어 관리

→ AVAsset 클래스를 활용해 파일이나 입출력 스트림, 네트워크 미디어를 추상화하여 독립적으로 로드하고 메타데이터를 처리할 수 있다.

  1. 캡처 및 입력 관리

→ AVCaptureSession을 통해 카메라나 마이크와 같은 입, 출력 하드웨어의 실시간 데이터 흐름을 구축할 수 있다.

  1. 재생 및 출력

→ AVPlayer를 이용하여 동영상과 오디오 재생, AVAudioPlayer 로 간단한 오디오 처리나 AVKit을 통해 UI 구현을 할 수 있다.

AVFoundation의 아키텍처

About AVFoundation

AVFoundation은 Core Audio, Core Media, Core Animation 위에 구축되어 코덱과 컨테이너 같이 저레벨 구성 요소를 감추고 고레벨 API를 제공한다.

예를 들어서.. 카메라 앱에서는 AVCapture로 입/출력 파이프라인 구축을, 동영상 플레이어에서는 AVPlayer로 동영상 재생 제어를 구현할 수 있다.

아래 이미지는 iOS 에서의 AVFoundation 레이어를 뜻한다.

이미지 출처 : https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40010188-CH1-SW3

아래 이미지는 MacOS(OX X) 에서의 AVFoundation 레이어를 뜻한다. (iOS의 AVFoundation과 약간 차이가 있는 것 같다.)

이미지 출처 : https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40010188-CH1-SW3

카메라 제어 및 관리는 AVFoundation 하위에 존재하는 AVCapture 계열을 활용하여 구현할 수 있다!

AVAsset, AVKit, AVPlayer, AVAudioSession 등은 넘어간다.
위 내용들이 정말 잘 정리되어있는 문서 :
https://ios-development.tistory.com/927


AVCapture 개념

AVCapture는 AVFoundation 안에서 실시간 오디오, 비디오 캡처를 위한 계층을 통칭해서 부르는 개념이라고 보면 된다.

카메라와 마이크에서 들어오는 스트림을 입력 -> 처리 -> 출력 단계로 흘려보내는 파이프라인을 구축하고 이 과정에서 CPU, GPU, 메모리 등을 사용한다.

주요 구성 요소는 다음과 같다.

  1. AVCaptureSession
  • AVCapture 파이프라인의 핵심이자 허브이다.
  • 어떤 입력을 어떤 출력으로 연결할지 정의하고 start와 stop을 관리한다.
  1. AVCaptureDevice / AVCaptureDeviceInput
  • 실제 하드웨어(카메라, 마이크)를 선택하고 이를 Session의 input으로 연결한다.
  1. AVCaptureOutput 계열 → 캡처된 데이터를 어떻게 활용할 것인지 결정한다.
  • AVCapturePhotoOutput : 사진 캡처
  • AVCaptureMovieFileOutput : 동영상 파일
  • AVCaptureVideoDataOutput, AVCaptureAudioDataOutput : 프레임, 오디오 샘플을 콜백으로 직접 받아 처리한다.
  1. AVCaptureConnection
  • 입력, 출력 사이의 연결 관리, 방향이나 안정화 등의 설정을 진행한다.
  1. AVCaptureVideoPreviewLayer
  • Session에 연결하여 실시간 프리뷰를 화면에 렌더링할 수 있다.

🤔 그래서 이것들로 무엇을 할 수 있을까?
1️⃣ 카메라 앱 구현

2️⃣ 실시간 비디오, 오디오 처리

3️⃣ 고급

1️⃣ 카메라 앱 구현

  • 전면/후면 카메라 선택, 포커스/노출 조정, 플래시/토치, 사진 촬영, 동영상 녹화 등 기본 카메라 기능을 모두 구현할 수 있다.

2️⃣ 실시간 비디오, 오디오 처리

  • VideoDataOutput으로 CMSampleBuffer 를 받아서 필터링, 얼굴 인식, 머신러닝 추론 등 커스텀 파이프라인을 구축할 수 있다.
  • AudioDataOutput 으로 실시간 오디오 분석, 시각화, 필터 등을 적용할 수 있다.

3️⃣ 고급

  • 멀티 카메라(듀얼이나 울트라와이드 같은?)를 결합하여 심도 효과, 고해상도 사진, HDR과 포트레이트 효과 등도 AVCapture 계층 위에서 제공되는 고수준 API를 통해 구현할 수 있다.

🤔 CPU, GPU, 메모리와는 무슨 관계일까?
1️⃣ CPU와의 관계

2️⃣ GPU와의 관계

3️⃣ 메모리와의 관계

1️⃣ CPU와의 관계

  • Session 구성 변경(입, 출력 추가 및 제거), 포맷 선택, 포커스 및 노출 제어, 제스처 처리, 파일 입출력 등 제어 로직과 일반적인 앱 로직은 주로 CPU가 처리한다.
  • VideoDataOutput 콜백에서 ⭐ CMSampleBuffer를 직접 만지기에 여기서 포맷 변환이나 인코딩, ML 추론 등을 시도하면 CPU 부담이 커지므로 최적화를 반드시 고려하자.
  • 프레임 처리 비용이 크면 클수록 프레임 드랍 현상과 발열, UI 블로킹, 배터리 광탈 등의 현상이 발생하므로 가능한 Metal 이나 Core Image 과 같이 GPU나 전용 하드웨어로 오프로드 시키는 것이 좋다고 한다. (살짝 이해가 안간다..)

2️⃣ GPU와의 관계

  • ⭐ 카메라에서 나온 윈시 프레임을 Image Signal Processor가 1차적으로 처리하고 이후 프리뷰 렌더링과 이미지 필터링 작업은 GPU가 수행한다.
  • AVCaptureVideoPreviewLayer는 내부적으로 Core Animation과 GPU를 통해 프레임을 화면에 뿌린다.
  • Metal 이나 Core Image와 함께 사용하면 CMSampleBuffer → CVPixelBuffer → 텍스처로 이어지는 나만의 GPU 파이프라인을 구성할 수 있어서 여기서 커스텀할 수 있다!

3️⃣ 메모리와의 관계

  • 카메라 스트림은 ⭐ 초당 수십 장의 고해상도 프레임(CVPixelBuffer)을 다루기 때문에 버퍼 수와 해상도에 비래하여 메모리 사용량이 커지므로 유의하자.
  • 고해상도 / 4K / 60FPS로 올릴 수록 버퍼당 크기와 초당 버퍼 수가 계속 우상향하므로 메모리와 대역폭 부담이 커지게 되니 유의하자.
  • ⭐ 불필요한 버퍼 복사와 보관을 줄이고 필요한 경우엔 GPU와 CVMetalTextureCache 등 공유 메모리를 사용하여 copy-less 파이프라인을 구성하자.

→ https://developer.apple.com/documentation/corevideo/cvmetaltexturecache-q3j


AVCapture 구현 예시

1. AVCaptureSession

💡 AVCaptureSession은 입력 장치에서 캡처 출력으로 데이터 흐름을 조정하고 캡처 동작을 구성하는 객체이다. 모든 AVCapture 파이프라인의 중심에 위치하며 어떤 입력을 어떤 출력으로 연결할지 정의한다.

주요 속성 및 메서드 정리

sessionPreset : 캡처 품질과 해상도를 설정한다.

let session = AVCaptureSession()
session.sessionPreset = .hd1920x1080

// 주요 preset 옵션
// .photo - 최고 해상도 사진 품질
// .high - 고품질 비디오 출력
// .medium - 중간 품질
// .low - 낮은 품질
// .hd1280x720 - 720p HD
// .hd1920x1080 - 1080p Full HD
// .hd4K3840x2160 - 4K
// .vga640x480 - VGA 해상도

단, 위 참고 단락에서 정리한 것 처럼 높은 해상도일수록 CPU와 GPU의 부하와 메모리 사용량이 커지므로 요구사항에 맞는 적절한 preset을 선택하자.

생명주기 메서드

// 세션 시작 (blocking 메서드이므로 백그라운드 큐에서 호출하는 게 좋다)
session.startRunning()

// 세션 중지
session.stopRunning()

// 세션 실행 여부 확인
if session.isRunning {
    print("세션이 실행중입니다.")
}

Configuration 메서드

beginConfiguration__, commitConfiguration 은 실행 중인 캡처 세션의 설정 변경을 중간 상태 없이 한 번에 적용하기 위한 안전장치이다.

// 올바른 사용법
session.beginConfiguration()

// 여러 설정 변경을 원자적으로 처리
if session.canAddInput(videoInput) {
    session.addInput(videoInput)
}
if session.canAddOutput(photoOutput) {
    session.addOutput(photoOutput)
}

session.commitConfiguration()

// 잘못된 사용법 - startRunning()을 configuration 사이에 호출하지 말자.
session.beginConfiguration()
session.startRunning() // 에러 발생 뚜둥
session.commitConfiguration()

즉, 잘못된 사용법은 더 쉽게 말하면 아래와 같다.

session.beginConfiguration()

// 이 사이의 변경은 즉시 적용되지 않음
session.removeInput(oldInput)
session.addInput(newInput)
session.addOutput(videoOutput)
session.sessionPreset = .high

session.commitConfiguration() // 여기서 한 번에 적용된다.

AVCaptureSession은 보통 실행 중인 상태에서 설정을 바꾼다.

Session이 running 중일 때 입력 디바이스를 바꾸거나 출력을 추가하거나 해상도 프리셋을 변경하는 설정을 하나씩 바로 적용하면 문제가 생길 수 있다.

session.addInput(cameraInput)     // 1단계
session.addOutput(videoOutput)    // 2단계
session.sessionPreset = .high     // 3단계

위처럼 구성하면 실행 중인 세선은 1단계 반영 완료, 2단계 반영 완료 … 이런 중간 상태를 가지기 떄문에 크래시나 프레임 드롭 등의 현상이 발생할 수 있으므로 beginConfiguration, commitConfiguration 을 잘 활용하자.

⭐ 단, 세션 최초 설정이거나 단일 설정만 변경하고 Session이 멈춰있을 때는 상관없다.

스레드 안정성

Session 관련 모든 작업은 blocking이므로 별도의 직렬 큐(serial queue)에서 처리하는 것이 권장되므로 유의하자.

private let sessionQueue = DispatchQueue(label: "session.queue") // 이렇게!

sessionQueue.async { [weak self] in
    guard let self else { return }
    self.session.startRunning()
}

예시 코드

import AVFoundation

class CameraManager {
    private let session = AVCaptureSession()
    private let sessionQueue = DispatchQueue(label: "session.queue")
    
    func setupSession() {
        sessionQueue.async { [weak self] in
            guard let self = self else { return }
            
            self.session.beginConfiguration()
            
            // MARK: - Preset 설정
            if self.session.canSetSessionPreset(.hd1920x1080) {
                self.session.sessionPreset = .hd1920x1080
            }
            
            // MARK: - Input 및 Output 추가는 이후 섹션에서 진행한다.
            
            self.session.commitConfiguration()
        }
    }
    
    func startSession() {
        sessionQueue.async { [weak self] in
            self?.session.startRunning()
        }
    }
    
    func stopSession() {
        sessionQueue.async { [weak self] in
            self?.session.stopRunning()
        }
    }
}

2. AVCaptureDevice/Input

💡 AVCaptureDevice는 카메라나 마이크와 같은 하드웨어 또는 가상 캡처 장치를 나타내는 객체이다.

카메라 디바이스 찾기 (iOS 17+)

Discovery Session 사용을 권장한다.

import AVFoundation

// MARK: - 후면 카메라 찾기
let discoverySession = AVCaptureDevice.DiscoverySession(
    deviceTypes: [
        .builtInWideAngleCamera,      // 일반 광각 카메라
        .builtInUltraWideCamera,      // 초광각 카메라 (지원 기기만)
        .builtInTelephotoCamera,      // 망원 카메라 (지원 기기만)
        .builtInDualCamera,           // 듀얼 카메라 시스템
        .builtInTripleCamera,         // 트리플 카메라 시스템
        .builtInTrueDepthCamera       // True Depth 카메라 (전면)
    ],
    mediaType: .video,
    position: .back // 후면
)

guard let backCamera = discoverySession.devices.first else {
    print("후면 카메라를 찾을 수 없습니다")
    return
}

// MARK: - 전면 카메라 찾기
let frontDiscoverySession = AVCaptureDevice.DiscoverySession(
    deviceTypes: [.builtInWideAngleCamera],
    mediaType: .video,
    position: .front // 전면
)

guard let frontCamera = frontDiscoverySession.devices.first else {
    print("전면 카메라를 찾을 수 없습니다")
    return
}

간단하게 기본 카메라를 사용하는 방법은?

// 기본 후면 카메라
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, 
                                          for: .video, 
                                          position: .back) else {
    print("카메라를 사용할 수 없습니다")
    return
}

AVCaptureDeviceInput 생성

do {
    let videoInput = try AVCaptureDeviceInput(device: backCamera)
    
    // Session에 추가하기 전 확인한다.
    if session.canAddInput(videoInput) {
        session.addInput(videoInput)
    } else {
        print("비디오 입력을 추가할 수 없습니다")
    }
} catch {
    print("카메라 입력 생성 실패: \(error.localizedDescription)")
}

카메라 전환 기능 추가하기

class CameraManager {
    private let session = AVCaptureSession()
    private var currentCamera: AVCaptureDevice?
    private var currentInput: AVCaptureDeviceInput?
    
    func switchCamera() {
        sessionQueue.async { [weak self] in
            guard let self else { return }
            
            self.session.beginConfiguration()
            
            // 기존 입력 제거
            if let currentInput = self.currentInput {
                self.session.removeInput(currentInput)
            }
            
            // 새 카메라 선택
            let newPosition: AVCaptureDevice.Position = 
                self.currentCamera?.position == .back ? .front : .back
            
            guard let newCamera = AVCaptureDevice.default(
                .builtInWideAngleCamera,
                for: .video,
                position: newPosition
            ) else {
                self.session.commitConfiguration()
                return
            }
            
            // 새 입력 추가
            do {
                let newInput = try AVCaptureDeviceInput(device: newCamera)
                if self.session.canAddInput(newInput) {
                    self.session.addInput(newInput)
                    self.currentCamera = newCamera
                    self.currentInput = newInput
                }
            } catch {
                print("카메라 전환 실패: \(error)")
            }
            
            self.session.commitConfiguration()
        }
    }
}

3. AVCaptureOutput

AVCapturePhotoOutput - 사진 캡처

정지된 이미지, Live Photos 및 기타 사진 타입을들 위한 캡처 출력이다.

let photoOutput = AVCapturePhotoOutput()

// Session에 추가
if session.canAddOutput(photoOutput) {
    session.addOutput(photoOutput)
}

// 사진 촬영
func capturePhoto() {
    let settings = AVCapturePhotoSettings()
    
    // Flash 설정 (선택적)
    if photoOutput.supportedFlashModes.contains(.auto) {
        settings.flashMode = .auto
    }
    
    // 고해상도 캡처 활성화
    settings.isHighResolutionPhotoEnabled = true
    
    photoOutput.capturePhoto(with: settings, delegate: self)
}

// Delegate 구현
extension CameraManager: AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput, 
                    didFinishProcessingPhoto photo: AVCapturePhoto, 
                    error: Error?) {
        guard error == nil else {
            print("사진 캡처 실패: \(error!)")
            return
        }
        
        guard let imageData = photo.fileDataRepresentation() else {
            print("이미지 데이터 변환 실패")
            return
        }
        
        // UIImage로 변환
        if let image = UIImage(data: imageData) {
            // 이미지 저장 또는 처리
            UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
        }
    }
}

AVCaptureMovieFileOutput - 동영상 녹화

QuickTime 동영상 파일로 비디오와 오디오를 녹화하는 캡처 출력

let movieOutput = AVCaptureMovieFileOutput()

// Session에 추가
if session.canAddOutput(movieOutput) {
    session.addOutput(movieOutput)
}

// 녹화 시작
func startRecording() {
    guard !movieOutput.isRecording else { return }
    
    let outputPath = NSTemporaryDirectory() + "output.mov"
    let outputURL = URL(fileURLWithPath: outputPath)
    
    // 기존 파일 삭제
    if FileManager.default.fileExists(atPath: outputPath) {
        try? FileManager.default.removeItem(atPath: outputPath)
    }
    
    movieOutput.startRecording(to: outputURL, recordingDelegate: self)
}

// 녹화 중지
func stopRecording() {
    if movieOutput.isRecording {
        movieOutput.stopRecording()
    }
}

// Delegate 구현
extension CameraManager: AVCaptureFileOutputRecordingDelegate {
    func fileOutput(_ output: AVCaptureFileOutput, 
                   didFinishRecordingTo outputFileURL: URL, 
                   from connections: [AVCaptureConnection], 
                   error: Error?) {
        if let error = error {
            print("녹화 실패: \(error)")
            return
        }
        
        // 동영상 저장
        UISaveVideoAtPathToSavedPhotosAlbum(outputFileURL.path, 
                                           nil, nil, nil)
    }
}

AVCaptureVideoDataOutput - 실시간 프레임 처리

프레임별로 데이터를 받아서 필터링, 객체 인식 등 커스텀 처리를 할 수 있다.

let videoDataOutput = AVCaptureVideoDataOutput()

// 픽셀 포맷 설정
videoDataOutput.videoSettings = [
    kCVPixelBufferPixelFormatTypeKey as String: 
        kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
]

// 늦게 도착한 프레임 드랍
videoDataOutput.alwaysDiscardsLateVideoFrames = true

// Delegate 큐 설정
let videoQueue = DispatchQueue(label: "video.queue", 
                               qos: .userInteractive)
videoDataOutput.setSampleBufferDelegate(self, queue: videoQueue)

// Session에 추가
if session.canAddOutput(videoDataOutput) {
    session.addOutput(videoDataOutput)
}

// Delegate 구현
extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, 
                      didOutput sampleBuffer: CMSampleBuffer, 
                      from connection: AVCaptureConnection) {
        // CMSampleBuffer 처리
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        
        // 이미지 처리, ML 모델 추론 등
        // 예: Core Image 필터 적용
        let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
        // ... 필터링 로직
    }
    
    func captureOutput(_ output: AVCaptureOutput, 
                      didDrop sampleBuffer: CMSampleBuffer, 
                      from connection: AVCaptureConnection) {
        print("프레임 드랍 발생")
    }
}

AVCaptureConnection - 입출력 연결 관리

AVCaptureConnection은 입력과 출력 사이의 연결을 관리하고 방향, 안정화 등을 설정한다.

// Output에서 Connection 가져오기
if let connection = videoDataOutput.connection(with: .video) {
    
    // 비디오 방향 설정
    if connection.isVideoOrientationSupported {
        connection.videoOrientation = .portrait
    }
    
    // 비디오 미러링 (전면 카메라용)
    if connection.isVideoMirroringSupported {
        connection.isVideoMirrored = true
    }
    
    // 비디오 안정화 (지원되는 경우)
    if connection.isVideoStabilizationSupported {
        connection.preferredVideoStabilizationMode = .auto
    }
}

AVCaptureVideoPreviewLayer - 실시간 프리뷰

카메라 장치의 비디오를 표시하는 Core Animation 레이어
이때, UIKit 기반 UIView이기 때문에 SwiftUI에서 사용하려면 UIViewRepresentable로 감싸야한다.

UIKit 에서 사용할 경우

let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = .resizeAspectFill
previewLayer.frame = view.bounds

// View에 추가
view.layer.addSublayer(previewLayer)

// 방향 설정
if let connection = previewLayer.connection {
    connection.videoOrientation = .portrait
}

SwiftUI 에서 사용할 경우

import SwiftUI
import AVFoundation

struct CameraPreview: UIViewRepresentable {
    let session: AVCaptureSession
    
    func makeUIView(context: Context) -> VideoPreviewView {
        let view = VideoPreviewView()
        view.backgroundColor = .black
        view.videoPreviewLayer.session = session
        view.videoPreviewLayer.videoGravity = .resizeAspectFill
        return view
    }
    
    func updateUIView(_ uiView: VideoPreviewView, context: Context) {
        // 업데이트 로직 (필요시)
    }
}

class VideoPreviewView: UIView {
    override class var layerClass: AnyClass {
        AVCaptureVideoPreviewLayer.self
    }
    
    var videoPreviewLayer: AVCaptureVideoPreviewLayer {
        return layer as! AVCaptureVideoPreviewLayer
    }
}

// 사용 예시
struct ContentView: View {
    @StateObject private var camera = CameraManager()
    
    var body: some View {
        CameraPreview(session: camera.session)
            .onAppear {
                camera.startSession()
            }
            .onDisappear {
                camera.stopSession()
            }
    }
}

AVCapture를 사용할 때 유의사항

1. 권한 처리

카메라와 마이크 사용 목적을 _Info.plist_에 반드시 명시하자.

<key>NSCameraUsageDescription</key>
<string>사진과 비디오를 촬영하기 위해 카메라 접근이 필요합니다.</string>

<key>NSMicrophoneUsageDescription</key>
<string>비디오와 함께 오디오를 녹음하기 위해 마이크 접근이 필요합니다.</string>

또한, AVCaptureSession 을 사용하기 전에는 항상 authorizationStatus() 를 통해 권한 여부를 확인해야한다.

let status = AVCaptureDevice.authorizationStatus(for: .video)
var isAuthorized = status == .authorized
        
// 아직 권한을 요청하지 않은 경우
if status == .notDetermined {
	isAuthorized = await AVCaptureDevice.requestAccess(for: .video)
}

좀 더 응용하면 switch문을 통해 권한 상태별 처리가 가능하다.

func handleCameraPermission() {
    switch AVCaptureDevice.authorizationStatus(for: .video) {
    case .authorized:
        // 권한이 이미 허용됨 -> 카메라 사용 가능
        setupCamera()
        
    case .notDetermined:
        // 권한을 아직 요청하지 않음 -> 요청하기
        Task {
            let granted = await AVCaptureDevice.requestAccess(for: .video)
            if granted {
                setupCamera()
            }
        }
        
    case .denied:
        // 사용자가 권한을 거부함 -> 설정으로 안내
        showSettingsAlert()
        
    case .restricted:
        // 권한이 제한되었을 경우 -> 이런 경우가 있나...? 🧐
        showRestrictedAlert()
        
    @unknown default:
        break
    }
}

2. Session 중단 상황 처리

Session은 아래와 같은 상황에서 중단될 수 있다.

  • 전화 수신
  • 다른 앱이 오디오 세션을 먹고 있을 때
  • 앱이 백그라운드로 전환되었을 때
  • 멀티태스킹

각 모든 경우를 반드시 처리해줘야 안정적인 앱이 될 수 있다.


참고 문서

[iOS - swift] 1. AVFoundation 개념 - 구조, AVAsset, AVKit

[AVFoundation] AVFoundation에서 자주 쓰이는 객체들에 관하여

[iOS] AVFoundation, AVAudioPlayer, Timer

[iOS] AVFoundation & Media Playback Programming Guide

AVFoundation - 낸슥 iOS

iOS ) AVKit과 AVFoundation

[iOS] AVKit과 AVFoundation

AVCaptureSession | Apple Developer Documentation

Setting up a capture session | Apple Developer Documentation

Requesting authorization to capture and save media | Apple Developer Documentation

AVCapturePhotoOutput | Apple Developer Documentation

AVCaptureDevice.DiscoverySession | Apple Developer Documentation