Benutzerdefinierte Videoaufzeichnung

Übersicht

Die Vonage Video API ermöglicht es, den Video-Capturer für die Verwendung in Android- und iOS-Applikationen zu modifizieren.

In dieser Anleitung wird das Thema behandelt:

  • Nehmen Sie Änderungen am Video-Capturer in Ihrer Vonage Video Android-Anwendung vor
  • Nehmen Sie Änderungen am Video-Capturer in Ihrer Vonage Video iOS-Anwendung vor

Android

Bevor Sie beginnen

Der Code für diesen Abschnitt ist im Basic-Video-Capturer-Camera-2-Java-Projekt des opentok-android-sdk-Beispiele Repo. Wenn Sie dies noch nicht getan haben, müssen Sie das Projektarchiv in ein lokales Verzeichnis klonen. Führen Sie in der Befehlszeile Folgendes aus:

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

Öffnen Sie das Basic-Video-Capturer-Camera-2-Java-Projekt in Android Studio, um dem Projekt zu folgen.

Erforschung des Codes

In diesem Beispiel verwendet die Anwendung einen benutzerdefinierten Video-Capturer, um ein Videobild zu spiegeln. Dies geschieht, um die grundlegenden Prinzipien der Einrichtung eines benutzerdefinierten Video-Capturers zu veranschaulichen.

MirrorVideoCapturer ist eine benutzerdefinierte Klasse, die die BaseVideoCapturer Klasse (definiert im Android SDK). Die BaseVideoCapturer Klasse können Sie einen benutzerdefinierten Video-Capturer definieren, der von einem Vonage Video Publisher verwendet wird:

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

Die getCaptureSettings() Methode bietet Einstellungen, die von dem benutzerdefinierten Video-Capturer verwendet werden:

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

Die BaseVideoCapturer.CaptureSetting Klasse (die die Eigenschaft capturerSettings definiert) wird vom Android SDK definiert. In diesem Beispielcode wird das Format des Video-Capturers so eingestellt, dass NV21 als Pixelformat verwendet wird, mit einer bestimmten Anzahl von Bildern pro Sekunde, einer bestimmten Höhe und einer bestimmten Breite.

Die BaseVideoCapturer startCapture() Methode wird aufgerufen, wenn ein Verleger mit der Aufnahme von Videos beginnt, die als Stream an die Sitzung gesendet werden sollen. Dies geschieht, nachdem die Session.publish(publisher) Methode aufgerufen wird:

@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

Bevor Sie beginnen

Der Code für diesen Abschnitt befindet sich im Basic Video Capturer Projekt des opentok-ios-sdk-samples Repos. Wenn Sie dies noch nicht getan haben, müssen Sie das Repo in ein lokales Verzeichnis klonen - dies kann über die Kommandozeile geschehen:

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

Wechseln Sie in das Verzeichnis des Basic Video Capturer-Projekts:

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

Installieren Sie dann die Vonage Video-Abhängigkeit:

pod install

Erforschung des Codes

Dieses Projekt zeigt Ihnen, wie Sie kleinere Änderungen an dem von der Klasse OTPublisher verwendeten Video-Capturer vornehmen können. Öffnen Sie das Projekt in Xcode, um es nachzuvollziehen.

In diesem Beispiel verwendet die Anwendung einen benutzerdefinierten Video-Capturer, um zufällige Pixel (weißes Rauschen) zu veröffentlichen. Dies dient lediglich zur Veranschaulichung der grundlegenden Prinzipien beim Einrichten eines benutzerdefinierten Video-Capturers. (Ein praktischeres Beispiel finden Sie in den Beispielen Kamera-Video-Capturer und Bildschirm-Video-Capturer, die in den folgenden Abschnitten beschrieben werden).

Im Haupt-ViewController, nach dem Aufruf von [_session publish:_publisher error:&error] um die Veröffentlichung eines Audio-Video-Streams zu veranlassen, die videoCapture des OTPublisher-Objekts wird auf eine Instanz von OTKBasicVideoCapturer gesetzt:

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

OTKBasicVideoCapturer ist eine benutzerdefinierte Klasse, die das OTVideoCapture Protokoll implementiert (definiert im Vonage Video iOS SDK). Mit diesem Protokoll können Sie einen benutzerdefinierten Video-Capturer definieren, der von einem Vonage Video-Publisher verwendet werden kann.

