Captura de vídeo personalizada

Visión general

La Video API de Vonage permite realizar modificaciones en la capturadora de video para usarlas en aplicaciones de Android e iOS.

Este tutorial repasará:

  • Realiza modificaciones en el capturador de video de tu aplicación Vonage Video para Android
  • Realiza modificaciones en el capturador de video en tu aplicación Vonage Video iOS

Android

Antes de empezar

El código de esta sección está disponible en el proyecto Basic-Video-Capturer-Camera-2-Java del archivo opentok-android-sdk-samples repo. Si aún no lo has hecho, tendrás que clonar el repositorio en un directorio local. En la línea de comandos, ejecute:

git clone git@github.com:opentok/opentok-android-sdk-samples.git

Abre el proyecto Basic-Video-Capturer-Camera-2-Java en Android Studio para seguir el proceso.

Explorar el código

En este ejemplo, la aplicación utiliza un capturador de vídeo personalizado para reflejar una imagen de vídeo. Esto se hace para ilustrar los principios básicos de la configuración de un capturador de vídeo personalizado.

MirrorVideoCapturer es una clase personalizada que amplía la clase BaseVideoCapturer (definida en el SDK de Android). La dirección BaseVideoCapturer te permite definir una capturadora de video personalizada para ser utilizada por un editor de Vonage Video:

publisher = new Publisher.Builder(MainActivity.this)
    .capturer(new MirrorVideoCapturer(MainActivity.this))
    .build();

En getCaptureSettings() proporciona los ajustes utilizados por el capturador de vídeo personalizado:

@Override
public synchronized CaptureSettings getCaptureSettings() {
    CaptureSettings captureSettings = new CaptureSettings();
    captureSettings.fps = desiredFps;
    captureSettings.width = (null != cameraFrame) ? cameraFrame.getWidth() : 0;
    captureSettings.height = (null != cameraFrame) ? cameraFrame.getHeight() : 0;
    captureSettings.format = BaseVideoCapturer.NV21;
    captureSettings.expectedDelay = 0;
    return captureSettings;
}

En BaseVideoCapturer.CaptureSetting (que define la propiedad capturerSettings) está definida por el SDK de Android. En este código de ejemplo, el formato de la capturadora de vídeo está configurado para utilizar NV21 como formato de píxel, con un número específico de fotogramas por segundo, una altura específica y una anchura específica.

En BaseVideoCapturer startCapture() se llama cuando un editor comienza a capturar vídeo para enviarlo como flujo a la sesión. Esto ocurrirá después de que Session.publish(publisher) se llama al método:

@Override
public synchronized int startCapture() {
    Log.d(TAG,"startCapture enter (cameraState: "+ cameraState +")");

    if (null != camera && CameraState.OPEN == cameraState) {
        return startCameraCapture();
    } else if (CameraState.SETUP == cameraState) {
        Log.d(TAG,"camera not yet ready, queuing the start until camera is opened");
        executeAfterCameraOpened = () -> startCameraCapture();
    } else {
        throw new Camera2Exception("Start Capture called before init successfully completed");
    }

    Log.d(TAG,"startCapture exit");

    return 0;
}

iOS

Antes de empezar

El código para esta sección está en el proyecto Basic Video Capturer del repositorio opentok-ios-sdk-samples, así que si aún no lo has hecho, necesitarás clonar el repositorio en un directorio local - esto puede hacerse usando la línea de comandos:

git clone https://github.com/Vonage/vonage-video-ios-sdk-samples

Cambia de directorio al proyecto Capturador de Vídeo Básico:

cd vonage-video-ios-sdk-samples/SwiftUI/BasicVideoCapturer

A continuación, instala la dependencia de Vonage Video:

pod install

Explorar el código

Este proyecto muestra cómo hacer pequeñas modificaciones en el capturador de vídeo utilizado por la clase OTPublisher. Abre el proyecto en Xcode para seguirlo.

En este ejemplo, la aplicación utiliza un capturador de vídeo personalizado para publicar píxeles aleatorios (ruido blanco). Esto se hace simplemente para ilustrar los principios básicos de la configuración de un capturador de vídeo personalizado. (Para un ejemplo más práctico, consulte los ejemplos Capturador de vídeo de cámara y Capturador de vídeo de pantalla, descritos en las secciones siguientes).

En el ViewController principal, después de llamar a [_session publish:_publisher error:&error] para iniciar la publicación de un flujo de audio-vídeo, el videoCapture del objeto OTPublisher es una instancia de OTKBasicVideoCapturer:

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

OTKBasicVideoCapturer es una clase personalizada que implementa el protocolo OTVideoCapture (definido en el SDK de Vonage Video para iOS). Este protocolo te permite definir una capturadora de video personalizada para ser utilizada por un editor de Vonage Video.

En [OTVideoCapture initCapture:] inicializa la configuración de captura que utilizará el capturador de vídeo personalizado. En esta implementación personalizada de OTVideoCapture (OTKBasicVideoCapturer) el método initCapture establece las propiedades del método format de la instancia 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;
}

La clase OTVideoFormat (que define este format ) está definida por el SDK de Vonage Video para iOS. En este código de ejemplo, el formato de la capturadora de vídeo está configurado para utilizar ARGB como formato de píxel, con un número específico de bytes por fila, una altura específica y una anchura específica.

En [OTVideoCapture setVideoCaptureConsumer] establece un objeto OTVideoCaptureConsumer (definido por el SDK de Vonage Video para iOS) que el consumidor de video utiliza para transmitir cuadros de video a la transmisión del editor. En el OTKBasicVideoCapturer, este método establece una instancia local de OTVideoCaptureConsumer como el consumidor:

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

