Gravação personalizada de vídeo (iOS)

Visão geral

Este tutorial explica as etapas necessárias para fazer alterações no capturador de vídeo do seu aplicativo OpenTok para iOS.

Etapa 1: Inicialização da captura

O código desta seção está no Capturador Básico de Vídeo projeto do repositório opentok-ios-sdk-samples, portanto, se ainda não tiver feito isso, você precisará clonar o repositório em um diretório local — isso pode ser feito usando a linha de comando:

git clone https://github.com/opentok/opentok-ios-sdk-samples

Mude para o diretório do projeto Basic Video Capturer:

cd opentok-ios-sdk-samples/Basic-Video-Capturer

Em seguida, instale a dependência do OpenTok:

pod install

Este projeto mostra como fazer pequenas modificações no capturador de vídeo utilizado pela classe OTPublisher. Abra o projeto no Xcode para acompanhar o passo a passo.

Neste exemplo, o aplicativo utiliza um capturador de vídeo personalizado para exibir pixels aleatórios (ruído branco). Isso é feito simplesmente para ilustrar os princípios básicos da configuração de um capturador de vídeo personalizado. (Para um exemplo mais prático, consulte os exemplos “Capturador de vídeo da câmera” e “Capturador de vídeo da tela”, descritos nas seções a seguir.)

No ViewController principal, após chamar [_session publish:_publisher error:&error] para iniciar a transmissão de um fluxo de áudio e vídeo, o videoCapture A propriedade do objeto OTPublisher é definida como uma instância de OTKBasicVideoCapturer:

_publisher.videoCapture = [[OTKBasicVideoCapturer alloc] init];

OTKBasicVideoCapturer é uma classe personalizada que implementa o protocolo OTVideoCapture (definido no SDK do OpenTok para iOS). Esse protocolo permite definir um capturador de vídeo personalizado para ser utilizado por um emissor do OpenTok.

O [OTVideoCapture initCapture:] O método inicializa as configurações de captura a serem utilizadas pelo capturador de vídeo personalizado. Na implementação personalizada de OTVideoCapture (OTKBasicVideoCapturer) deste exemplo, o initCapture O método define as propriedades do format propriedade da instância OTVideoCapture:

- (void)initCapture
{
    self.format = [[OTVideoFormat alloc] init];
    self.format.pixelFormat = OTPixelFormatARGB;
    self.format.bytesPerRow = [@[@(kImageWidth * 4)] mutableCopy];
    self.format.imageHeight = kImageHeight;
    self.format.imageWidth = kImageWidth;
}

A classe OTVideoFormat (que define isso format (propriedade) é definida pelo SDK do OpenTok para iOS. Neste código de exemplo, o formato do capturador de vídeo está configurado para usar ARGB como formato de pixel, com um número específico de bytes por linha, uma altura específica e uma largura específica.

O [OTVideoCapture setVideoCaptureConsumer] define um objeto OTVideoCaptureConsumer (definido pelo SDK do OpenTok para iOS) que o consumidor de vídeo utiliza para transmitir quadros de vídeo para o stream do editor. No OTKBasicVideoCapturer, esse método define uma instância local do OTVideoCaptureConsumer como o consumidor:

- (void)setVideoCaptureConsumer:(id<OTVideoCaptureConsumer>)videoCaptureConsumer
{
    // Save consumer instance in order to use it to send frames to the session
    self.consumer = videoCaptureConsumer;
}

O [OTVideoCapture startCapture:] O método é chamado quando um emissor começa a capturar vídeo para enviá-lo como um stream para a sessão do OpenTok. Isso ocorrerá após o [Session publish: error:] método é chamado. No OTKBasicVideoCapturer desse método, o [self produceFrame] O método é chamado em uma fila de segundo plano após um intervalo definido:

- (int32_t)startCapture
{
    self.captureStarted = YES;
    dispatch_after(kTimerInterval,
                   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
                   ^{
                       [self produceFrame];
                   });

    return 0;
}

O [self produceFrame] O método gera um objeto OTVideoFrame (definido pelo SDK do OpenTok para iOS) que representa um quadro de vídeo. Nesse caso, o quadro contém pixels aleatórios que preenchem a altura e a largura definidas para o formato de vídeo de amostra:

- (void)produceFrame
{
     OTVideoFrame *frame = [[OTVideoFrame alloc] initWithFormat:self.format];

    // Generate a image with random pixels
    u_int8_t *imageData[1];
    imageData[0] = malloc(sizeof(uint8_t) * kImageHeight * kImageWidth * 4);
    for (int i = 0; i < kImageWidth * kImageHeight * 4; i+=4) {
        imageData[0][i] = rand() % 255;   // A
        imageData[0][i+1] = rand() % 255; // R
        imageData[0][i+2] = rand() % 255; // G
        imageData[0][i+3] = rand() % 255; // B
    }

    [frame setPlanesWithPointers:imageData numPlanes:1];
    [self.consumer consumeFrame:frame];

    free(imageData[0]);

    if (self.captureStarted) {
        dispatch_after(kTimerInterval,
                       dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),
                       ^{
                           [self produceFrame];
                       });
    }
}

