📷 Camera App

CaptureSessionController

Controller de baixo nível que encapsula toda a complexidade do AVCaptureSession. Gerencia dispositivos de captura, formatos de vídeo, zoom, foco, exposição e estabilização.

Arquivo: CaptureSessionController.swift


📋 Visão Geral

O CaptureSessionController é responsável por:


🏗️ Arquitetura Interna

CaptureSessionController
├── session: AVCaptureSession
├── sessionQueue: DispatchQueue              // Queue serial para thread-safety
├── videoDevice: AVCaptureDevice?            // Dispositivo de câmera atual
├── videoDeviceInput: AVCaptureDeviceInput?
├── audioDeviceInput: AVCaptureDeviceInput?
└── movieFileOutput: AVCaptureMovieFileOutput?

🔧 Métodos Principais

Configuração de Session

func configureSession(
    desiredFrameRate: DesiredFrameRate = .fps60,
    position: AVCaptureDevice.Position = .front,
    completion: ((Error?) -> Void)? = nil
)

Descrição: Configura completamente a AVCaptureSession.

Parâmetros:

Fluxo de Execução (na sessionQueue):

  1. session.beginConfiguration()
  2. Define preset .high
  3. Encontra melhor câmera via findBestCamera(for:)
  4. Remove inputs existentes
  5. Adiciona video input e audio input
  6. Configura frame rate via setFrameRateLocked(to:)
  7. Adiciona AVCaptureMovieFileOutput
  8. Aplica estabilização cinemática
  9. Configura mirroring (front = espelhado)
  10. Define codec HEVC como preferido
  11. Força zoom 1.0 inicial
  12. session.commitConfiguration()

Exemplo:

controller.configureSession(desiredFrameRate: .fps60, position: .front) { error in
    if let error = error {
        print("Configuration failed: \(error)")
    } else {
        print("Camera configured successfully")
    }
}

Seleção de Dispositivo

private func findBestCamera(for position: AVCaptureDevice.Position) throws -> AVCaptureDevice

Descrição: Encontra o melhor dispositivo de câmera disponível.

Hierarquia de Preferência:

Back Camera:

  1. .builtInTripleCamera (iPhone 11 Pro+, 13 Pro+, 14 Pro+)
  2. .builtInDualWideCamera (iPhone 11, 12, 13)
  3. .builtInDualCamera (iPhone 7 Plus - X)
  4. .builtInWideAngleCamera (fallback universal)

Front Camera:

  1. .builtInTrueDepthCamera (iPhone X+, iPad Pro 2018+)
  2. .builtInWideAngleCamera (fallback universal)

Implementação:

let types: [AVCaptureDevice.DeviceType] = position == .back 
    ? [.builtInTripleCamera, .builtInDualWideCamera, .builtInDualCamera, .builtInWideAngleCamera]
    : [.builtInTrueDepthCamera, .builtInWideAngleCamera]

let discovery = AVCaptureDevice.DiscoverySession(
    deviceTypes: types,
    mediaType: .video,
    position: position
)

guard let device = discovery.devices.first else {
    throw CameraError.noDeviceFound
}
return device

Seleção de Formato e Frame Rate

private func setFrameRateLocked(to fps: Int) throws

Descrição: Seleciona o melhor formato que suporte o frame rate desejado.

Algoritmo:

  1. Filtra device.formats para encontrar formatos que suportam o FPS
  2. Para cada formato, verifica videoSupportedFrameRateRanges
  3. Seleciona formato com maior resolução (width × height)
  4. Define device.activeFormat
  5. Configura activeVideoMinFrameDuration e activeVideoMaxFrameDuration

Código:

var bestFormat: AVCaptureDevice.Format?
var bestDimensions = CMVideoDimensions(width: 0, height: 0)

