์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ“ธ iOS ์นด๋ฉ”๋ผ์™€ AVFoundation์„ ํ™œ์šฉํ•œ ์‚ฌ์ง„ยท๋น„๋””์˜ค ์ฒ˜๋ฆฌ ์™„์ „ ์ •๋ณต
Swift ยท iOS ๊ฐœ๋ฐœ

๐Ÿ“ธ iOS ์นด๋ฉ”๋ผ์™€ AVFoundation์„ ํ™œ์šฉํ•œ ์‚ฌ์ง„ยท๋น„๋””์˜ค ์ฒ˜๋ฆฌ ์™„์ „ ์ •๋ณต

์นด๋ฉ”๋ผ ์•ฑ ๊ฐœ๋ฐœ์ด ์–ด๋ ต๋‹ค๊ณ ? ๊ฑฑ์ • ใ„ดใ„ด ๐Ÿ™… AVFoundation ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์‹ค์ „ ์ฝ”๋“œ๊นŒ์ง€ ์‹น ๋‹ค ์•Œ๋ ค์ค„๊ฒŒ!

์•ผ ์†”์งํžˆ ๋งํ•ด์„œ, iOS ์นด๋ฉ”๋ผ ๊ฐœ๋ฐœ ์ฒ˜์Œ ์ ‘ํ•˜๋ฉด ์ง„์งœ ๋ฉ˜๋ถ• ์˜ค์ž–์•„ ๐Ÿ˜ต
AVFoundation์ด ๋ญ”์ง€, AVCaptureSession์€ ๋˜ ๋ญ”์ง€, ์šฉ์–ด๋ถ€ํ„ฐ ๋‚ฏ์„ค์–ด์„œ ํฌ๊ธฐํ•˜๊ณ  ์‹ถ์–ด์ง€๋Š” ๊ทธ ๋А๋‚Œ ์•Œ์ง€?
๊ทผ๋ฐ ํ•œ๋ฒˆ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๋ฉด ์ง„์งœ ๋ณ„๊ฑฐ ์—†์–ดใ…‹ใ…‹ ์˜ค๋Š˜ ์ด ๊ธ€ ๋‹ค ์ฝ๊ณ  ๋‚˜๋ฉด "์•„ ์ด๊ฑฐ์˜€์–ด?" ํ•˜๊ฒŒ ๋  ๊ฑฐ์ž„ ๐Ÿ˜Ž

iOS์—์„œ ์นด๋ฉ”๋ผ์™€ ๋ฏธ๋””์–ด๋ฅผ ๋‹ค๋ฃจ๋Š” ํ•ต์‹ฌ ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ๋ฐ”๋กœ AVFoundation์ด์•ผ.
Apple์ด iOS 4.0๋ถ€ํ„ฐ ์ œ๊ณตํ•ด์˜จ ๊ฐ•๋ ฅํ•œ ๋ฏธ๋””์–ด ์ฒ˜๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ์‚ฌ์ง„ ์ดฌ์˜๋ถ€ํ„ฐ ๋น„๋””์˜ค ๋…นํ™”, ์‹ค์‹œ๊ฐ„ ํ•„ํ„ฐ ์ ์šฉ, ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ๊นŒ์ง€ ๊ฑฐ์˜ ๋ชจ๋“  ๊ฑธ ์ปค๋ฒ„ํ•ด.
์ž, ๊ทธ๋Ÿผ ์ง„์งœ ์‹œ์ž‘ํ•ด๋ณด์ž๊ณ ! ๐Ÿš€


AVFoundation ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ AVCaptureSession ์„ธ์…˜ ๊ด€๋ฆฌ ยท ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ œ์–ด Input (์ž…๋ ฅ) AVCaptureDeviceInput Output (์ถœ๋ ฅ) Photo / Video / Data ๐Ÿ“ท ์‚ฌ์ง„ ์ดฌ์˜ AVCapturePhoto Output ๐ŸŽฌ ๋น„๋””์˜ค ๋…นํ™” AVCaptureMovie FileOutput ๐Ÿ”ด ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ AVCaptureVideo DataOutput AVCaptureVideoPreviewLayer ์‹ค์‹œ๊ฐ„ ์นด๋ฉ”๋ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
๐ŸŽฏ AVFoundation์ด ๋ญ”๋ฐ? ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์žก๊ณ  ๊ฐ€์ž

AVFoundation์€ Apple์ด ์ œ๊ณตํ•˜๋Š” ๋ฏธ๋””์–ด ์ฒ˜๋ฆฌ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ.
iOS, macOS, tvOS, watchOS ์ „๋ถ€ ์ง€์›ํ•˜๊ณ , ์˜ค๋””์˜คยท๋น„๋””์˜ค์˜ ์บก์ฒ˜, ์žฌ์ƒ, ํŽธ์ง‘, ๋‚ด๋ณด๋‚ด๊ธฐ๋ฅผ ๋ชจ๋‘ ๋‹ด๋‹นํ•ด.
์นด๋ฉ”๋ผ ์•ฑ ๋งŒ๋“ค ๋•Œ ์ด ํ”„๋ ˆ์ž„์›Œํฌ ์—†์ด๋Š” ์ง„์งœ ์•„๋ฌด๊ฒƒ๋„ ๋ชป ํ•œ๋‹ค๊ณ  ๋ด๋„ ๋จ ๐Ÿ˜…

AVFoundation์˜ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋จผ์ € ์•Œ์•„์•ผ ์ „์ฒด ๊ทธ๋ฆผ์ด ๊ทธ๋ ค์ง€๊ฑฐ๋“ .
๋งˆ์น˜ ๋ ˆ๊ณ  ๋ธ”๋ก์ฒ˜๋Ÿผ ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์กฐ๋ฆฝํ•ด์„œ ์นด๋ฉ”๋ผ ๊ธฐ๋Šฅ์„ ๋งŒ๋“œ๋Š” ๊ฑฐ์•ผ!

๐Ÿ“ฆ ํ•ต์‹ฌ ํด๋ž˜์Šค ํ•œ๋ˆˆ์— ๋ณด๊ธฐ
ํด๋ž˜์Šค๋ช… ์—ญํ• 
AVCaptureSession ์ „์ฒด ์บก์ฒ˜ ํŒŒ์ดํ”„๋ผ์ธ์˜ ์ค‘์‹ฌ. ์ž…๋ ฅ๊ณผ ์ถœ๋ ฅ์„ ์—ฐ๊ฒฐํ•˜๋Š” ํ—ˆ๋ธŒ
AVCaptureDevice ์‹ค์ œ ์นด๋ฉ”๋ผ/๋งˆ์ดํฌ ํ•˜๋“œ์›จ์–ด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ์ฒด
AVCaptureDeviceInput ๋””๋ฐ”์ด์Šค๋ฅผ ์„ธ์…˜์— ์—ฐ๊ฒฐํ•˜๋Š” ์ž…๋ ฅ ๊ฐ์ฒด
AVCapturePhotoOutput ์‚ฌ์ง„ ์ดฌ์˜ ์ถœ๋ ฅ ์ฒ˜๋ฆฌ
AVCaptureMovieFileOutput ๋น„๋””์˜ค ํŒŒ์ผ ๋…นํ™” ์ถœ๋ ฅ ์ฒ˜๋ฆฌ
AVCaptureVideoDataOutput ์‹ค์‹œ๊ฐ„ ๋น„๋””์˜ค ํ”„๋ ˆ์ž„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
AVCaptureVideoPreviewLayer ์นด๋ฉ”๋ผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๋Š” ๋ ˆ์ด์–ด
์ด๊ฑฐ ๋‹ค ์™ธ์›Œ์•ผ ํ•ด์š”? ๐Ÿ˜ฑ ๋„ˆ๋ฌด ๋งŽ์€๋ฐ...

ใ„ดใ„ด ์™ธ์šธ ํ•„์š” ์—†์–ดใ…‹ใ…‹ ๊ทธ๋ƒฅ "์•„ ์ด๋Ÿฐ ๊ฒŒ ์žˆ๊ตฌ๋‚˜" ํ•˜๊ณ  ํ๋ฆ„๋งŒ ํŒŒ์•…ํ•˜๋ฉด ๋ผ.
์ฝ”๋”ฉํ•˜๋‹ค ๋ณด๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ตํ˜€์ง€๊ฑฐ๋“ . ์ผ๋‹จ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ! ๐Ÿ’ช


๐Ÿ”ง ํ”„๋กœ์ ํŠธ ์„ธํŒ… - ๊ถŒํ•œ ์„ค์ •๋ถ€ํ„ฐ ์‹œ์ž‘

์นด๋ฉ”๋ผ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์ „์— ๋ฐ˜๋“œ์‹œ ํ•ด์•ผ ํ•˜๋Š” ๊ฒŒ ์žˆ์–ด.
๋ฐ”๋กœ Info.plist ๊ถŒํ•œ ์„ค์ •์ด์•ผ.
์ด๊ฑฐ ๋น ๋œจ๋ฆฌ๋ฉด ์•ฑ์ด ํฌ๋ž˜์‹œ ๋‚˜๊ฑฐ๋‚˜ ์นด๋ฉ”๋ผ๊ฐ€ ์•„์˜ˆ ์•ˆ ์—ด๋ ค์„œ ๋ฉ˜๋ถ• ์˜ด ๐Ÿ˜ค

๐Ÿ’ก Info.plist์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•  ํ‚ค๋“ค NSCameraUsageDescription - ์นด๋ฉ”๋ผ ์ ‘๊ทผ ๊ถŒํ•œ ์„ค๋ช…
NSMicrophoneUsageDescription - ๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ ์„ค๋ช… (๋น„๋””์˜ค ๋…นํ™” ์‹œ)
NSPhotoLibraryUsageDescription - ์‚ฌ์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ‘๊ทผ ๊ถŒํ•œ ์„ค๋ช…
NSPhotoLibraryAddUsageDescription - ์‚ฌ์ง„ ์ €์žฅ ๊ถŒํ•œ ์„ค๋ช…

