πŸ“· Camera App

SegmentedRecorder

Gerencia a gravaΓ§Γ£o de mΓΊltiplos segmentos de vΓ­deo independentes, cada um em um arquivo temporΓ‘rio separado.

Arquivo: SegmentedRecorder.swift


πŸ“‹ VisΓ£o Geral

O SegmentedRecorder permite:


πŸ—οΈ Protocol Delegate

protocol SegmentedRecorderDelegate: AnyObject {
    func recorder(_ recorder: SegmentedRecorder, didFinishSegment url: URL)
    func recorder(_ recorder: SegmentedRecorder, didFailWith error: Error)
}

Uso:

extension CameraViewModel: SegmentedRecorderDelegate {
    func recorder(_ recorder: SegmentedRecorder, didFinishSegment url: URL) {
        // Gerar thumbnail
        // Adicionar a segments array
    }
    
    func recorder(_ recorder: SegmentedRecorder, didFailWith error: Error) {
        // Mostrar erro
        // Resetar UI
    }
}

πŸ”§ MΓ©todos Principais

InicializaΓ§Γ£o

init(output: AVCaptureMovieFileOutput, delegate: SegmentedRecorderDelegate)

ParΓ’metros:


Iniciar Segmento

func startNewSegment()

Comportamento:

  1. Cria URL temporΓ‘rio: NSTemporaryDirectory() + "segment_\(UUID()).mov"
  2. Define orientaΓ§Γ£o via updateOrientation(from:)
  3. Chama output.startRecording(to: url, recordingDelegate: self)

Exemplo:

// UsuΓ‘rio aperta botΓ£o de gravar
recorder.startNewSegment()

Parar Segmento

func stopCurrentSegment()

Comportamento:

Exemplo:

// UsuΓ‘rio aperta botΓ£o de parar
recorder.stopCurrentSegment()

Atualizar OrientaΓ§Γ£o

func updateOrientation(from deviceOrientation: UIDeviceOrientation)

DescriΓ§Γ£o: Atualiza orientaΓ§Γ£o para prΓ³ximas gravaΓ§Γ΅es.

Uso: Chamado quando UIDevice.orientationDidChangeNotification dispara.


🎬 Fluxo de Gravação

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     User Action                             β”‚
β”‚              toggleRecording() pressed                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  isRecording == false β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  startNewSegment()                   β”‚
         β”‚  1. Create temp URL                  β”‚
         β”‚  2. Set video orientation            β”‚
         β”‚  3. output.startRecording(to:)       β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  Recording in progress...            β”‚
         β”‚  isRecording = true                  β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  User stops recordingβ”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  stopCurrentSegment()                β”‚
         β”‚  output.stopRecording()              β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  AVCaptureFileOutputRecordingDelegate        β”‚
         β”‚  fileOutput(_:didFinishRecordingTo:...)      β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚  delegate.recorder(didFinishSegment:)β”‚
         β”‚  CameraViewModel receives URL         β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”— AVCaptureFileOutputRecordingDelegate

extension SegmentedRecorder: AVCaptureFileOutputRecordingDelegate {
    func fileOutput(
        _ output: AVCaptureFileOutput,
        didFinishRecordingTo outputFileURL: URL,
        from connections: [AVCaptureConnection],
        error: Error?
    ) {
        if let error = error {
            delegate?.recorder(self, didFailWith: error)
        } else {
            delegate?.recorder(self, didFinishSegment: outputFileURL)
        }
    }
}

πŸ“ Gerenciamento de Arquivos

CriaΓ§Γ£o de Arquivo TemporΓ‘rio

let tempDir = NSTemporaryDirectory()
let filename = "segment_\(UUID().uuidString).mov"
let url = URL(fileURLWithPath: tempDir).appendingPathComponent(filename)

Exemplo de path:

/var/mobile/Containers/Data/Application/.../tmp/segment_12345678-1234-1234-1234-123456789abc.mov

Cleanup

Arquivos temporΓ‘rios sΓ£o removidos apΓ³s concatenaΓ§Γ£o:

// No CameraViewModel, apΓ³s salvar vΓ­deo final
for segment in segments {
    try? FileManager.default.removeItem(at: segment.url)
}
segments.removeAll()

🎯 Casos de Uso

GravaΓ§Γ£o Simples (1 Segmento)

// Iniciar
recorder.startNewSegment()

// ... usuΓ‘rio grava ...

// Parar
recorder.stopCurrentSegment()

// Delegate recebe
func recorder(_ recorder: SegmentedRecorder, didFinishSegment url: URL) {
    print("Segment saved at: \(url)")
}

GravaΓ§Γ£o MΓΊltipla (3 Segmentos)

// Segmento 1
recorder.startNewSegment()
// ... grava ...
recorder.stopCurrentSegment()

// Segmento 2
recorder.startNewSegment()
// ... grava ...
recorder.stopCurrentSegment()

// Segmento 3
recorder.startNewSegment()
// ... grava ...
recorder.stopCurrentSegment()

// Todos os 3 URLs sΓ£o recebidos via delegate
// CameraViewModel armazena em segments array

βš™οΈ ConfiguraΓ§Γ£o

saveToPhotoLibrary

var saveToPhotoLibrary: Bool = false

DescriΓ§Γ£o: Se true, cada segmento Γ© salvo individualmente na galeria.

Uso TΓ­pico: false (salvamos apenas o vΓ­deo final concatenado).


🧡 Threading

Callbacks

Delegates sΓ£o chamados na thread onde o output foi configurado (tipicamente main thread ou sessionQueue).

Best Practice: Sempre dispatch para main thread antes de atualizar UI:

func recorder(_ recorder: SegmentedRecorder, didFinishSegment url: URL) {
    DispatchQueue.global(qos: .userInitiated).async {
        let thumbnail = generateThumbnail(for: url)
        
        DispatchQueue.main.async {
            self.segments.append(RecordedSegment(url: url, thumbnail: thumbnail))
        }
    }
}

πŸ› Troubleshooting

Segmento nΓ£o grava

Sintomas: didFinishSegment nunca Γ© chamado.

Causas:

SoluΓ§Γ£o:

// Verificar session
print("Session running: \(session.isRunning)")

// Verificar connections
print("Output connections: \(output.connections)")

// Verificar permissΓ΅es
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
print("Can write to temp: \(FileManager.default.isWritableFile(atPath: tempURL.path))")

Erro β€œRecording already in progress”

Sintomas: Crash ao chamar startNewSegment() duas vezes.

SoluΓ§Γ£o:

guard !output.isRecording else {
    print("Already recording")
    return
}
output.startRecording(to: url, recordingDelegate: self)

πŸ“š Ver TambΓ©m


← CaptureSessionController Teleprompter β†’