for format in device.formats {
    guard let range = format.videoSupportedFrameRateRanges.first(
        where: { $0.maxFrameRate >= Double(fps) }
    ) else { continue }
    
    let dims = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
    let isBetter = (dims.width * dims.height) > (bestDimensions.width * bestDimensions.height)
    
    if isBetter {
        bestFormat = format
        bestDimensions = dims
    }
}

if let bestFormat = bestFormat {
    device.activeFormat = bestFormat
    
    let duration = CMTimeMake(value: 1, timescale: Int32(fps))
    device.activeVideoMinFrameDuration = duration
    device.activeVideoMaxFrameDuration = duration
}

Resultado: Formato com maior resolução disponível que suporte o FPS desejado.


Controle de Zoom

func setZoomFactor(_ factor: CGFloat, animated: Bool, rampRate: Float = 3.0)

Descrição: Aplica zoom com ou sem animação.

Parâmetros:

Limites:

let minZoom = device.minAvailableVideoZoomFactor  // Tipicamente 1.0 ou 0.5
let maxZoom = min(device.maxAvailableVideoZoomFactor, 6.0)  // Limite de 6.0x
let clamped = max(minZoom, min(maxZoom, factor))

Implementação:

sessionQueue.async { [weak self] in
    guard let device = self?.videoDevice else { return }
    
    do {
        try device.lockForConfiguration()
        
        if animated {
            device.ramp(toVideoZoomFactor: clamped, withRate: rampRate)
        } else {
            device.videoZoomFactor = clamped
        }
        
        device.unlockForConfiguration()
    } catch {
        print("Zoom error: \(error)")
    }
}

Exemplo:

// Zoom suave para 2x
controller.setZoomFactor(2.0, animated: true)

// Zoom instantâneo para 1x
controller.setZoomFactor(1.0, animated: false)

Jump Zooms

func jumpToHalfX()
func jumpToOneX()
func jumpToTwoX()

Descrição: Atalhos para zooms comuns (0.5x / 1x / 2x).

jumpToHalfX()

Comportamento:

Código:

func jumpToHalfX() {
    if videoDevice?.minAvailableVideoZoomFactor ?? 1.0 <= 0.5 {
        setZoomFactor(0.5, animated: true, rampRate: 6.0)
    } else if currentPosition == .back {
        // Try physical ultra-wide
        if let ultraWide = AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) {
            useDevice(ultraWide)
            setFrameRateLocked(to: currentFrameRate)
        }
    }
}

jumpToOneX()

Comportamento:

jumpToTwoX()

Comportamento:

Nota: Virtual devices (Triple/Dual Camera) gerenciam transições entre lentes automaticamente.


Foco e Exposição

func focusAndExpose(at devicePoint: CGPoint)

Descrição: Configura foco e exposição em um ponto específico.

Parâmetros:

Implementação:

sessionQueue.async { [weak self] in
    guard let device = self?.videoDevice else { return }
    
    do {
        try device.lockForConfiguration()
        
        if device.isFocusPointOfInterestSupported {
            device.focusPointOfInterest = devicePoint
            device.focusMode = .continuousAutoFocus
        }
        
        if device.isExposurePointOfInterestSupported {
            device.exposurePointOfInterest = devicePoint
            device.exposureMode = .continuousAutoExposure
        }
        
        device.isSubjectAreaChangeMonitoringEnabled = true
        
        device.unlockForConfiguration()
    } catch {
        print("Focus error: \(error)")
    }
}

Conversão de Coordenadas (na View):

let location = gesture.location(in: self)
let devicePoint = videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location)
controller.focusAndExpose(at: devicePoint)

Torch (Flash)

func setTorchEnabled(_ enabled: Bool)

Descrição: Liga/desliga o torch.

Comportamento por Câmera:

Back Camera (Hardware Torch):

guard device.hasTorch else { return }

if enabled {
    let level = min(max(0.0, AVCaptureDevice.maxAvailableTorchLevel), 1.0)
    try device.setTorchModeOn(level: level)
} else {
    device.torchMode = .off
}

Front Camera (Screen Brightness):