๊ฐ ํ‚ค์— ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ์„ค๋ช… ๋ฌธ์ž์—ด์„ ๋„ฃ์–ด์ค˜์•ผ ํ•ด.
์˜ˆ: "์‚ฌ์ง„ ์ดฌ์˜์„ ์œ„ํ•ด ์นด๋ฉ”๋ผ ์ ‘๊ทผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."

Xcode 14 ์ดํ›„๋ถ€ํ„ฐ๋Š” Info.plist ํŒŒ์ผ์„ ์ง์ ‘ ํŽธ์ง‘ํ•˜๊ฑฐ๋‚˜
ํ”„๋กœ์ ํŠธ ์„ค์ • โ†’ Info ํƒญ์—์„œ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด.

๊ถŒํ•œ ์š”์ฒญ ์ฝ”๋“œ๋„ ์ž‘์„ฑํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด๋ ‡๊ฒŒ ํ•ด:

import AVFoundation

// ์นด๋ฉ”๋ผ ๊ถŒํ•œ ์š”์ฒญ
func requestCameraPermission() {
    let status = AVCaptureDevice.authorizationStatus(for: .video)
    
    switch status {
    case .authorized:
        // ์ด๋ฏธ ๊ถŒํ•œ ์žˆ์Œ - ๋ฐ”๋กœ ์„ธ์…˜ ์„ค์ • ์‹œ์ž‘
        setupCaptureSession()
        
    case .notDetermined:
        // ์•„์ง ์š”์ฒญ ์•ˆ ํ•จ - ๊ถŒํ•œ ์š”์ฒญ
        AVCaptureDevice.requestAccess(for: .video) { granted in
            if granted {
                DispatchQueue.main.async {
                    self.setupCaptureSession()
                }
            }
        }
        
    case .denied, .restricted:
        // ๊ฑฐ๋ถ€๋จ - ์„ค์ • ์•ฑ์œผ๋กœ ์•ˆ๋‚ด
        showPermissionAlert()
        
    @unknown default:
        break
    }
}
โš ๏ธ ์ฃผ์˜! ๊ถŒํ•œ ์š”์ฒญ ์ฝœ๋ฐฑ์€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์•„๋‹ ์ˆ˜ ์žˆ์–ด.
UI ์—…๋ฐ์ดํŠธ๋Š” ๋ฐ˜๋“œ์‹œ DispatchQueue.main.async๋กœ ๊ฐ์‹ธ์ค˜์•ผ ํ•ด!
์ด๊ฑฐ ๋น ๋œจ๋ฆฌ๋ฉด UI๊ฐ€ ์ด์ƒํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๋ฒ„๊ทธ ์ƒ๊น€ ๐Ÿ˜…

๐Ÿ“ท AVCaptureSession ์„ค์ • - ์นด๋ฉ”๋ผ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•

์ž, ์ด์ œ ์ง„์งœ ํ•ต์‹ฌ์ธ AVCaptureSession ์„ค์ •์ด์•ผ.
์ด๊ฒŒ ์นด๋ฉ”๋ผ ๊ธฐ๋Šฅ์˜ ์‹ฌ์žฅ ๊ฐ™์€ ์กด์žฌ๊ฑฐ๋“ .
์ž…๋ ฅ(์นด๋ฉ”๋ผ)๊ณผ ์ถœ๋ ฅ(์‚ฌ์ง„/๋น„๋””์˜ค)์„ ์—ฐ๊ฒฐํ•ด์ฃผ๋Š” ํŒŒ์ดํ”„๋ผ์ธ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ! ๐Ÿ”—

๐Ÿ—๏ธ ๊ธฐ๋ณธ ์„ธ์…˜ ๊ตฌ์„ฑ ์ฝ”๋“œ
import UIKit
import AVFoundation

class CameraViewController: UIViewController {
    
    // ํ•ต์‹ฌ ํ”„๋กœํผํ‹ฐ๋“ค
    private let captureSession = AVCaptureSession()
    private var videoDeviceInput: AVCaptureDeviceInput?
    private let photoOutput = AVCapturePhotoOutput()
    private var previewLayer: AVCaptureVideoPreviewLayer?
    
    // ์„ธ์…˜ ์„ค์ •์€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ์—์„œ ์‹คํ–‰ (์ค‘์š”!)
    private let sessionQueue = DispatchQueue(label: "session.queue")
    
    override func viewDidLoad() {
        super.viewDidLoad()
        requestCameraPermission()
    }
    
    func setupCaptureSession() {
        // ๋ฐ˜๋“œ์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ์—์„œ ์‹คํ–‰
        sessionQueue.async {
            self.configureCaptureSession()
        }
    }
    
    private func configureCaptureSession() {
        // ์„ธ์…˜ ์„ค์ • ์‹œ์ž‘
        captureSession.beginConfiguration()
        
        // ์„ธ์…˜ ํ”„๋ฆฌ์…‹ ์„ค์ • (ํ™”์งˆ ๊ฒฐ์ •)
        captureSession.sessionPreset = .photo
        
        // 1. ์นด๋ฉ”๋ผ ๋””๋ฐ”์ด์Šค ๊ฐ€์ ธ์˜ค๊ธฐ
        guard let videoDevice = AVCaptureDevice.default(
            .builtInWideAngleCamera,
            for: .video,
            position: .back
        ) else {
            print("์นด๋ฉ”๋ผ ๋””๋ฐ”์ด์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ ๐Ÿ˜ฑ")
            captureSession.commitConfiguration()
            return
        }
        
        // 2. ๋””๋ฐ”์ด์Šค ์ž…๋ ฅ ์ƒ์„ฑ
        do {
            let videoInput = try AVCaptureDeviceInput(device: videoDevice)
            
            if captureSession.canAddInput(videoInput) {
                captureSession.addInput(videoInput)
                self.videoDeviceInput = videoInput
            }
        } catch {
            print("์ž…๋ ฅ ์ƒ์„ฑ ์‹คํŒจ: \(error)")
            captureSession.commitConfiguration()
            return
        }
        
        // 3. ์‚ฌ์ง„ ์ถœ๋ ฅ ์ถ”๊ฐ€
        if captureSession.canAddOutput(photoOutput) {
            captureSession.addOutput(photoOutput)
            
            // HEIF ํฌ๋งท ์ง€์› ์—ฌ๋ถ€ ํ™•์ธ
            photoOutput.isHighResolutionCaptureEnabled = true
        }
        
        // ์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ
        captureSession.commitConfiguration()
        
        // ์„ธ์…˜ ์‹œ์ž‘
        captureSession.startRunning()
        
        // ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ํ”„๋ฆฌ๋ทฐ ๋ ˆ์ด์–ด ์„ค์ •
        DispatchQueue.main.async {
            self.setupPreviewLayer()
        }
    }
}
๐Ÿ’ก sessionQueue ์™œ ํ•„์š”ํ•ด? AVCaptureSession ๊ด€๋ จ ์ž‘์—…์€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ํ•˜๋ฉด UI๊ฐ€ ๋ธ”๋กœํ‚น๋ผ์„œ ์•ฑ์ด ๋ฒ„๋ฒ…๊ฑฐ๋ ค.
๊ทธ๋ž˜์„œ ๋ณ„๋„์˜ ์ง๋ ฌ ํ(serial queue)๋ฅผ ๋งŒ๋“ค์–ด์„œ ๊ฑฐ๊ธฐ์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ Apple ๊ถŒ์žฅ ๋ฐฉ์‹์ด์•ผ.
๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋Š” UI ์—…๋ฐ์ดํŠธ๋งŒ ๋‹ด๋‹นํ•˜๊ฒŒ ํ•˜๋Š” ๊ฑฐ์ง€! ๐ŸŽฏ
๐Ÿ–ฅ๏ธ ํ”„๋ฆฌ๋ทฐ ๋ ˆ์ด์–ด ์„ค์ •

์นด๋ฉ”๋ผ ํ™”๋ฉด์„ ์‹ค์ œ๋กœ ๋ณด์—ฌ์ฃผ๋ ค๋ฉด AVCaptureVideoPreviewLayer๋ฅผ ๋ทฐ์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด.
์ด๊ฒŒ ์—†์œผ๋ฉด ์นด๋ฉ”๋ผ๋Š” ๋Œ์•„๊ฐ€๋Š”๋ฐ ํ™”๋ฉด์— ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋ณด์ด๋Š” ์ƒํ™ฉ์ด ๋จใ…‹ใ…‹

private func setupPreviewLayer() {
    // ํ”„๋ฆฌ๋ทฐ ๋ ˆ์ด์–ด ์ƒ์„ฑ
    let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    previewLayer.videoGravity = .resizeAspectFill
    previewLayer.frame = view.bounds
    
    // ๋ทฐ์˜ ๋ ˆ์ด์–ด์— ์ถ”๊ฐ€
    view.layer.insertSublayer(previewLayer, at: 0)
    self.previewLayer = previewLayer
}

// ํ™”๋ฉด ํšŒ์ „ ์‹œ ํ”„๋ฆฌ๋ทฐ ๋ ˆ์ด์–ด ํฌ๊ธฐ ์—…๋ฐ์ดํŠธ
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    previewLayer?.frame = view.bounds
}

// ๋ทฐ๊ฐ€ ์‚ฌ๋ผ์งˆ ๋•Œ ์„ธ์…˜ ์ค‘์ง€ (๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ!)
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    sessionQueue.async {
        self.captureSession.stopRunning()
    }
}

