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:
- Configurar e gerenciar
AVCaptureSession
- Selecionar e trocar dispositivos de câmera
- Configurar formatos de vídeo e frame rates
- Controlar zoom, foco e exposição
- Aplicar estabilização e codec preferido
- Gerenciar torch (flash)
🏗️ 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:
desiredFrameRate
:.fps30
ou.fps60
position
:.front
ou.back
completion
: Callback com erro (se houver)
Fluxo de Execução (na sessionQueue
):
session.beginConfiguration()
- Define preset
.high
- Encontra melhor câmera via
findBestCamera(for:)
- Remove inputs existentes
- Adiciona video input e audio input
- Configura frame rate via
setFrameRateLocked(to:)
- Adiciona
AVCaptureMovieFileOutput
- Aplica estabilização cinemática
- Configura mirroring (front = espelhado)
- Define codec HEVC como preferido
- Força zoom 1.0 inicial
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:
.builtInTripleCamera
(iPhone 11 Pro+, 13 Pro+, 14 Pro+).builtInDualWideCamera
(iPhone 11, 12, 13).builtInDualCamera
(iPhone 7 Plus - X).builtInWideAngleCamera
(fallback universal)
Front Camera:
.builtInTrueDepthCamera
(iPhone X+, iPad Pro 2018+).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:
- Filtra
device.formats
para encontrar formatos que suportam o FPS - Para cada formato, verifica
videoSupportedFrameRateRanges
- Seleciona formato com maior resolução (width × height)
- Define
device.activeFormat
- Configura
activeVideoMinFrameDuration
eactiveVideoMaxFrameDuration
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:
factor
: Zoom desejado (0.5 - 6.0)animated
: Se true, usa ramping suaverampRate
: Velocidade de transição (pts/segundo)
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:
- Se virtual device suporta 0.5x: aplica zoom digital
- Senão, tenta trocar para câmera ultra-wide física (
.builtInUltraWideCamera
) - Mantém frame rate após troca
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:
- Se atualmente em ultra-wide física: troca de volta para wide
- Senão, apenas aplica zoom 1.0
jumpToTwoX()
Comportamento:
- Aplica zoom 2.0x
- Em devices com telephoto, o virtual device troca automaticamente
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:
devicePoint
: Coordenadas (0.0-1.0, 0.0-1.0) relativas ao sensor
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:
- Determina próxima posição (front ↔ back)
- Encontra melhor dispositivo via
findBestCamera(for:)
- Troca input via
useDevice(_:)
:session.beginConfiguration()
- Remove input antigo
- Adiciona input novo
session.commitConfiguration()
- Reaplica frame rate configurado
- Reseta zoom para 1.0
- Reaplica estado do torch (se suportado)
- 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:
.off
: Sem estabilização.standard
: Estabilização básica (crop pequeno).cinematic
: Estabilização agressiva (crop maior, suavização máxima) ✅.auto
: Sistema decide
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:
- Front camera: Espelhado (UX padrão iOS)
- Back camera: Normal (não espelhado)
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
:
- beginConfiguration / commitConfiguration
- Adicionar/remover inputs/outputs
- Trocar dispositivos
- Configurar formatos
- Device lock/unlock
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:
.builtInTripleCamera
: Ultra-wide + Wide + Telephoto.builtInDualWideCamera
: Ultra-wide + Wide.builtInDualCamera
: Wide + Telephoto
Vantagens:
- Transições suaves entre lentes
- Sistema gerencia automaticamente
- Melhor UX
Zoom Behavior:
- 0.5x-1.0x: Ultra-wide
- 1.0x-2.0x: Wide com zoom digital
- 2.0x+: Telephoto (se disponível)
Physical Device
O que é: Câmera física individual.
Exemplos:
.builtInWideAngleCamera
.builtInUltraWideCamera
.builtInTelephotoCamera
Quando Usar:
- Necessário trocar
AVCaptureDeviceInput
manualmente - Usado no
jumpToHalfX()
em back camera sem virtual device
📊 Performance
Otimizações
- Queue Serial: Evita race conditions sem overhead de locks
- Formato Eficiente: Seleciona maior resolução que suporte FPS
- Weak Self: Evita retain cycles em closures
- 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:
- Permissões não concedidas
- Device ocupado por outra app
- Configuração inválida
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 - Coordenação de alto nível
- SegmentedRecorder - Sistema de gravação
- Guia de Câmera - Tutorial de controles
- Escolhas Técnicas - Decisões de design
← CameraViewModel | SegmentedRecorder → |