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:
- Noções básicas sobre a Video API — Se ainda não o fez, reserve alguns minutos para aprender exatamente como o OpenTok funciona
- Exemplos de código — uma coleção de aplicativos de exemplo que exploram diversos recursos e casos de uso do OpenTok
- Guias para desenvolvedores — documentação detalhada sobre todos os recursos e funcionalidades do OpenTok
- Tutoriais adicionais — guias passo a passo para a implementação de outros recursos e casos de uso do OpenTok
- Referência do SDK do iOS — explore os métodos e eventos específicos associados ao SDK do OpenTok para iOS