// ๋ทฐ๊ฐ€ ๋‹ค์‹œ ๋‚˜ํƒ€๋‚  ๋•Œ ์„ธ์…˜ ์žฌ์‹œ์ž‘
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    sessionQueue.async {
        if !self.captureSession.isRunning {
            self.captureSession.startRunning()
        }
    }
}
๐ŸŽจ videoGravity ์˜ต์…˜ ๋น„๊ต
์˜ต์…˜ ์„ค๋ช…
.resizeAspectFill ํ™”๋ฉด์„ ๊ฝ‰ ์ฑ„์›€ (์ž˜๋ฆด ์ˆ˜ ์žˆ์Œ). ์ธ์Šคํƒ€๊ทธ๋žจ ์นด๋ฉ”๋ผ ์Šคํƒ€์ผ
.resizeAspect ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ์ „์ฒด ํ‘œ์‹œ (๋ ˆํ„ฐ๋ฐ•์Šค ์ƒ๊ธธ ์ˆ˜ ์žˆ์Œ)
.resize ๋น„์œจ ๋ฌด์‹œํ•˜๊ณ  ํ™”๋ฉด์— ๋งž์ถค (์™œ๊ณก ๋ฐœ์ƒ)

๐Ÿ“ธ ์‚ฌ์ง„ ์ดฌ์˜ ๊ตฌํ˜„ - AVCapturePhotoOutput

๋“œ๋””์–ด ์‹ค์ œ๋กœ ์‚ฌ์ง„์„ ์ฐ๋Š” ์ฝ”๋“œ์•ผ!
AVCapturePhotoOutput์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ณ ํ•ด์ƒ๋„ ์‚ฌ์ง„, HDR, Live Photo, RAW ํฌ๋งท๊นŒ์ง€ ๋‹ค ์ง€์›ํ•ด.
iOS 10๋ถ€ํ„ฐ ๊ธฐ์กด์˜ AVCaptureStillImageOutput์ด deprecated๋˜๊ณ  ์ด๊ฑธ๋กœ ๋Œ€์ฒด๋์–ด. ๐Ÿ“ธ

๐Ÿ“ท ์‚ฌ์ง„ ์ดฌ์˜ ํŠธ๋ฆฌ๊ฑฐ
// ์‚ฌ์ง„ ์ดฌ์˜ ๋ฒ„ํŠผ ์•ก์…˜
@IBAction func capturePhoto(_ sender: UIButton) {
    // ์ดฌ์˜ ์„ค์ • ๊ตฌ์„ฑ
    let photoSettings = AVCapturePhotoSettings()
    
    // ํ”Œ๋ž˜์‹œ ์„ค์ •
    if videoDeviceInput?.device.isFlashAvailable == true {
        photoSettings.flashMode = .auto
    }
    
    // ๊ณ ํ•ด์ƒ๋„ ์ดฌ์˜ ํ™œ์„ฑํ™”
    photoSettings.isHighResolutionPhotoEnabled = true
    
    // HEIF ํฌ๋งท ์‚ฌ์šฉ (iOS 11+, ์šฉ๋Ÿ‰ ์ ˆ์•ฝ)
    if photoOutput.availablePhotoCodecTypes.contains(.hevc) {
        let settings = AVCapturePhotoSettings(
            format: [AVVideoCodecKey: AVVideoCodecType.hevc]
        )
        photoOutput.capturePhoto(with: settings, delegate: self)
    } else {
        photoOutput.capturePhoto(with: photoSettings, delegate: self)
    }
}

// AVCapturePhotoCaptureDelegate ๊ตฌํ˜„
extension CameraViewController: AVCapturePhotoCaptureDelegate {
    
    // ์ดฌ์˜ ์™„๋ฃŒ ์ฝœ๋ฐฑ
    func photoOutput(
        _ output: AVCapturePhotoOutput,
        didFinishProcessingPhoto photo: AVCapturePhoto,
        error: Error?
    ) {
        if let error = error {
            print("์ดฌ์˜ ์‹คํŒจ: \(error) ๐Ÿ˜ญ")
            return
        }
        
        // ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
        guard let imageData = photo.fileDataRepresentation() else {
            print("์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ์‹คํŒจ")
            return
        }
        
        // UIImage๋กœ ๋ณ€ํ™˜
        guard let image = UIImage(data: imageData) else { return }
        
        // ์‚ฌ์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ €์žฅ
        saveToPhotoLibrary(image: image)
        
        // ๋˜๋Š” ์ด๋ฏธ์ง€๋ทฐ์— ํ‘œ์‹œ
        DispatchQueue.main.async {
            self.previewImageView.image = image
        }
    }
}
๐Ÿ’พ ์‚ฌ์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ €์žฅ
import Photos

func saveToPhotoLibrary(image: UIImage) {
    PHPhotoLibrary.requestAuthorization { status in
        guard status == .authorized else {
            print("์‚ฌ์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ ๐Ÿ˜ค")
            return
        }
        
        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.creationRequestForAsset(from: image)
        }) { success, error in
            if success {
                print("์‚ฌ์ง„ ์ €์žฅ ์™„๋ฃŒ! ๐ŸŽ‰")
            } else if let error = error {
                print("์ €์žฅ ์‹คํŒจ: \(error)")
            }
        }
    }
}
๐Ÿ’ก HEIF vs JPEG ๋ญ๊ฐ€ ๋‚˜์•„? HEIF(HEVC): iOS 11+์—์„œ ์ง€์›. ๊ฐ™์€ ํ™”์งˆ์—์„œ JPEG ๋Œ€๋น„ ์•ฝ 50% ์šฉ๋Ÿ‰ ์ ˆ์•ฝ.
iPhone ๊ธฐ๋ณธ ์นด๋ฉ”๋ผ ์•ฑ์ด ์‚ฌ์šฉํ•˜๋Š” ํฌ๋งท์ด์•ผ.

JPEG: ํ˜ธํ™˜์„ฑ ์ตœ๊ณ . ๋‹ค๋ฅธ ํ”Œ๋žซํผ์ด๋‚˜ ์„œ๋ฒ„์— ์—…๋กœ๋“œํ•  ๋•Œ ์•ˆ์ „ํ•œ ์„ ํƒ.

์•ฑ ์šฉ๋„์— ๋”ฐ๋ผ ์„ ํƒํ•˜๋ฉด ๋ผ. ์„œ๋ฒ„ ์—…๋กœ๋“œ๊ฐ€ ๋งŽ์œผ๋ฉด JPEG, ๋กœ์ปฌ ์ €์žฅ ์œ„์ฃผ๋ฉด HEIF ์ถ”์ฒœ! ๐Ÿ˜Š
๐ŸŒŸ Live Photo ์ดฌ์˜

Live Photo๋Š” ์‚ฌ์ง„ ์ฐ๊ธฐ ์ „ํ›„ 1.5์ดˆ ์˜์ƒ์„ ๊ฐ™์ด ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ์ด์•ผ.
iPhone 6s๋ถ€ํ„ฐ ์ง€์›๋๊ณ , ๊ตฌํ˜„๋„ ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ต์ง€ ์•Š์•„!

// Live Photo ์„ค์ •
func setupLivePhoto() {
    let photoSettings = AVCapturePhotoSettings()
    
    // Live Photo ์ง€์› ์—ฌ๋ถ€ ํ™•์ธ
    if photoOutput.isLivePhotoCaptureSupported {
        // ์ž„์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ ์„ค์ •
        let livePhotoMovieURL = URL(
            fileURLWithPath: NSTemporaryDirectory()
        ).appendingPathComponent("\(UUID().uuidString).mov")
        
        photoSettings.livePhotoMovieFileURL = livePhotoMovieURL
    }
    
    photoOutput.capturePhoto(with: photoSettings, delegate: self)
}

// Live Photo ์ฒ˜๋ฆฌ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ
func photoOutput(
    _ output: AVCapturePhotoOutput,
    didFinishRecordingLivePhotoMovieForEventualFileAt outputFileURL: URL,
    resolvedSettings: AVCaptureResolvedPhotoSettings
) {
    print("Live Photo ์˜์ƒ ๋…นํ™” ์™„๋ฃŒ: \(outputFileURL)")
}

func photoOutput(
    _ output: AVCapturePhotoOutput,
    didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL,
    duration: CMTime,
    photoDisplayTime: CMTime,
    resolvedSettings: AVCaptureResolvedPhotoSettings,
    error: Error?
) {
    if let error = error {
        print("Live Photo ์ฒ˜๋ฆฌ ์‹คํŒจ: \(error)")
        return
    }
    print("Live Photo ์™„์„ฑ! ๐ŸŽฌ ๊ฒฝ๋กœ: \(outputFileURL)")
}

๐ŸŽฌ ๋น„๋””์˜ค ๋…นํ™” ํ”Œ๋กœ์šฐ โ‘  ์„ธ์…˜ ์„ค์ • MovieFileOutput ์ถ”๊ฐ€ โ‘ก ๋…นํ™” ์‹œ์ž‘ startRecording ํ˜ธ์ถœ โ‘ข ๋…นํ™” ์ค‘ isRecording = true โ‘ฃ ๋…นํ™” ์ข…๋ฃŒ stopRecording ํ˜ธ์ถœ โ‘ค ์™„๋ฃŒ ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌ fileOutput(_:didFinishRecordingTo:from:error:) ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฉ”์„œ๋“œ์—์„œ ํŒŒ์ผ ์ฒ˜๋ฆฌ ์ž„์‹œ ๊ฒฝ๋กœ โ†’ ์‚ฌ์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ €์žฅ ๋˜๋Š” ์„œ๋ฒ„ ์—…๋กœ๋“œ
๐ŸŽฌ ๋น„๋””์˜ค ๋…นํ™” ๊ตฌํ˜„ - AVCaptureMovieFileOutput