Die [OTVideoCapture initCapture:] Methode initialisiert die Aufnahmeeinstellungen, die von dem benutzerdefinierten Video-Capturer verwendet werden sollen. In der benutzerdefinierten Implementierung von OTVideoCapture (OTKBasicVideoCapturer) in diesem Beispiel wird die initCapture Methode setzt Eigenschaften der format Eigenschaft der OTVideoCapture-Instanz:

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

Die Klasse OTVideoFormat (die dieses Format definiert) format Eigenschaft) ist durch das Vonage Video iOS SDK definiert. In diesem Beispielcode ist das Format des Video-Capturers so eingestellt, dass ARGB als Pixelformat verwendet wird, mit einer bestimmten Anzahl von Bytes pro Zeile, einer bestimmten Höhe und einer bestimmten Breite.

Die [OTVideoCapture setVideoCaptureConsumer] setzt ein OTVideoCaptureConsumer-Objekt (definiert durch das Vonage Video iOS SDK), das der Videokonsument verwendet, um Videobilder an den Stream des Herausgebers zu übertragen. Im OTKBasicVideoCapturer setzt diese Methode eine lokale OTVideoCaptureConsumer-Instanz als Verbraucher:

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

Die [OTVideoCapture startCapture:] Methode wird aufgerufen, wenn ein Publisher mit der Aufnahme von Videos beginnt, um diese als Stream an die Vonage Video-Sitzung zu senden. Dies geschieht, nachdem die [Session publish: error:] Methode aufgerufen wird. In dem OTKBasicVideoCapturer dieser Methode wird die [self produceFrame] Methode wird nach einem festgelegten Intervall in einer Hintergrundwarteschlange aufgerufen:

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

    return 0;
}

Die [self produceFrame] Methode erzeugt ein OTVideoFrame-Objekt (definiert durch das Vonage Video iOS SDK), das ein Videobild darstellt. In diesem Fall enthält der Frame zufällige Pixel, die die definierte Höhe und Breite für das Beispielvideoformat ausfüllen:

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

Die Methode übergibt den Rahmen an die [consumeFrame] Methode der Instanz von OTVideoCaptureConsumer, die von diesem Video-Capturer verwendet wird (siehe oben). Dies veranlasst den Herausgeber, das Datenbild an den Videostream in der Sitzung zu senden.

Der Code für dieses Beispiel ist auch in der Basic Video Capturer Projekt des opentok-ios-sdk-samples Repos. Um es zu verwenden, entfernen Sie das Kommentarzeichen in der folgenden Zeile:

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

Dann kommentieren Sie die Zeile aus Teil 1 aus:

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

Dieses Projekt zeigt Ihnen, wie Sie einen benutzerdefinierten Video-Capturer verwenden, der die Gerätekamera als Videoquelle nutzt.

Dieser Beispielcode verwendet das Apple AVFoundation-Framework, um Videos von einer Kamera aufzunehmen und in einer verbundenen Sitzung zu veröffentlichen. Die ViewController-Klasse erstellt eine Sitzung, instanziiert Abonnenten und richtet den Herausgeber ein. Die captureOutput Methode erstellt ein Bild, nimmt einen Screenshot auf, versieht das Bild mit einem Zeitstempel und speichert es in einer Instanz von consumer. Der Herausgeber greift auf den Verbraucher zu, um das Videobild zu erhalten.

Da dieses Beispiel auf die Kamera des Geräts zugreifen muss, müssen Sie es auf einem iOS-Gerät testen. Sie können es nicht im iOS-Simulator testen.

Die [OTKBasicVideoCapturerCamera initWithPreset: andDesiredFrameRate:] Methode ist ein Initialisierer für die Klasse OTKBasicVideoCapturerCamera. Sie ruft die sizeFromAVCapturePreset um die Auflösung des Bildes festzulegen. Auch die Bildgröße und die Bildrate werden hier eingestellt. Für die Erfassung von Bildern wird eine separate Warteschlange erstellt, um die UI-Warteschlange nicht zu beeinträchtigen.

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