O método passa o quadro para o [consumeFrame] método da instância do OTVideoCaptureConsumer utilizada por este capturador de vídeo (descrito acima). Isso faz com que o editor envie o quadro de dados para o fluxo de vídeo na sessão.

Etapa 2: Captura de quadros de vídeo

O código deste exemplo também está incluído no Capturador Básico de Vídeo projeto do repositório opentok-ios-sdk-samples. Para usá-lo, remova o comentário da seguinte linha:

_publisher.videoCapture = [[OTKBasicVideoCapturerCamera alloc] initWithPreset:AVCaptureSessionPreset352x288 andDesiredFrameRate:30];

Em seguida, coloque a linha da parte 1 entre comentários:

// _publisher.videoCapture = [[OTKBasicVideoCapturer alloc] init];

Este projeto mostra como usar um capturador de vídeo personalizado, utilizando a câmera do dispositivo como fonte de vídeo.

Este código de exemplo utiliza o framework AVFoundation da Apple para capturar vídeo de uma câmera e publicá-lo em uma sessão conectada. A classe ViewController cria uma sessão, instancia os assinantes e configura o editor. O captureOutput O método cria um quadro, captura uma imagem da tela, marca o quadro com um carimbo de data e hora e o salva em uma instância do consumidor. O editor acessa o consumidor para obter o quadro do vídeo.

Observe que, como este exemplo precisa acessar a câmera do dispositivo, você deve testá-lo em um dispositivo iOS. Não é possível testá-lo no simulador do iOS.

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

O [OTKBasicVideoCapturerCamera initWithPreset: andDesiredFrameRate:] O método é um inicializador para a classe OTKBasicVideoCapturerCamera. Ele chama o sizeFromAVCapturePreset método para definir a resolução da imagem. O tamanho da imagem e a taxa de quadros também são definidos aqui. É criada uma fila separada para a captura de imagens, de modo a não afetar a fila da interface do usuário.