์‚ฌ์ง„ ์ฐ๋Š” ๊ฑด ํ•ด๋ดค์œผ๋‹ˆ ์ด์ œ ๋น„๋””์˜ค ๋…นํ™”์•ผ!
AVCaptureMovieFileOutput์„ ์‚ฌ์šฉํ•˜๋ฉด MP4/MOV ํŒŒ์ผ๋กœ ์ง์ ‘ ์ €์žฅํ•  ์ˆ˜ ์žˆ์–ด.
์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ์—†์ด ํŒŒ์ผ๋กœ ๋ฐ”๋กœ ์ €์žฅํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์ด์ง€ ๐Ÿ˜Š

class VideoRecordingViewController: UIViewController {
    
    private let captureSession = AVCaptureSession()
    private let movieOutput = AVCaptureMovieFileOutput()
    private let sessionQueue = DispatchQueue(label: "video.session.queue")
    
    func setupVideoSession() {
        sessionQueue.async {
            self.captureSession.beginConfiguration()
            self.captureSession.sessionPreset = .high
            
            // ๋น„๋””์˜ค ์ž…๋ ฅ ์ถ”๊ฐ€
            guard let videoDevice = AVCaptureDevice.default(
                .builtInWideAngleCamera,
                for: .video,
                position: .back
            ),
            let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
            self.captureSession.canAddInput(videoInput) else { return }
            
            self.captureSession.addInput(videoInput)
            
            // ์˜ค๋””์˜ค ์ž…๋ ฅ ์ถ”๊ฐ€ (๋งˆ์ดํฌ)
            guard let audioDevice = AVCaptureDevice.default(for: .audio),
            let audioInput = try? AVCaptureDeviceInput(device: audioDevice),
            self.captureSession.canAddInput(audioInput) else { return }
            
            self.captureSession.addInput(audioInput)
            
            // ๋น„๋””์˜ค ํŒŒ์ผ ์ถœ๋ ฅ ์ถ”๊ฐ€
            if self.captureSession.canAddOutput(self.movieOutput) {
                self.captureSession.addOutput(self.movieOutput)
                
                // ๋น„๋””์˜ค ์•ˆ์ •ํ™” ์„ค์ • (์†๋–จ๋ฆผ ๋ณด์ •)
                if let connection = self.movieOutput.connection(with: .video) {
                    if connection.isVideoStabilizationSupported {
                        connection.preferredVideoStabilizationMode = .auto
                    }
                }
            }
            
            self.captureSession.commitConfiguration()
            self.captureSession.startRunning()
        }
    }
    
    // ๋…นํ™” ์‹œ์ž‘
    func startRecording() {
        guard !movieOutput.isRecording else { return }
        
        // ์ž„์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ ์ƒ์„ฑ
        let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent("\(UUID().uuidString).mov")
        
        movieOutput.startRecording(to: outputURL, recordingDelegate: self)
        print("๋…นํ™” ์‹œ์ž‘! ๐Ÿ”ด")
    }
    
    // ๋…นํ™” ์ข…๋ฃŒ
    func stopRecording() {
        guard movieOutput.isRecording else { return }
        movieOutput.stopRecording()
        print("๋…นํ™” ์ข…๋ฃŒ! โน๏ธ")
    }
}

// ๋…นํ™” ์™„๋ฃŒ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ
extension VideoRecordingViewController: AVCaptureFileOutputRecordingDelegate {
    
    func fileOutput(
        _ output: AVCaptureFileOutput,
        didFinishRecordingTo outputFileURL: URL,
        from connections: [AVCaptureConnection],
        error: Error?
    ) {
        if let error = error {
            print("๋…นํ™” ์‹คํŒจ: \(error) ๐Ÿ˜ญ")
            return
        }
        
        print("๋…นํ™” ์™„๋ฃŒ! ํŒŒ์ผ ๊ฒฝ๋กœ: \(outputFileURL)")
        
        // ์‚ฌ์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ €์žฅ
        PHPhotoLibrary.requestAuthorization { status in
            if status == .authorized {
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(
                        atFileURL: outputFileURL
                    )
                }) { success, error in
                    // ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
                    try? FileManager.default.removeItem(at: outputFileURL)
                    
                    if success {
                        print("๋น„๋””์˜ค ์ €์žฅ ์™„๋ฃŒ! ๐ŸŽ‰")
                    }
                }
            }
        }
    }
}
โš ๏ธ ๋…นํ™” ์‹œ๊ฐ„ ์ œํ•œ ์„ค์ • movieOutput.maxRecordedDuration์œผ๋กœ ์ตœ๋Œ€ ๋…นํ™” ์‹œ๊ฐ„์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด.
movieOutput.maxRecordedFileSize๋กœ ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ๋„ ์ œํ•œ ๊ฐ€๋Šฅ.
๋ฌด์ œํ•œ์œผ๋กœ ๋‘๋ฉด ์ €์žฅ ๊ณต๊ฐ„ ๋‹ค ์žก์•„๋จน์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜ํ•ด! ๐Ÿ˜…

๐Ÿ”ด ์‹ค์‹œ๊ฐ„ ๋น„๋””์˜ค ์ฒ˜๋ฆฌ - AVCaptureVideoDataOutput

์ด์ œ ์ง„์งœ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์ด์•ผ!
AVCaptureVideoDataOutput์„ ์‚ฌ์šฉํ•˜๋ฉด ์นด๋ฉ”๋ผ์—์„œ ๋“ค์–ด์˜ค๋Š” ๊ฐ ํ”„๋ ˆ์ž„์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.
ํ•„ํ„ฐ ์ ์šฉ, ์–ผ๊ตด ์ธ์‹, AR ํšจ๊ณผ, ๋จธ์‹ ๋Ÿฌ๋‹ ์ถ”๋ก  ๋“ฑ ๋‹ค ์—ฌ๊ธฐ์„œ ์ด๋ฃจ์–ด์ง€๊ฑฐ๋“ .
์ธ์Šคํƒ€๊ทธ๋žจ ํ•„ํ„ฐ, ์Šค๋…ธ์šฐ ์•ฑ ๊ฐ™์€ ๊ฒŒ ๋‹ค ์ด ๋ฐฉ์‹์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ๊ฑฐ์•ผ ๐Ÿ˜Ž

์˜ค ๊ทธ๋Ÿผ ์‹ค์‹œ๊ฐ„ ํ•„ํ„ฐ ์•ฑ๋„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒ ๋„ค์š”?? ์™„์ „ ์‹ ๊ธฐใ…‹ใ…‹

๋งž์•„! ๊ทธ๋ฆฌ๊ณ  Vision ํ”„๋ ˆ์ž„์›Œํฌ๋‚˜ Core ML์ด๋ž‘ ์—ฐ๋™ํ•˜๋ฉด ์‹ค์‹œ๊ฐ„ ๊ฐ์ฒด ์ธ์‹๋„ ๊ฐ€๋Šฅํ•ด.
์ผ๋‹จ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ถ€ํ„ฐ ๋ณด์ž!

class RealtimeVideoProcessor: NSObject {
    
    private let captureSession = AVCaptureSession()
    private let videoDataOutput = AVCaptureVideoDataOutput()
    
    // ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ์šฉ ๋ณ„๋„ ํ (๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ธ”๋กœํ‚น ๋ฐฉ์ง€)
    private let videoDataOutputQueue = DispatchQueue(
        label: "video.data.output",
        qos: .userInitiated,
        attributes: [],
        autoreleaseFrequency: .workItem
    )
    
    func setupRealtimeCapture() {
        captureSession.beginConfiguration()
        captureSession.sessionPreset = .high
        
        // ์นด๋ฉ”๋ผ ์ž…๋ ฅ ์„ค์ •
        guard let videoDevice = AVCaptureDevice.default(
            .builtInWideAngleCamera,
            for: .video,
            position: .front  // ์ „๋ฉด ์นด๋ฉ”๋ผ
        ),
        let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
        captureSession.canAddInput(videoInput) else { return }
        
        captureSession.addInput(videoInput)
        
        // ๋น„๋””์˜ค ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ ์„ค์ •
        videoDataOutput.videoSettings = [
            kCVPixelBufferPixelFormatTypeKey as String:
                kCVPixelFormatType_32BGRA  // BGRA ํฌ๋งท
        ]
        
        // ์ฒ˜๋ฆฌ ๋ชปํ•œ ํ”„๋ ˆ์ž„์€ ๋“œ๋กญ (์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ์œ ์ง€)
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        
        // ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ์„ค์ •
        videoDataOutput.setSampleBufferDelegate(
            self,
            queue: videoDataOutputQueue
        )
        
        if captureSession.canAddOutput(videoDataOutput) {
            captureSession.addOutput(videoDataOutput)
        }
        
        captureSession.commitConfiguration()
        captureSession.startRunning()
    }
}

// ์‹ค์‹œ๊ฐ„ ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ
extension RealtimeVideoProcessor: AVCaptureVideoDataOutputSampleBufferDelegate {
    
    func captureOutput(
        _ output: AVCaptureOutput,
        didOutput sampleBuffer: CMSampleBuffer,
        from connection: AVCaptureConnection
    ) {
        // ์—ฌ๊ธฐ์„œ ๊ฐ ํ”„๋ ˆ์ž„์„ ์ฒ˜๋ฆฌ!
        // sampleBuffer์—์„œ ํ”ฝ์…€ ๋ฒ„ํผ ์ถ”์ถœ
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        
        // CIImage๋กœ ๋ณ€ํ™˜ (Core Image ํ•„ํ„ฐ ์ ์šฉ ๊ฐ€๋Šฅ)
        let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
        
        // ์˜ˆ: ํ‘๋ฐฑ ํ•„ํ„ฐ ์ ์šฉ
        let filter = CIFilter(name: "CIPhotoEffectMono")!
        filter.setValue(ciImage, forKey: kCIInputImageKey)
        
        if let outputImage = filter.outputImage {
            // ์ฒ˜๋ฆฌ๋œ ์ด๋ฏธ์ง€๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œ
            DispatchQueue.main.async {
                // MTKView๋‚˜ GLKView์— ๋ Œ๋”๋ง
                self.renderFilteredImage(outputImage)
            }
        }
        
        // Vision ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ์–ผ๊ตด ๊ฐ์ง€
        detectFaces(in: pixelBuffer)
    }
    