if enabled {
    savedScreenBrightness = UIScreen.main.brightness
    UIScreen.main.brightness = 1.0
} else {
    UIScreen.main.brightness = savedScreenBrightness ?? 0.5
}

Exemplo:

// Ligar torch
controller.setTorchEnabled(true)

// Desligar torch
controller.setTorchEnabled(false)

Alternância de Câmera

func toggleCameraPosition()

Descrição: Alterna entre câmera frontal e traseira.

Processo:

  1. Determina próxima posição (front ↔ back)
  2. Encontra melhor dispositivo via findBestCamera(for:)
  3. Troca input via useDevice(_:):
    • session.beginConfiguration()
    • Remove input antigo
    • Adiciona input novo
    • session.commitConfiguration()
  4. Reaplica frame rate configurado
  5. Reseta zoom para 1.0
  6. Reaplica estado do torch (se suportado)
  7. Atualiza mirroring

Código:

func toggleCameraPosition() {
    sessionQueue.async { [weak self] in
        guard let self = self else { return }
        
        let nextPosition: AVCaptureDevice.Position = self.currentPosition == .front ? .back : .front
        
        do {
            let newDevice = try self.findBestCamera(for: nextPosition)
            self.useDevice(newDevice)
            
            // Reaplica configurações
            try self.setFrameRateLocked(to: self.currentFrameRate)
            self.setZoomFactor(1.0, animated: false)
            
            self.currentPosition = nextPosition
            self.applyMirroringForCurrentPosition()
            
        } catch {
            print("Toggle camera error: \(error)")
        }
    }
}

Estabilização

func applyPreferredStabilizationMode()

Descrição: Aplica estabilização cinemática ao output.

Modos Disponíveis:

Implementação:

func applyPreferredStabilizationMode() {
    for connection in movieFileOutput.connections {
        if connection.mediaType == .video,
           connection.isVideoStabilizationSupported {
            connection.preferredVideoStabilizationMode = .cinematic
        }
    }
}

Escolha Técnica: .cinematic oferece melhor resultado para vídeos de roteiro/apresentação.


Mirroring

private func applyMirroringForCurrentPosition()

Descrição: Configura espelhamento baseado na posição da câmera.

Comportamento:

Implementação:

let shouldMirror = (currentPosition == .front)

for connection in movieFileOutput.connections {
    if connection.mediaType == .video,
       connection.isVideoMirroringSupported {
        connection.automaticallyAdjustsVideoMirroring = false
        connection.isVideoMirrored = shouldMirror
    }
}

Codec Selection

func setPreferredCodecHEVC(_ enabled: Bool)

Descrição: Configura codec preferido (HEVC ou H.264).

Implementação:

guard let connection = movieFileOutput.connection(with: .video) else { return }
let available = movieFileOutput.availableVideoCodecTypes

if enabled, available.contains(.hevc) {
    movieFileOutput.setOutputSettings(
        [AVVideoCodecKey: AVVideoCodecType.hevc],
        for: connection
    )
} else if available.contains(.h264) {
    movieFileOutput.setOutputSettings(
        [AVVideoCodecKey: AVVideoCodecType.h264],
        for: connection
    )
}

Comparação:

Codec Compressão Compatibilidade Suporte
HEVC (H.265) ~50% menor iOS 11+ iPhone 7+
H.264 Padrão Universal Todos

Escolha Técnica: HEVC é preferido por tamanho de arquivo menor com qualidade equivalente.


Orientação

func setVideoOrientation(_ orientation: AVCaptureVideoOrientation)

Descrição: Define orientação do vídeo gravado.

Conversão de UIDeviceOrientation:

extension AVCaptureVideoOrientation {
    init?(deviceOrientation: UIDeviceOrientation) {
        switch deviceOrientation {
        case .portrait: self = .portrait
        case .portraitUpsideDown: self = .portraitUpsideDown
        case .landscapeLeft: self = .landscapeRight   // Invertido!
        case .landscapeRight: self = .landscapeLeft   // Invertido!
        default: return nil
        }
    }
}

Nota: landscapeLeft do device corresponde a landscapeRight da câmera devido ao offset de 90° do sensor.


🧵 Threading Model

sessionQueue

private let sessionQueue = DispatchQueue(label: "camera.session.queue")

Propósito: Queue serial para todas as operações de configuração.

Regras:

⚠️ Crítico

Todas as operações de configuração devem executar na sessionQueue:

Exemplo Correto:

func configureDevice() {
    sessionQueue.async { [weak self] in
        guard let device = self?.videoDevice else { return }
        
        do {
            try device.lockForConfiguration()
            // Configurações...
            device.unlockForConfiguration()
        } catch {
            print("Error: \(error)")
        }
    }
}

Exemplo Incorreto ❌:

func configureDevice() {
    // NUNCA fazer direto na main thread!
    try device.lockForConfiguration()
    device.videoZoomFactor = 2.0
    device.unlockForConfiguration()
}

🎯 Casos de Uso

Inicializar Câmera Frontal 60fps

let controller = CaptureSessionController()

controller.configureSession(desiredFrameRate: .fps60, position: .front) { error in
    if let error = error {
        print("Failed: \(error)")
    } else {
        controller.startSession()
    }
}

Trocar para Back Camera

controller.toggleCameraPosition()

Aplicar Zoom Suave

// Pinch gesture
.onChanged { gesture in
    let scale = gesture.scale
    controller.setZoomFactor(baseZoom * scale, animated: true, rampRate: 3.0)
}
.onEnded { _ in
    controller.cancelZoomRamp()
}

Foco por Toque

.gesture(
    TapGesture()
        .onEnded { location in
            let devicePoint = convertToDevicePoint(location)
            controller.focusAndExpose(at: devicePoint)
        }
)

🔍 Virtual Device vs Physical Device

Virtual Device

O que é: Dispositivo lógico que representa múltiplas câmeras físicas.

Exemplos:

Vantagens:

Zoom Behavior:

Physical Device

O que é: Câmera física individual.

Exemplos:

Quando Usar:


📊 Performance

Otimizações

  1. Queue Serial: Evita race conditions sem overhead de locks
  2. Formato Eficiente: Seleciona maior resolução que suporte FPS
  3. Weak Self: Evita retain cycles em closures
  4. Lock Duration: Minimiza tempo entre lockForConfiguration / unlock

Profiling

let start = Date()
controller.configureSession { _ in
    let elapsed = Date().timeIntervalSince(start)
    print("Configuration took \(elapsed)s")  // Tipicamente < 0.5s
}

🐛 Troubleshooting

Session não inicia

Sintomas: isSessionRunning = false, preview preto

Causas Comuns:

Solução:

// Verificar autorização
let status = AVCaptureDevice.authorizationStatus(for: .video)
print("Auth status: \(status)")

// Verificar erro na configuração
controller.configureSession { error in
    if let error = error {
        print("Config error: \(error.localizedDescription)")
    }
}

Zoom não funciona

Sintomas: Zoom fica travado em 1.0x

Solução:

// Verificar limites do device
print("Min zoom: \(device.minAvailableVideoZoomFactor)")
print("Max zoom: \(device.maxAvailableVideoZoomFactor)")

// Garantir lock
try device.lockForConfiguration()
device.videoZoomFactor = 2.0
device.unlockForConfiguration()

Frame rate não aplica

Sintomas: Vídeo grava em 30fps mesmo configurando 60fps

Solução:

// Verificar se formato suporta
for format in device.formats {
    let ranges = format.videoSupportedFrameRateRanges
    print("Format: \(format), ranges: \(ranges)")
}

// Garantir que setFrameRateLocked foi chamado
try setFrameRateLocked(to: 60)

📚 Ver Também


← CameraViewModel SegmentedRecorder →