Die sizeFromAVCapturePreset Methode identifiziert den String-Wert der Bildauflösung im iOS AVFoundation Framework und gibt eine CGSize-Darstellung zurück.

Die Umsetzung der [OTVideoCapture initCapture] Methode verwendet das AVFoundation-Framework, um die Kamera auf die Aufnahme von Bildern einzustellen. Im ersten Teil der Methode wird eine Instanz von AVCaptureVideoDataOutput verwendet, um Bildrahmen zu erzeugen:

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

Auf die mit dieser Methode erfassten Bilder wird mit der Methode [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] Delegat-Methode. Das AVCaptureDevice-Objekt repräsentiert die Kamera und ihre Eigenschaften. Es liefert die aufgenommenen Bilder an ein AVCaptureSession-Objekt.

Der zweite Teil der initCapture Methode ruft die bestFrameRateForDevice Methode, um die beste Bildrate für die Bildaufnahme zu erhalten:

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

Die [self bestFrameRateForDevice] Methode gibt die beste Bildrate für das Aufnahmegerät zurück:

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

Das AVFoundation-Framework benötigt einen minimalen und maximalen Bereich von Bildraten, um die Qualität einer Bildaufnahme zu optimieren. Dieser Bereich wird in der bestFrameRate Objekt. Der Einfachheit halber sind die minimale und die maximale Bildrate auf dieselbe Zahl eingestellt, aber vielleicht möchten Sie Ihre eigenen minimalen und maximalen Bildraten festlegen, um eine bessere Bildqualität zu erhalten, die von der Geschwindigkeit Ihres Netzwerks abhängt. In dieser Anwendung sind die Bildrate und die Auflösung fest eingestellt.

Mit dieser Methode wird der durch das OTVideoCaptureConsumer-Protokoll definierte Videoaufnahmekonsument festgelegt.

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

Die [OTVideoCapture captureSettings] Methode legt das Pixelformat und die Größe des vom Video-Capturer verwendeten Bildes fest, indem Eigenschaften des OTVideoFormat-Objekts gesetzt werden.

Die [OTVideoCapture currentDeviceOrientation] Methode fragt die Ausrichtung des Bildes im AVFoundation Framework ab und gibt das Äquivalent zurück, das durch das OTVideoOrientation Enum im Vonage Video iOS SDK definiert ist.

Die Umsetzung der [OTVideoCapture startCapture] Methode wird aufgerufen, wenn der Herausgeber mit der Aufnahme von Videos zur Veröffentlichung beginnt. Sie ruft die [AVCaptureSession startRunning] Methode des AVCaptureSession-Objekts:

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

    return 0;
}

Die [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] Delegate-Methode wird aufgerufen, wenn ein neues Videobild von der Kamera verfügbar ist.

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

Diese Methode bewirkt Folgendes:

  • Erzeugt eine OTVideoFrame-Instanz, um das neue Videobild zu definieren.

  • Speichert einen Bildpuffer in Abhängigkeit von der Größe des Bildes.

  • Schreibt die Bilddaten von zwei Ebenen in einen Speicherpuffer. Da es sich bei dem Bild um ein NV12 handelt, sind seine Daten auf zwei Ebenen verteilt. Es gibt eine Ebene für Y-Daten und eine Ebene für UV-Daten. Eine for-Schleife wird ausgeführt, um die beiden Ebenen zu durchlaufen und ihre Daten in einen Speicherpuffer zu schreiben.

  • Erzeugt einen Zeitstempel, um ein aufgenommenes Bild zu kennzeichnen. Jedes Bild wird mit einem Zeitstempel versehen, so dass Herausgeber und Abonnent die gleiche Zeitleiste erstellen und die Bilder in der gleichen Reihenfolge referenzieren können.

  • Ruft die [OTVideoCaptureConsumer consumeFrame:] Methode und übergibt dabei das OTVideoFrame-Objekt. Dies veranlasst den Herausgeber, das Bild in dem von ihm veröffentlichten Stream zu senden.

Die Umsetzung der [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didDropSampleBuffer:fromConnection] wird immer dann aufgerufen, wenn es eine Verzögerung beim Empfang von Frames gibt. Sie verwirft Frames, um die Veröffentlichung in der Sitzung nicht zu unterbrechen:

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

Siehe auch