    // ํ”„๋ ˆ์ž„ ๋“œ๋กญ ์•Œ๋ฆผ
    func captureOutput(
        _ output: AVCaptureOutput,
        didDrop sampleBuffer: CMSampleBuffer,
        from connection: AVCaptureConnection
    ) {
        // ์ฒ˜๋ฆฌ ์†๋„๊ฐ€ ๋А๋ ค์„œ ํ”„๋ ˆ์ž„ ๋“œ๋กญ ๋ฐœ์ƒ
        // ์ฒ˜๋ฆฌ ๋กœ์ง ์ตœ์ ํ™” ํ•„์š” ์‹ ํ˜ธ!
        print("โš ๏ธ ํ”„๋ ˆ์ž„ ๋“œ๋กญ ๋ฐœ์ƒ!")
    }
}
๐Ÿ‘๏ธ Vision ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ์–ผ๊ตด ๊ฐ์ง€
import Vision

func detectFaces(in pixelBuffer: CVPixelBuffer) {
    // ์–ผ๊ตด ๊ฐ์ง€ ์š”์ฒญ ์ƒ์„ฑ
    let faceDetectionRequest = VNDetectFaceRectanglesRequest { request, error in
        if let error = error {
            print("์–ผ๊ตด ๊ฐ์ง€ ์˜ค๋ฅ˜: \(error)")
            return
        }
        
        guard let observations = request.results as? [VNFaceObservation] else {
            return
        }
        
        DispatchQueue.main.async {
            // ๊ฐ์ง€๋œ ์–ผ๊ตด ์ˆ˜ ์ถœ๋ ฅ
            print("๊ฐ์ง€๋œ ์–ผ๊ตด: \(observations.count)๊ฐœ ๐Ÿ˜Š")
            
            // ๊ฐ ์–ผ๊ตด์˜ ์œ„์น˜ ์ •๋ณด ์ฒ˜๋ฆฌ
            for face in observations {
                // boundingBox๋Š” 0~1 ์‚ฌ์ด ์ •๊ทœํ™”๋œ ์ขŒํ‘œ
                let boundingBox = face.boundingBox
                print("์–ผ๊ตด ์œ„์น˜: \(boundingBox)")
                
                // ํ™”๋ฉด ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ํ•ด์„œ ๋ฐ•์Šค ๊ทธ๋ฆฌ๊ธฐ
                self.drawFaceBox(boundingBox: boundingBox)
            }
        }
    }
    
    // ์š”์ฒญ ์‹คํ–‰
    let handler = VNImageRequestHandler(
        cvPixelBuffer: pixelBuffer,
        orientation: .up,
        options: [:]
    )
    
    do {
        try handler.perform([faceDetectionRequest])
    } catch {
        print("Vision ์š”์ฒญ ์‹คํŒจ: \(error)")
    }
}
๐Ÿ’ก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ! ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ์—์„œ ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•ด. ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ์•Œ๋ ค์ค„๊ฒŒ:

1. ํ•ด์ƒ๋„ ๋‚ฎ์ถ”๊ธฐ: ๋จธ์‹ ๋Ÿฌ๋‹ ์ถ”๋ก ์€ 640ร—480์œผ๋กœ๋„ ์ถฉ๋ถ„ํ•œ ๊ฒฝ์šฐ ๋งŽ์•„.
2. ํ”„๋ ˆ์ž„ ์Šคํ‚ต: ๋งค ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ ๋Œ€์‹  2~3ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ฒ˜๋ฆฌํ•ด๋„ ๋ผ.
3. Metal ํ™œ์šฉ: Core Image ํ•„ํ„ฐ๋Š” GPU์—์„œ ์‹คํ–‰๋˜๋‹ˆ Metal ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง ์‚ฌ์šฉ.
4. autoreleaseFrequency: ํ ์ƒ์„ฑ ์‹œ .workItem์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ํšจ์œจ์ .

โš™๏ธ ์นด๋ฉ”๋ผ ์ œ์–ด - ํฌ์ปค์Šค, ๋…ธ์ถœ, ์คŒ

์นด๋ฉ”๋ผ ์•ฑ์—์„œ ํƒญํ•ด์„œ ํฌ์ปค์Šค ๋งž์ถ”๊ฑฐ๋‚˜, ํ•€์น˜๋กœ ์คŒํ•˜๋Š” ๊ธฐ๋Šฅ ์žˆ์ž–์•„.
๊ทธ๊ฑฐ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ์ƒ๊ฐ๋ณด๋‹ค ์ฝ”๋“œ๊ฐ€ ๊น”๋”ํ•ด์„œ ๋†€๋ž„ ๊ฑฐ์•ผ ๐Ÿ˜„

๐ŸŽฏ ํƒญ ํฌ์ปค์Šค ๊ตฌํ˜„
// ํƒญ ์ œ์Šค์ฒ˜๋กœ ํฌ์ปค์Šค ์„ค์ •
@objc func handleTapToFocus(_ gesture: UITapGestureRecognizer) {
    let tapPoint = gesture.location(in: view)
    
    // ํ™”๋ฉด ์ขŒํ‘œ๋ฅผ ์นด๋ฉ”๋ผ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
    guard let devicePoint = previewLayer?.captureDevicePointConverted(
        fromLayerPoint: tapPoint
    ) else { return }
    
    focusAt(point: devicePoint)
}

func focusAt(point: CGPoint) {
    guard let device = videoDeviceInput?.device else { return }
    
    do {
        try device.lockForConfiguration()
        
        // ํฌ์ปค์Šค ์„ค์ •
        if device.isFocusPointOfInterestSupported {
            device.focusPointOfInterest = point
            device.focusMode = .autoFocus
        }
        
        // ๋…ธ์ถœ ์„ค์ •
        if device.isExposurePointOfInterestSupported {
            device.exposurePointOfInterest = point
            device.exposureMode = .autoExpose
        }
        
        // ํ™”์ดํŠธ๋ฐธ๋Ÿฐ์Šค ์ž๋™ ์„ค์ •
        if device.isWhiteBalanceModeSupported(.autoWhiteBalance) {
            device.whiteBalanceMode = .autoWhiteBalance
        }
        
        device.unlockForConfiguration()
        
    } catch {
        print("์นด๋ฉ”๋ผ ์„ค์ • ์‹คํŒจ: \(error)")
    }
}

// ํ•€์น˜ ์คŒ ๊ตฌํ˜„
@objc func handlePinchZoom(_ gesture: UIPinchGestureRecognizer) {
    guard let device = videoDeviceInput?.device else { return }
    
    let maxZoom = min(device.activeFormat.videoMaxZoomFactor, 10.0)
    let currentZoom = device.videoZoomFactor
    
    if gesture.state == .changed {
        let newZoom = max(1.0, min(currentZoom * gesture.scale, maxZoom))
        
        do {
            try device.lockForConfiguration()
            device.videoZoomFactor = newZoom
            device.unlockForConfiguration()
        } catch {
            print("์คŒ ์„ค์ • ์‹คํŒจ: \(error)")
        }
        
        gesture.scale = 1.0  // ์Šค์ผ€์ผ ๋ฆฌ์…‹
    }
}
๐Ÿ’ก ๋…ธ์ถœ ๋ณด์ • (EV ์กฐ์ ˆ)
// ๋…ธ์ถœ ๋ณด์ •๊ฐ’ ์„ค์ • (-3.0 ~ +3.0 EV)
func setExposureCompensation(_ value: Float) {
    guard let device = videoDeviceInput?.device else { return }
    
    let clampedValue = max(
        device.minExposureTargetBias,
        min(value, device.maxExposureTargetBias)
    )
    
    do {
        try device.lockForConfiguration()
        device.setExposureTargetBias(clampedValue) { _ in
            print("๋…ธ์ถœ ๋ณด์ • ์™„๋ฃŒ: \(clampedValue) EV")
        }
        device.unlockForConfiguration()
    } catch {
        print("๋…ธ์ถœ ์„ค์ • ์‹คํŒจ: \(error)")
    }
}

// ์ „/ํ›„๋ฉด ์นด๋ฉ”๋ผ ์ „ํ™˜
func switchCamera() {
    guard let currentInput = videoDeviceInput else { return }
    
    let currentPosition = currentInput.device.position
    let newPosition: AVCaptureDevice.Position = 
        currentPosition == .back ? .front : .back
    
    guard let newDevice = AVCaptureDevice.default(
        .builtInWideAngleCamera,
        for: .video,
        position: newPosition
    ) else { return }
    
    sessionQueue.async {
        do {
            let newInput = try AVCaptureDeviceInput(device: newDevice)
            
            self.captureSession.beginConfiguration()
            self.captureSession.removeInput(currentInput)
            
            if self.captureSession.canAddInput(newInput) {
                self.captureSession.addInput(newInput)
                self.videoDeviceInput = newInput
            }
            
            self.captureSession.commitConfiguration()
            print("์นด๋ฉ”๋ผ ์ „ํ™˜ ์™„๋ฃŒ! ๐Ÿ”„")
            
        } catch {
            print("์นด๋ฉ”๋ผ ์ „ํ™˜ ์‹คํŒจ: \(error)")
        }
    }
}

๐ŸŽจ Core Image๋กœ ์‹ค์‹œ๊ฐ„ ํ•„ํ„ฐ ์ ์šฉ