- (id)initWithPreset:(NSString *)preset andDesiredFrameRate:(NSUInteger)frameRate
{
    self = [super init];
    if (self) {
        self.sessionPreset = preset;
        CGSize imageSize = [self sizeFromAVCapturePreset:self.sessionPreset];
        _imageHeight = imageSize.height;
        _imageWidth = imageSize.width;
        _desiredFrameRate = frameRate;

        _captureQueue = dispatch_queue_create("com.tokbox.OTKBasicVideoCapturer",
          DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

O sizeFromAVCapturePreset O método identifica o valor da string correspondente à resolução da imagem no framework AVFoundation do iOS e retorna uma representação CGSize.

A implementação do [OTVideoCapture initCapture] O método utiliza a estrutura AVFoundation para configurar a câmera para capturar imagens. Na primeira parte do método, uma instância da classe AVCaptureVideoDataOutput é usada para gerar quadros de imagem:

- (void)initCapture
{
    NSError *error;
    self.captureSession = [[AVCaptureSession alloc] init];

   [self.captureSession beginConfiguration];

    // Set device capture
    self.captureSession.sessionPreset = self.sessionPreset;
    AVCaptureDevice *videoDevice =
      [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    self.inputDevice =
      [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    [self.captureSession addInput:self.inputDevice];

    AVCaptureVideoDataOutput *outputDevice = [[AVCaptureVideoDataOutput alloc] init];
    outputDevice.alwaysDiscardsLateVideoFrames = YES;
    outputDevice.videoSettings =
      @{(NSString *)kCVPixelBufferPixelFormatTypeKey:
        @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
      )};

    [outputDevice setSampleBufferDelegate:self queue:self.captureQueue];

    [self.captureSession addOutput:outputDevice];

    // See the next section ...
}

Os quadros capturados com esse método são acessados por meio do [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] método delegado. O objeto AVCaptureDevice representa a câmera e suas propriedades. Ele fornece as imagens capturadas a um objeto AVCaptureSession.

A segunda parte do initCapture O método chama o bestFrameRateForDevice método para obter a melhor taxa de quadros na captura de imagens:

- (void)initCapture
{
    // See previous section ...

    // Set framerate
    double bestFrameRate = [self bestFrameRateForDevice];

    CMTime desiredMinFrameDuration = CMTimeMake(1, bestFrameRate);
    CMTime desiredMaxFrameDuration = CMTimeMake(1, bestFrameRate);

    [self.inputDevice.device lockForConfiguration:&error];
    self.inputDevice.device.activeVideoMaxFrameDuration = desiredMaxFrameDuration;
    self.inputDevice.device.activeVideoMinFrameDuration = desiredMinFrameDuration;

    [self.captureSession commitConfiguration];

    self.format = [OTVideoFormat videoFormatNV12WithWidth:self.imageWidth
                                                   height:self.imageHeight];
}

O [self bestFrameRateForDevice] O método retorna a melhor taxa de quadros para o dispositivo de captura:

- (double)bestFrameRateForDevice
{
    double bestFrameRate = 0;
    for (AVFrameRateRange* range in
         self.inputDevice.device.activeFormat.videoSupportedFrameRateRanges)
    {
        CMTime currentDuration = range.minFrameDuration;
        double currentFrameRate = currentDuration.timescale / currentDuration.value;
        if (currentFrameRate > bestFrameRate && currentFrameRate < self.desiredFrameRate) {
            bestFrameRate = currentFrameRate;
        }
    }
    return bestFrameRate;
}

A estrutura AVFoundation exige um intervalo mínimo e máximo de taxas de quadros para otimizar a qualidade da captura de imagem. Esse intervalo é definido no bestFrameRate objeto. Por uma questão de simplicidade, a taxa de quadros mínima e máxima está definida como o mesmo valor, mas você pode definir suas próprias taxas de quadros mínima e máxima para obter melhor qualidade de imagem, de acordo com a velocidade da sua rede. Neste aplicativo, a taxa de quadros e a resolução são fixas.

Este método define o consumidor de captura de vídeo, definido pelo protocolo OTVideoCaptureConsumer.

- (void)setVideoCaptureConsumer:(id<OTVideoCaptureConsumer>)videoCaptureConsumer
{
    self.consumer = videoCaptureConsumer;
}

O [OTVideoCapture captureSettings] Este método define o formato de pixel e o tamanho da imagem utilizada pelo capturador de vídeo, configurando as propriedades do objeto OTVideoFormat.

O [OTVideoCapture currentDeviceOrientation] O método consulta a orientação da imagem no framework AVFoundation e retorna seu equivalente, conforme definido pela enumeração OTVideoOrientation no SDK do OpenTok para iOS.

Captura de quadros para o vídeo da editora

A implementação do [OTVideoCapture startCapture] O método é chamado quando o editor começa a capturar o vídeo para publicação. Ele chama o [AVCaptureSession startRunning] método do objeto AVCaptureSession:

- (int32_t)startCapture
{
    self.captureStarted = YES;
    [self.captureSession startRunning];

    return 0;
}

O [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] O método delegado é chamado quando um novo quadro de vídeo fica disponível na câmera.

- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection
{
    if (!self.captureStarted)
        return;

    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    OTVideoFrame *frame = [[OTVideoFrame alloc] initWithFormat:self.format];

    NSUInteger planeCount = CVPixelBufferGetPlaneCount(imageBuffer);

    uint8_t *buffer = malloc(sizeof(uint8_t) * CVPixelBufferGetDataSize(imageBuffer));
    uint8_t *dst = buffer;
    uint8_t *planes[planeCount];

    CVPixelBufferLockBaseAddress(imageBuffer, 0);
    for (int i = 0; i < planeCount; i++) {
        size_t planeSize = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, i)
          * CVPixelBufferGetHeightOfPlane(imageBuffer, i);

        planes[i] = dst;
        dst += planeSize;

        memcpy(planes[i],
                CVPixelBufferGetBaseAddressOfPlane(imageBuffer, i),
                planeSize);
    }

    CMTime minFrameDuration = self.inputDevice.device.activeVideoMinFrameDuration;
    frame.format.estimatedFramesPerSecond = minFrameDuration.timescale / minFrameDuration.value;
    frame.format.estimatedCaptureDelay = 100;
    frame.orientation = [self currentDeviceOrientation];

    CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
    frame.timestamp = time;
    [frame setPlanesWithPointers:planes numPlanes:planeCount];

    [self.consumer consumeFrame:frame];

    free(buffer);
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}

Este método realiza o seguinte:

  • Cria uma instância de OTVideoFrame para definir o novo quadro de vídeo.

  • Salva um buffer de imagem na memória com base no tamanho da imagem.

  • Grava os dados da imagem de dois planos em um único buffer de memória. Como a imagem é do formato NV12, seus dados estão distribuídos por dois planos. Há um plano para os dados Y e outro para os dados UV. Um loop “for” é executado para percorrer ambos os planos e gravar seus dados em um único buffer de memória.

  • Cria um carimbo de data e hora para marcar uma imagem capturada. Cada imagem é marcada com um carimbo de data e hora para que tanto o emissor quanto o receptor possam criar a mesma linha do tempo e referenciar os quadros na mesma ordem.

  • Chama o [OTVideoCaptureConsumer consumeFrame:] método, passando o objeto OTVideoFrame. Isso faz com que o publisher envie o quadro no fluxo que ele publica.

A implementação do [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didDropSampleBuffer:fromConnection] O método é chamado sempre que há um atraso no recebimento de quadros. Ele descarta quadros para continuar a publicação na sessão sem interrupção:

- (void)captureOutput:(AVCaptureOutput *)captureOutput
  didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
       fromConnection:(AVCaptureConnection *)connection
{
    NSLog(@"Frame dropped");
}

Outras observações sobre o aplicativo

O protocolo OTVideoCapture inclui outros métodos obrigatórios, que são implementados pela classe OTKBasicVideoCapturerCamera. No entanto, este exemplo não realiza nenhuma ação relevante nesses métodos; portanto, eles não serão abordados nesta discussão.

Parabéns! Você concluiu o tutorial sobre captura de vídeo personalizada para iOS.
Você pode continuar experimentando e ajustando o código que desenvolveu aqui, ou conferir o Próximos passos abaixo.

Próximos passos

Quando terminar esta seção, continue desenvolvendo e aprimorando seu aplicativo OpenTok com estes recursos úteis: