Swift

Captura de quadros de vídeo

Este exemplo mostra como usar um capturador de vídeo personalizado, utilizando a câmera do dispositivo como fonte de vídeo. Para usar a implementação da câmera, instancie o capturador de câmera em seu VonageVideoManager:

publisher.videoCapture = BasicVideoCapturerCamera(preset: AVCaptureSession.Preset.cif352x288, desiredFrameRate: 30)

Inicialização e configuração do capturador de vídeo

O inicializador chama size(from:) para determinar a resolução e configura uma fila de envio serial para a captura de imagens, a fim de não afetar a fila da interface do usuário.

init(preset: AVCaptureSession.Preset, desiredFrameRate: Int) {
    self.sessionPreset = preset
    self.desiredFrameRate = desiredFrameRate
    self.captureQueue = DispatchQueue(label: "com.vonage.BasicVideoCapturer")
    
    super.init()
    
    let size = self.size(from: self.sessionPreset)
    self.imageWidth = Int(size.width)
    self.imageHeight = Int(size.height)
}

A implementação de initCapture utiliza o AVFoundation estrutura para configurar a câmera para capturar imagens. Ela cria um AVCaptureSession, define o dispositivo de entrada e configura um AVCaptureVideoDataOutput:

func initCapture() {
    let session = AVCaptureSession()
    session.beginConfiguration()
    
    // Set device capture
    session.sessionPreset = sessionPreset
    
    guard let videoDevice = AVCaptureDevice.default(for: .video),
          let deviceInput = try? AVCaptureDeviceInput(device: videoDevice) else { return }
          
    self.inputDevice = deviceInput
    if session.canAddInput(deviceInput) {
        session.addInput(deviceInput)
    }
    
    let outputDevice = AVCaptureVideoDataOutput()
    outputDevice.alwaysDiscardsLateVideoFrames = true
    outputDevice.videoSettings = [
        kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
    ]
    
    outputDevice.setSampleBufferDelegate(self, queue: captureQueue)
    if session.canAddOutput(outputDevice) {
        session.addOutput(outputDevice)
    }
    
    // ... Frame rate configuration (see below)
}

Os quadros capturados com esse método são acessados por meio do AVCaptureVideoDataOutputSampleBufferDelegate.

A segunda parte de initCapture configura a taxa de quadros:

    // Set framerate
    let bestFPS = self.bestFrameRate(for: videoDevice)
    do {
        try videoDevice.lockForConfiguration()
        let duration = CMTime(value: 1, timescale: CMTimeScale(bestFPS))
        videoDevice.activeVideoMinFrameDuration = duration
        videoDevice.activeVideoMaxFrameDuration = duration
        videoDevice.unlockForConfiguration()
    } catch {
        print("Error locking configuration")
    }
    
    session.commitConfiguration()
    self.captureSession = session
    
    self.format = OTVideoFormat(nv12WithWidth: UInt32(imageWidth), height: UInt32(imageHeight))

O bestFrameRate(for:) O método retorna a melhor taxa de quadros para o dispositivo de captura:

private func bestFrameRate(for device: AVCaptureDevice) -> Double {
    var bestRate: Double = 0
    for range in device.activeFormat.videoSupportedFrameRateRanges {
        let duration = range.minFrameDuration
        let currentRate = Double(duration.timescale) / Double(duration.value)
        
        if currentRate > bestRate && currentRate < Double(desiredFrameRate) {
            bestRate = currentRate
        }
    }
    return bestRate
}

Captura de quadros para o vídeo da editora

O start O método inicia o AVCaptureSession:

//
//  returns:
//  - a negative value for error
//  - 0 value when all is OK
//
func start() -> Int32 {
    self.captureStarted = true
    self.captureSession?.startRunning()
    return 0
}

O método delegado captureOutput(_:didOutput:from:) é chamada quando um novo quadro de vídeo fica disponível.

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    guard captureStarted, let format = self.format else { return }
    guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
    
    let frame = OTVideoFrame(format: format)
    
    // Prepare memory copy for NV12 (2 planes)
    let planeCount = CVPixelBufferGetPlaneCount(imageBuffer)
    let totalSize = CVPixelBufferGetDataSize(imageBuffer)
    let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: totalSize)
    
    CVPixelBufferLockBaseAddress(imageBuffer, .readOnly)
    
    var planePointers = [UnsafeMutablePointer<UInt8>?]()
    var currentDestination = buffer
    
    // Copy planes
    for i in 0..<planeCount {
        guard let sourceBaseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, i) else { continue }
        let planeSize = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, i) * CVPixelBufferGetHeightOfPlane(imageBuffer, i)
        
        planePointers.append(currentDestination)
        memcpy(currentDestination, sourceBaseAddress, planeSize)
        currentDestination += planeSize
    }
    
    // Set metadata and consume
    if let device = self.inputDevice?.device {
         let minDuration = device.activeVideoMinFrameDuration
         frame.format?.estimatedFramesPerSecond = Double(minDuration.timescale) / Double(minDuration.value)
    }
    
    frame.orientation = self.currentDeviceOrientation()
    frame.timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
    
    planePointers.withUnsafeBufferPointer { bufferPointer in
        frame.setPlanesWithPointers(bufferPointer.baseAddress, numPlanes: Int(planeCount))
    }
    
    videoCaptureConsumer?.consumeFrame(frame)
    
    buffer.deallocate()
    CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
}

Este método realiza o seguinte:

  1. Cria um OTVideoFrame instância.
  2. Aloca um buffer de memória.
  3. Copia os dados da imagem do CVImageBuffer (formato NV12) no buffer manual. O NV12 possui dois planos (Y e UV), que são copiados sequencialmente.
  4. Marca o quadro com a data e a hora e a orientação.
  5. Chamadas consumeFrame, passando o quadro para o SDK da Vonage.