์ด์ œ ์ง„์งœ ์žฌ๋ฐŒ๋Š” ๋ถ€๋ถ„์ด์•ผ!
Core Image ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ AVFoundation๊ณผ ์—ฐ๋™ํ•˜๋ฉด ์‹ค์‹œ๊ฐ„ ํ•„ํ„ฐ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด.
Apple์ด ์ œ๊ณตํ•˜๋Š” ๋นŒํŠธ์ธ ํ•„ํ„ฐ๋งŒ 200๊ฐœ๊ฐ€ ๋„˜๊ฑฐ๋“  ๐Ÿ˜ฒ
์ธ์Šคํƒ€๊ทธ๋žจ ํ•„ํ„ฐ ๊ฐ™์€ ๊ฑฐ ์ง์ ‘ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๊ณ !

import CoreImage
import MetalKit

class FilterCameraViewController: UIViewController {
    
    // Metal ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง ๋ทฐ (์„ฑ๋Šฅ ์ตœ๊ณ !)
    private var metalView: MTKView!
    private var ciContext: CIContext!
    private var commandQueue: MTLCommandQueue!
    
    // ํ˜„์žฌ ์ ์šฉ ์ค‘์ธ ํ•„ํ„ฐ
    private var currentFilter: CIFilter?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupMetal()
        setupCamera()
    }
    
    private func setupMetal() {
        guard let device = MTLCreateSystemDefaultDevice() else { return }
        
        metalView = MTKView(frame: view.bounds, device: device)
        metalView.framebufferOnly = false
        metalView.isPaused = true
        metalView.enableSetNeedsDisplay = false
        view.insertSubview(metalView, at: 0)
        
        commandQueue = device.makeCommandQueue()
        ciContext = CIContext(mtlDevice: device)
    }
    
    // ํ•„ํ„ฐ ์ ์šฉ ๋ฐ ๋ Œ๋”๋ง
    func renderFilteredImage(_ image: CIImage) {
        guard let drawable = metalView.currentDrawable,
              let commandBuffer = commandQueue.makeCommandBuffer() else { return }
        
        // ํ•„ํ„ฐ ์ ์šฉ
        var filteredImage = image
        
        if let filter = currentFilter {
            filter.setValue(image, forKey: kCIInputImageKey)
            filteredImage = filter.outputImage ?? image
        }
        
        // Metal drawable์— ๋ Œ๋”๋ง
        let renderDestination = CIRenderDestination(
            width: Int(metalView.drawableSize.width),
            height: Int(metalView.drawableSize.height),
            pixelFormat: metalView.colorPixelFormat,
            commandBuffer: commandBuffer
        ) { () -> MTLTexture in
            return drawable.texture
        }
        
        try? ciContext.startTask(toRender: filteredImage, to: renderDestination)
        
        commandBuffer.present(drawable)
        commandBuffer.commit()
    }
    
    // ํ•„ํ„ฐ ๋ณ€๊ฒฝ
    func applyFilter(named filterName: String) {
        currentFilter = CIFilter(name: filterName)
        print("ํ•„ํ„ฐ ์ ์šฉ: \(filterName) โœจ")
    }
}

// ์ธ๊ธฐ ํ•„ํ„ฐ ๋ชฉ๋ก ์˜ˆ์‹œ
extension FilterCameraViewController {
    
    func applyVintageFilter() {
        // ๋นˆํ‹ฐ์ง€ ํšจ๊ณผ (์—ฌ๋Ÿฌ ํ•„ํ„ฐ ์ฒด์ด๋‹)
        let sepia = CIFilter(name: "CISepiaTone")!
        sepia.setValue(0.8, forKey: kCIInputIntensityKey)
        currentFilter = sepia
    }
    
    func applyBlurFilter(radius: Float = 10.0) {
        // ๊ฐ€์šฐ์‹œ์•ˆ ๋ธ”๋Ÿฌ
        let blur = CIFilter(name: "CIGaussianBlur")!
        blur.setValue(radius, forKey: kCIInputRadiusKey)
        currentFilter = blur
    }
    
    func applyColorInvertFilter() {
        // ์ƒ‰์ƒ ๋ฐ˜์ „
        currentFilter = CIFilter(name: "CIColorInvert")
    }
    
    func applyVignetteFilter() {
        // ๋น„๋„คํŒ… ํšจ๊ณผ
        let vignette = CIFilter(name: "CIVignette")!
        vignette.setValue(1.5, forKey: kCIInputIntensityKey)
        vignette.setValue(1.0, forKey: kCIInputRadiusKey)
        currentFilter = vignette
    }
}
๐ŸŽจ ์ž์ฃผ ์“ฐ๋Š” Core Image ํ•„ํ„ฐ ๋ชจ์Œ
ํ•„ํ„ฐ ์ด๋ฆ„ ํšจ๊ณผ ์ฃผ์š” ํŒŒ๋ผ๋ฏธํ„ฐ
CISepiaTone ์„ธํ”ผ์•„ ๋นˆํ‹ฐ์ง€ ํšจ๊ณผ inputIntensity (0~1)
CIPhotoEffectMono ํ‘๋ฐฑ ๋ณ€ํ™˜ ์—†์Œ
CIGaussianBlur ๊ฐ€์šฐ์‹œ์•ˆ ๋ธ”๋Ÿฌ inputRadius
CIVignette ๋น„๋„คํŒ… (๊ฐ€์žฅ์ž๋ฆฌ ์–ด๋‘ก๊ฒŒ) inputIntensity, inputRadius
CIColorControls ๋ฐ๊ธฐ/๋Œ€๋น„/์ฑ„๋„ ์กฐ์ ˆ inputBrightness, inputContrast, inputSaturation
CIPhotoEffectChrome ํฌ๋กฌ ํ•„ํ„ฐ (์„ ๋ช…ํ•œ ์ƒ‰๊ฐ) ์—†์Œ
CIBloom ๋น› ๋ฒˆ์ง ํšจ๊ณผ inputRadius, inputIntensity

๐ŸŽฅ AVAsset์œผ๋กœ ๋น„๋””์˜ค ํŽธ์ง‘ํ•˜๊ธฐ

์ดฌ์˜ํ•œ ๋น„๋””์˜ค๋ฅผ ํŽธ์ง‘ํ•˜๋Š” ๊ฒƒ๋„ AVFoundation์œผ๋กœ ํ•  ์ˆ˜ ์žˆ์–ด!
AVAsset, AVMutableComposition์„ ์‚ฌ์šฉํ•˜๋ฉด ํŠธ๋ฆฌ๋ฐ, ํ•ฉ์น˜๊ธฐ, ํ…์ŠคํŠธ ์˜ค๋ฒ„๋ ˆ์ด๊นŒ์ง€ ๊ฐ€๋Šฅํ•ด.
์ง„์งœ ๋ฏธ๋‹ˆ ์˜์ƒ ํŽธ์ง‘ ์•ฑ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๊ฑฐ์ง€ ๐ŸŽฌ

โœ‚๏ธ ๋น„๋””์˜ค ํŠธ๋ฆฌ๋ฐ
import AVFoundation

func trimVideo(
    inputURL: URL,
    startTime: Double,
    endTime: Double,
    completion: @escaping (URL?) -> Void
) {
    let asset = AVAsset(url: inputURL)
    
    // ๋‚ด๋ณด๋‚ด๊ธฐ ์„ธ์…˜ ์ƒ์„ฑ
    guard let exportSession = AVAssetExportSession(
        asset: asset,
        presetName: AVAssetExportPresetHighestQuality
    ) else {
        completion(nil)
        return
    }
    
    // ์ถœ๋ ฅ ํŒŒ์ผ ๊ฒฝ๋กœ
    let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent("\(UUID().uuidString).mp4")
    
    // ํŠธ๋ฆฌ๋ฐ ๋ฒ”์œ„ ์„ค์ •
    let startCMTime = CMTime(seconds: startTime, preferredTimescale: 600)
    let endCMTime = CMTime(seconds: endTime, preferredTimescale: 600)
    let timeRange = CMTimeRange(start: startCMTime, end: endCMTime)
    
    exportSession.outputURL = outputURL
    exportSession.outputFileType = .mp4
    exportSession.timeRange = timeRange
    
    // ๋‚ด๋ณด๋‚ด๊ธฐ ์‹คํ–‰
    exportSession.exportAsynchronously {
        switch exportSession.status {
        case .completed:
            print("ํŠธ๋ฆฌ๋ฐ ์™„๋ฃŒ! โœ‚๏ธ")
            completion(outputURL)
        case .failed:
            print("ํŠธ๋ฆฌ๋ฐ ์‹คํŒจ: \(exportSession.error?.localizedDescription ?? "")")
            completion(nil)
        case .cancelled:
            print("ํŠธ๋ฆฌ๋ฐ ์ทจ์†Œ๋จ")
            completion(nil)
        default:
            break
        }
    }
}