En [OTVideoCapture startCapture:] se llama cuando un editor comienza a capturar video para enviarlo como flujo a la sesión de Vonage Video. Esto ocurrirá después de que el [Session publish: error:] es llamado. En el OTKBasicVideoCapturer de este método, la función [self produceFrame] es llamado en una cola de fondo después de un intervalo establecido:

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

    return 0;
}

En [self produceFrame] genera un objeto OTVideoFrame (definido por el SDK de Vonage Video para iOS) que representa un cuadro de video. En este caso, el fotograma contiene píxeles aleatorios que rellenan la altura y la anchura definidas para el formato de vídeo de muestra:

- (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];
                       });
    }
}

El método pasa el marco al [consumeFrame] de la instancia de OTVideoCaptureConsumer utilizada por este capturador de vídeo (descrito anteriormente). Esto hace que el editor envíe el fotograma de datos al flujo de vídeo de la sesión.

El código de este ejemplo también se incluye en el archivo Capturador de vídeo básico del repositorio opentok-ios-sdk-samples. Para utilizarlo, descomente la siguiente línea:

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

Luego comenta la línea de la parte 1:

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

Este proyecto muestra cómo utilizar un capturador de vídeo personalizado utilizando la cámara del dispositivo como fuente de vídeo.

Este código de ejemplo utiliza el framework AVFoundation de Apple para capturar vídeo de una cámara y publicarlo en una sesión conectada. La clase ViewController crea una sesión, crea instancias de suscriptores y configura el editor. La página captureOutput crea un fotograma, realiza una captura de pantalla, etiqueta el fotograma con una marca de tiempo y lo guarda en una instancia del consumidor. El editor accede al consumidor para obtener el fotograma de vídeo.

Tenga en cuenta que, dado que este ejemplo necesita acceder a la cámara del dispositivo, debe probarlo en un dispositivo iOS. No puede probarlo en el simulador de iOS.

En [OTKBasicVideoCapturerCamera initWithPreset: andDesiredFrameRate:] es un inicializador para la clase OTKBasicVideoCapturerCamera. Llama al método sizeFromAVCapturePreset para establecer la resolución de la imagen. El tamaño de la imagen y la velocidad de fotogramas también se establecen aquí. Se crea una cola separada para capturar imágenes, para no afectar a la cola de la interfaz de usuario.

- (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;
}

En sizeFromAVCapturePreset identifica el valor de cadena de la resolución de la imagen en el marco AVFoundation de iOS y devuelve una representación CGSize.

La aplicación de la [OTVideoCapture initCapture] utiliza el framework AVFoundation para configurar la cámara para capturar imágenes. En la primera parte del método se utiliza una instancia de AVCaptureVideoDataOutput para producir fotogramas de imagen:

- (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 ...
}

A los fotogramas capturados con este método se accede con la función [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] método delegado. El objeto AVCaptureDevice representa la cámara y sus propiedades. Proporciona las imágenes capturadas a un objeto AVCaptureSession.

La segunda parte del initCapture llama al método bestFrameRateForDevice para obtener la mejor velocidad de fotogramas para la captura de imágenes:

- (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];
}

En [self bestFrameRateForDevice] devuelve la mejor frecuencia de imagen para el 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;
}

El framework AVFoundation requiere un rango mínimo y máximo de frecuencias de cuadro para optimizar la calidad de una captura de imagen. Este rango se establece en el bestFrameRate objeto. Para simplificar, la frecuencia de imagen mínima y máxima se establece como el mismo número, pero es posible que desee establecer sus propias frecuencias de imagen mínima y máxima para obtener una mejor calidad de imagen en función de la velocidad de su red. En esta aplicación, la frecuencia de imagen y la resolución son fijas.

Este método establece el consumidor de captura de vídeo, definido por el protocolo OTVideoCaptureConsumer.

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

En [OTVideoCapture captureSettings] establece el formato de píxel y el tamaño de la imagen utilizada por el capturador de vídeo, estableciendo las propiedades del objeto OTVideoFormat.

En [OTVideoCapture currentDeviceOrientation] consulta la orientación de la imagen en el framework AVFoundation y devuelve su equivalente definido por el enum OTVideoOrientation en el SDK de Vonage Video para iOS.

La aplicación de la [OTVideoCapture startCapture] se ejecuta cuando el editor empieza a capturar vídeo para publicarlo. Llama al método [AVCaptureSession startRunning] del objeto AVCaptureSession:

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

    return 0;
}

En [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] es llamado cuando un nuevo fotograma de vídeo está disponible desde la cámara.

- (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 hace lo siguiente:

  • Crea una instancia de OTVideoFrame para definir el nuevo fotograma de vídeo.

  • Guarda un búfer de imagen de la memoria en función del tamaño de la imagen.

  • Escribe datos de imagen de dos planos en un búfer de memoria. Como la imagen es una NV12, sus datos se distribuyen en dos planos. Hay un plano para los datos Y y un plano para los datos UV. Se ejecuta un bucle for para recorrer ambos planos y escribir sus datos en una memoria intermedia.

  • Crea una marca de tiempo para etiquetar una imagen capturada. Cada imagen se etiqueta con una marca de tiempo para que tanto el editor como el suscriptor puedan crear la misma línea de tiempo y referenciar los fotogramas en el mismo orden.

  • Llama al [OTVideoCaptureConsumer consumeFrame:] pasando el objeto OTVideoFrame. Esto hace que el editor envíe el fotograma en el flujo que publica.

La aplicación de la [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didDropSampleBuffer:fromConnection] es llamado cada vez que hay un retraso en la recepción de tramas. Deja caer tramas para seguir publicando en la sesión sin interrupciones:

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

Ver también