// ๋น„๋””์˜ค ํ•ฉ์น˜๊ธฐ (Composition)
func mergeVideos(
    urls: [URL],
    completion: @escaping (URL?) -> Void
) {
    let composition = AVMutableComposition()
    
    guard let videoTrack = composition.addMutableTrack(
        withMediaType: .video,
        preferredTrackID: kCMPersistentTrackID_Invalid
    ),
    let audioTrack = composition.addMutableTrack(
        withMediaType: .audio,
        preferredTrackID: kCMPersistentTrackID_Invalid
    ) else { return }
    
    var currentTime = CMTime.zero
    
    for url in urls {
        let asset = AVAsset(url: url)
        let duration = asset.duration
        let timeRange = CMTimeRange(start: .zero, duration: duration)
        
        do {
            // ๋น„๋””์˜ค ํŠธ๋ž™ ์ถ”๊ฐ€
            if let assetVideoTrack = asset.tracks(withMediaType: .video).first {
                try videoTrack.insertTimeRange(
                    timeRange,
                    of: assetVideoTrack,
                    at: currentTime
                )
            }
            
            // ์˜ค๋””์˜ค ํŠธ๋ž™ ์ถ”๊ฐ€
            if let assetAudioTrack = asset.tracks(withMediaType: .audio).first {
                try audioTrack.insertTimeRange(
                    timeRange,
                    of: assetAudioTrack,
                    at: currentTime
                )
            }
            
            currentTime = CMTimeAdd(currentTime, duration)
            
        } catch {
            print("ํŠธ๋ž™ ์ถ”๊ฐ€ ์‹คํŒจ: \(error)")
        }
    }
    
    // ํ•ฉ์ณ์ง„ ๋น„๋””์˜ค ๋‚ด๋ณด๋‚ด๊ธฐ
    let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent("\(UUID().uuidString).mp4")
    
    guard let exportSession = AVAssetExportSession(
        asset: composition,
        presetName: AVAssetExportPresetHighestQuality
    ) else { return }
    
    exportSession.outputURL = outputURL
    exportSession.outputFileType = .mp4
    
    exportSession.exportAsynchronously {
        if exportSession.status == .completed {
            print("๋น„๋””์˜ค ํ•ฉ์น˜๊ธฐ ์™„๋ฃŒ! ๐ŸŽฌ")
            completion(outputURL)
        } else {
            completion(nil)
        }
    }
}

๐Ÿš€ iOS 17+ ์ตœ์‹  ๊ธฐ๋Šฅ - AVCaptureSession ์—…๋ฐ์ดํŠธ

Apple์€ ๋งค๋…„ AVFoundation์„ ์—…๋ฐ์ดํŠธํ•˜๊ฑฐ๋“ .
์ตœ์‹  iOS์—์„œ ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ๋“ค๋„ ์•Œ์•„๋‘๋ฉด ์ข‹์•„! ๐Ÿ“ฑ

โœจ ์ตœ์‹  ์ฃผ์š” ์—…๋ฐ์ดํŠธ
๋ฒ„์ „ ์ฃผ์š” ๊ธฐ๋Šฅ
iOS 16+ Cinematic Mode API ๊ณต๊ฐœ, AVCaptureDevice.RotationCoordinator ์ถ”๊ฐ€
iOS 17+ Deferred Photo Delivery, ํ–ฅ์ƒ๋œ ํฌํŠธ๋ ˆ์ดํŠธ ๋ชจ๋“œ API
iOS 18+ Camera Control API, ํ–ฅ์ƒ๋œ ProRes ์ง€์›
๐ŸŽฌ Cinematic Mode (์‹œ๋„ค๋งˆํ‹ฑ ๋ชจ๋“œ) ํ™œ์šฉ
// iOS 16+ Cinematic Mode ์ง€์› ํ™•์ธ
if #available(iOS 16.0, *) {
    // ์‹œ๋„ค๋งˆํ‹ฑ ๋ชจ๋“œ ์ง€์› ์—ฌ๋ถ€ ํ™•์ธ
    let cinematicFormats = videoDevice.formats.filter { format in
        format.isCinematicVideoCaptureSupported
    }
    
    if !cinematicFormats.isEmpty {
        print("์‹œ๋„ค๋งˆํ‹ฑ ๋ชจ๋“œ ์ง€์›! ๐ŸŽฌ")
        
        // ์‹œ๋„ค๋งˆํ‹ฑ ํฌ๋งท ์„ค์ •
        if let cinematicFormat = cinematicFormats.last {
            try? videoDevice.lockForConfiguration()
            videoDevice.activeFormat = cinematicFormat
            videoDevice.unlockForConfiguration()
        }
    }
}

// iOS 17+ Deferred Photo Delivery
if #available(iOS 17.0, *) {
    if photoOutput.isAutoDeferredPhotoDeliverySupported {
        photoOutput.isAutoDeferredPhotoDeliveryEnabled = true
        print("์ง€์—ฐ ์‚ฌ์ง„ ์ „๋‹ฌ ํ™œ์„ฑํ™”! ๐Ÿ“ธ")
    }
}
๐Ÿ’ก ์žฌ๋Šฅ๋„ท์—์„œ ์นด๋ฉ”๋ผ ์•ฑ ๊ฐœ๋ฐœ ์˜๋ขฐ๋„ ๊ฐ€๋Šฅํ•ด! AVFoundation ๊ธฐ๋ฐ˜ ์นด๋ฉ”๋ผ ์•ฑ ๊ฐœ๋ฐœ์ด ๋ณต์žกํ•˜๊ฒŒ ๋А๊ปด์ง„๋‹ค๋ฉด,
์žฌ๋Šฅ๋„ท(jaenung.net)์—์„œ iOS ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์–ด.
๋‹ค์–‘ํ•œ Swift ๊ฐœ๋ฐœ์ž๋“ค์ด ์นด๋ฉ”๋ผ ์•ฑ, ์˜์ƒ ์ฒ˜๋ฆฌ ์•ฑ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ณ  ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜Š

๐Ÿ› ์ž์ฃผ ๋งŒ๋‚˜๋Š” ์—๋Ÿฌ์™€ ํ•ด๊ฒฐ๋ฒ•

AVFoundation ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ๋งž๋‹ฅ๋œจ๋ฆฌ๋Š” ์—๋Ÿฌ๋“ค์ด ์žˆ์–ด.
๋ฏธ๋ฆฌ ์•Œ์•„๋‘๋ฉด ์‚ฝ์งˆ ์‹œ๊ฐ„์„ ์—„์ฒญ ์ค„์ผ ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช

โŒ ์—๋Ÿฌ 1: "This app has crashed because it attempted to access privacy-sensitive data"

์›์ธ: Info.plist์— ๊ถŒํ•œ ์„ค๋ช… ํ‚ค ๋ˆ„๋ฝ
ํ•ด๊ฒฐ: NSCameraUsageDescription, NSMicrophoneUsageDescription ์ถ”๊ฐ€
โŒ ์—๋Ÿฌ 2: AVCaptureSession์ด ๋ธ”๋ž™ ์Šคํฌ๋ฆฐ๋งŒ ๋ณด์ž„ ์›์ธ 1: captureSession.startRunning()์„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ํ˜ธ์ถœ
ํ•ด๊ฒฐ 1: sessionQueue์—์„œ startRunning() ํ˜ธ์ถœ

์›์ธ 2: previewLayer.frame์ด ์„ค์ • ์•ˆ ๋จ
ํ•ด๊ฒฐ 2: viewDidLayoutSubviews์—์„œ frame ์—…๋ฐ์ดํŠธ
โŒ ์—๋Ÿฌ 3: ์‚ฌ์ง„ ์ดฌ์˜ ํ›„ ์ด๋ฏธ์ง€๊ฐ€ ํšŒ์ „๋˜์–ด ์ €์žฅ๋จ ์›์ธ: EXIF ๋ฐฉํ–ฅ ์ •๋ณด ๋ฏธ์ฒ˜๋ฆฌ
ํ•ด๊ฒฐ: UIImage์˜ imageOrientation ์ฒ˜๋ฆฌ ๋˜๋Š” fixedOrientation() ๋ฉ”์„œ๋“œ ๊ตฌํ˜„
// ์ด๋ฏธ์ง€ ํšŒ์ „ ๋ฌธ์ œ ํ•ด๊ฒฐ
extension UIImage {
    func fixedOrientation() -> UIImage {
        if imageOrientation == .up { return self }
        
        UIGraphicsBeginImageContextWithOptions(size, false, scale)
        draw(in: CGRect(origin: .zero, size: size))
        let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        
        return normalizedImage
    }
}

// ์‚ฌ์šฉ ์˜ˆ์‹œ
func photoOutput(
    _ output: AVCapturePhotoOutput,
    didFinishProcessingPhoto photo: AVCapturePhoto,
    error: Error?
) {
    guard let imageData = photo.fileDataRepresentation(),
          let image = UIImage(data: imageData) else { return }
    
    // ํšŒ์ „ ๋ณด์ • ํ›„ ์‚ฌ์šฉ
    let fixedImage = image.fixedOrientation()
    
    DispatchQueue.main.async {
        self.imageView.image = fixedImage
    }
}
โŒ ์—๋Ÿฌ 4: ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์นด๋ฉ”๋ผ ์•ˆ ๋จ ์›์ธ: iOS ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ๋Š” ์‹ค์ œ ์นด๋ฉ”๋ผ ํ•˜๋“œ์›จ์–ด ์—†์Œ
ํ•ด๊ฒฐ: ์นด๋ฉ”๋ผ ๊ธฐ๋Šฅ์€ ๋ฐ˜๋“œ์‹œ ์‹ค์ œ ๊ธฐ๊ธฐ์—์„œ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด!
Xcode 14.0+์—์„œ๋Š” ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜๋œ ์นด๋ฉ”๋ผ ์ผ๋ถ€ ์ง€์›ํ•˜๊ธด ํ•จ

๐Ÿ“‹ AVFoundation ์นด๋ฉ”๋ผ ๊ฐœ๋ฐœ ์ฒดํฌ๋ฆฌ์ŠคํŠธ 1 Info.plist ๊ถŒํ•œ ์„ค์ • ์นด๋ฉ”๋ผ/๋งˆ์ดํฌ/์‚ฌ์ง„๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ถŒํ•œ ํ‚ค ์ถ”๊ฐ€ 2 ๊ถŒํ•œ ์š”์ฒญ ์ฝ”๋“œ ๊ตฌํ˜„ AVCaptureDevice.requestAccess ์ฒ˜๋ฆฌ 3 AVCaptureSession ๊ตฌ์„ฑ sessionQueue์—์„œ ์„ค์ •, Input/Output ์—ฐ๊ฒฐ 4 PreviewLayer ์„ค์ • AVCaptureVideoPreviewLayer ๋ทฐ์— ์ถ”๊ฐ€ 5 ์ดฌ์˜/๋…นํ™” ๊ธฐ๋Šฅ ๊ตฌํ˜„ PhotoOutput / MovieFileOutput ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ 6 ์นด๋ฉ”๋ผ ์ œ์–ด ๊ตฌํ˜„ ํฌ์ปค์Šค, ๋…ธ์ถœ, ์คŒ, ์ „ํ›„๋ฉด ์ „ํ™˜ 7 ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ (์„ ํƒ) VideoDataOutput + Core Image ํ•„ํ„ฐ 8 ์„ธ์…˜ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ viewWillAppear/Disappear์—์„œ ์‹œ์ž‘/์ค‘์ง€ ๐ŸŽ‰ ์ด 8๋‹จ๊ณ„๋งŒ ๋”ฐ๋ผ๊ฐ€๋ฉด ์™„์„ฑ! ๊ถŒํ•œ โ†’ ์„ธ์…˜ โ†’ ํ”„๋ฆฌ๋ทฐ โ†’ ์ดฌ์˜/๋…นํ™” โ†’ ์ œ์–ด โ†’ ์ฒ˜๋ฆฌ โ†’ ์ƒ๋ช…์ฃผ๊ธฐ ์ˆœ์„œ๋Œ€๋กœ ๊ตฌํ˜„ํ•˜๋ฉด ์™„์„ฑ๋„ ๋†’์€ ์นด๋ฉ”๋ผ ์•ฑ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ! ์‹ค์ œ ๊ธฐ๊ธฐ ํ…Œ์ŠคํŠธ ํ•„์ˆ˜ ๐Ÿ”‘
๐Ÿ’ก ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์นด๋ฉ”๋ผ ์•ฑ์€ ๋ฐฐํ„ฐ๋ฆฌ์™€ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋งŽ์ด ์“ฐ๋Š” ์•ฑ์ด์•ผ.
์ตœ์ ํ™” ์•ˆ ํ•˜๋ฉด ๋ฐฐํ„ฐ๋ฆฌ ๋นจ๋ฆฌ ๋‹ณ๊ณ  ์•ฑ์ด ๋А๋ ค์ง€๊ฑฐ๋“ .
์ด ํŒ๋“ค ๊ผญ ์ฑ™๊ฒจ๊ฐ€! ๐Ÿ”‹

1
์„ธ์…˜ ํ”„๋ฆฌ์…‹ ์ ์ ˆํžˆ ์„ ํƒํ•˜๊ธฐ
.photo๋Š” ์‚ฌ์ง„ ์ดฌ์˜์šฉ, .high๋Š” ๋น„๋””์˜ค์šฉ.
ํ•„์š” ์ด์ƒ์œผ๋กœ ๋†’์€ ํ”„๋ฆฌ์…‹ ์“ฐ๋ฉด ๋ฐฐํ„ฐ๋ฆฌ ๋‚ญ๋น„์•ผ. ์šฉ๋„์— ๋งž๊ฒŒ ์„ ํƒ!
2
๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ง„์ž… ์‹œ ์„ธ์…˜ ์ค‘์ง€
์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๊ฐ€๋ฉด ๋ฐ˜๋“œ์‹œ captureSession.stopRunning() ํ˜ธ์ถœ.
์•ˆ ํ•˜๋ฉด ๋ฐฐํ„ฐ๋ฆฌ ๊ณ„์† ์†Œ๋ชจ๋˜๊ณ  iOS๊ฐ€ ์•ฑ ๊ฐ•์ œ ์ข…๋ฃŒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด.
3
VideoDataOutput ์ฒ˜๋ฆฌ ์ตœ์ ํ™”
alwaysDiscardsLateVideoFrames = true ์„ค์ •์œผ๋กœ ๋Šฆ์€ ํ”„๋ ˆ์ž„ ๋“œ๋กญ.
์ฒ˜๋ฆฌ ๋กœ์ง์ด ๋ฌด๊ฑฐ์šฐ๋ฉด ๋งค ํ”„๋ ˆ์ž„ ๋Œ€์‹  Nํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹ ์‚ฌ์šฉ.
4
Metal ๊ธฐ๋ฐ˜ ๋ Œ๋”๋ง ์‚ฌ์šฉ
Core Image ํ•„ํ„ฐ ๊ฒฐ๊ณผ๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•  ๋•Œ MTKView ์‚ฌ์šฉ.
UIImageView์— ์ง์ ‘ ๋„ฃ๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅด๊ณ  ๋ถ€๋“œ๋Ÿฌ์›Œ.
5
๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ์ฃผ์˜
CMSampleBuffer์™€ CVPixelBuffer๋Š” ๋ฉ”๋ชจ๋ฆฌ ์ง‘์•ฝ์ ์ด์•ผ.
์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ›„ ์ฐธ์กฐ๋ฅผ ๋น ๋ฅด๊ฒŒ ํ•ด์ œํ•˜๊ณ , autorelease pool ํ™œ์šฉ ๊ถŒ์žฅ.
// ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ ํŒจํ„ด
func captureOutput(
    _ output: AVCaptureOutput,
    didOutput sampleBuffer: CMSampleBuffer,
    from connection: AVCaptureConnection
) {
    // autorelease pool๋กœ ๋ฉ”๋ชจ๋ฆฌ ์ฆ‰์‹œ ํ•ด์ œ
    autoreleasepool {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        
        // ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ ๋กœ์ง
        processFrame(pixelBuffer)
        
        // pixelBuffer๋Š” ์ด ๋ธ”๋ก ๋์—์„œ ์ž๋™ ํ•ด์ œ
    }
}

// ์„ธ์…˜ ํ”„๋ฆฌ์…‹ ์„ ํƒ ๊ฐ€์ด๋“œ
func selectSessionPreset(for purpose: String) -> AVCaptureSession.Preset {
    switch purpose {
    case "photo":
        return .photo          // ์ตœ๊ณ  ํ™”์งˆ ์‚ฌ์ง„
    case "video_4k":
        return .hd4K3840x2160  // 4K ๋น„๋””์˜ค (iPhone 6s+)
    case "video_1080p":
        return .hd1920x1080    // 1080p ๋น„๋””์˜ค
    case "video_720p":
        return .hd1280x720     // 720p ๋น„๋””์˜ค
    case "realtime":
        return .medium         // ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ์šฉ (๋‚ฎ์€ ํ•ด์ƒ๋„)
    default:
        return .high
    }
}

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌ - ์ด๊ฒƒ๋งŒ ๊ธฐ์–ตํ•ด!

์ž, ์˜ค๋Š˜ AVFoundation์œผ๋กœ iOS ์นด๋ฉ”๋ผ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฑฐ ์ „์ฒด์ ์œผ๋กœ ํ›‘์–ด๋ดค์–ด!
์ฒ˜์Œ์—” ๋ณต์žกํ•ด ๋ณด์—ฌ๋„ ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๋ฉด ์ƒ๊ฐ๋ณด๋‹ค ์ฒด๊ณ„์ ์ด๊ณ  ๊น”๋”ํ•˜๋‹ค๋Š” ๊ฑฐ ๋А๊ผˆ์ง€? ๐Ÿ˜Š

ํ•ต์‹ฌ์„ ๋‹ค์‹œ ์ •๋ฆฌํ•˜๋ฉด:

๐Ÿ”‘ AVFoundation ์นด๋ฉ”๋ผ ๊ฐœ๋ฐœ ํ•ต์‹ฌ ์š”์•ฝ โ‘  ํŒŒ์ดํ”„๋ผ์ธ ์ดํ•ด: Session โ†’ Input(์นด๋ฉ”๋ผ) โ†’ Output(์‚ฌ์ง„/๋น„๋””์˜ค) ํ๋ฆ„

โ‘ก ์Šค๋ ˆ๋“œ ๊ด€๋ฆฌ: ์„ธ์…˜ ์ž‘์—…์€ sessionQueue, UI๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ

โ‘ข ๊ถŒํ•œ ์ฒ˜๋ฆฌ: Info.plist ์„ค์ • + ๋Ÿฐํƒ€์ž„ ๊ถŒํ•œ ์š”์ฒญ ํ•„์ˆ˜

โ‘ฃ ์ƒ๋ช…์ฃผ๊ธฐ: viewWillAppear/Disappear์—์„œ ์„ธ์…˜ ์‹œ์ž‘/์ค‘์ง€

โ‘ค ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ: VideoDataOutput + Core Image + Metal ์กฐํ•ฉ

โ‘ฅ ์ตœ์ ํ™”: ์ ์ ˆํ•œ ํ”„๋ฆฌ์…‹, ํ”„๋ ˆ์ž„ ๋“œ๋กญ ํ—ˆ์šฉ, Metal ๋ Œ๋”๋ง

AVFoundation์€ ์ •๋ง ๊ฐ•๋ ฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ.
์—ฌ๊ธฐ์„œ ๋ฐฐ์šด ๊ธฐ์ดˆ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ Vision, Core ML, ARKit๊ณผ ์—ฐ๋™ํ•˜๋ฉด
์ง„์งœ ์—„์ฒญ๋‚œ ์•ฑ๋“ค์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด!

ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๊ธฐ ๋ง‰๋ง‰ํ•˜๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ Swift/iOS ๊ฐœ๋ฐœ ๋ฉ˜ํ† ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ.
๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด 1:1 ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋‚˜ ํ”„๋กœ์ ํŠธ ๋„์›€์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ๊ฑฐ๋“  ๐Ÿ™Œ

์ž, ์ด์ œ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ด! ์ฝ”๋“œ๋Š” ์ง์ ‘ ์ณ๋ด์•ผ ์‹ค๋ ฅ์ด ๋А๋Š” ๊ฑฐ ์•Œ์ง€? ใ…‹ใ…‹
ํŒŒ์ดํŒ…! ๐Ÿ’ช๐Ÿ”ฅ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.