カスタム動画のキャプチャ(iOS)

概要

このチュートリアルでは、OpenTok iOS アプリケーションのビデオキャプチャ機能に変更を加えるために必要な手順を順を追って説明します。

ステップ 1: キャプチャの初期化

このセクションのコードは 基本的なビデオキャプチャ のプロジェクトである。 opentok-ios-sdk-samples リポジトリもしまだなら、レポをローカル・ディレクトリにクローンする必要がある。 これはコマンドラインを使って実行できる:

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

ディレクトリをBasic Video Capturerプロジェクトに変更する:

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

次に、OpenTokの依存関係をインストールします:

pod install

このプロジェクトでは、OTPublisherクラスで使用されているビデオ・キャプチャーにちょっとした変更を加える方法を紹介します。このプロジェクトをXcodeで開いてください。

この例では、アプリはカスタムビデオキャプチャを使用してランダムピクセル(ホワイトノイズ)を公開しています。これは、カスタムビデオキャプチャの基本的な設定方法を説明するためのものです。(より実用的な例については、この後のセクションで説明するカメラビデオキャプチャとスクリーンビデオキャプチャの例を参照してください)

メインの ViewController で [_session publish:_publisher error:&error] オーディオ・ビデオ・ストリームのパブリッシングを開始するには videoCapture プロパティはOTKBasicVideoCapturerのインスタンスに設定される:

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

OTKBasicVideoCapturer は、OTVideoCapture プロトコル(OpenTok iOS SDK で定義されている)を実装するカスタムクラスです。このプロトコルを使用することで、OpenTok パブリッシャーが使用するカスタムビデオキャプチャ機能を定義することができます。

について [OTVideoCapture initCapture:] メソッドは、カスタムビデオキャプチャで使用するキャプチャ設定を初期化します。このサンプルのOTVideoCaptureのカスタム実装(OTKBasicVideoCapturer)では initCapture メソッドは format 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;
}

OTVideoFormat クラス(このクラスを定義している。 format プロパティ)は、OpenTok iOS SDK によって定義されています。このサンプルコードでは、ビデオキャプチャのフォーマットが、ピクセルフォーマットとして ARGB を使用し、行あたりのバイト数、高さ、幅がそれぞれ指定されるように設定されています。

について [OTVideoCapture setVideoCaptureConsumer] ビデオコンシューマーがパブリッシャーのストリームにビデオフレームを送信するために使用する OTVideoCaptureConsumer オブジェクト(OpenTok iOS SDK で定義されている)を設定します。OTKBasicVideoCapturer では、このメソッドによってローカルの OTVideoCaptureConsumer インスタンスがコンシューマーとして設定されます:

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

について [OTVideoCapture startCapture:] このメソッドは、パブリッシャーがOpenTokセッションへストリームとして送信するための動画のキャプチャを開始したときに呼び出されます。これは、 [Session publish: error:] メソッドが呼び出される。このメソッドのOTKBasicVideoCapturerでは [self produceFrame] メソッドは、設定された間隔の後にバックグラウンド・キューで呼び出される:

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

    return 0;
}

について [self produceFrame] このメソッドは、ビデオの1フレームを表すOTVideoFrameオブジェクト(OpenTok iOS SDKで定義されている)を生成します。この場合、そのフレームには、サンプルビデオ形式で定義された高さと幅を埋めるランダムなピクセルが含まれます:

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

このメソッドは、フレームを [consumeFrame] メソッドは、このビデオキャプチャによって使用される OTVideoCaptureConsumer のインスタンスのメソッドです(上述)。これにより、パブリッシャはデータのフレームをセッション内のビデオストリームに送信します。

ステップ2:動画フレームのキャプチャ

このサンプルのコードも 基本的なビデオキャプチャ のプロジェクトである。 opentok-ios-sdk-samples リポジトリ. これを使用するには、次の行のコメントを外してください:

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

次に、パート1の行をコメントアウトする:

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

このプロジェクトでは、デバイスのカメラをビデオソースとして使用するカスタムビデオキャプチャの使い方を紹介します。

このサンプルコードでは、Apple AVFoundation フレームワークを使用してカメラからビデオをキャプチャし、接続されたセッションに公開します。ViewController クラスはセッションを作成し、購読者をインスタンス化して発行者を設定します。そして captureOutput メソッドはフレームを作成し、スクリーンショットをキャプチャし、フレームにタイムスタンプを付けてコンシューマーのインスタンスに保存します。パブリッシャーはコンシューマーにアクセスしてビデオフレームを取得します。

このサンプルはデバイスのカメラにアクセスする必要があるため、iOSデバイスでテストする必要があることに注意してください。iOSシミュレーターではテストできません。

ビデオキャプチャの初期化と設定

について [OTKBasicVideoCapturerCamera initWithPreset: andDesiredFrameRate:] メソッドはOTKBasicVideoCapturerCameraクラスのイニシャライザです。このメソッドは sizeFromAVCapturePreset メソッドを使って画像の解像度を設定する。画像サイズとフレームレートもここで設定する。UIのキューに影響を与えないように、画像をキャプチャするための別のキューが作成されます。

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

について sizeFromAVCapturePreset メソッドは、iOS AVFoundation フレームワークの画像解像度の文字列値を特定し、CGSize 表現を返します。

を実施した。 [OTVideoCapture initCapture] メソッドは AVFoundation フレームワークを使用して、画像をキャプチャするためにカメラを設定します。メソッドの最初の部分では、AVCaptureVideoDataOutput のインスタンスが画像フレームを生成するために使用されます:

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

このメソッドでキャプチャされたフレームには [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] デリゲート・メソッドを使用します。AVCaptureDevice オブジェクトは、カメラとそのプロパティを表します。AVCaptureSessionオブジェクトにキャプチャ画像を提供します。

の第2部である。 initCapture メソッドは bestFrameRateForDevice 画像キャプチャに最適なフレームレートを得るための方法:

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

について [self bestFrameRateForDevice] メソッドは、キャプチャデバイスに最適なフレームレートを返します:

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

AVFoundationフレームワークは、画像キャプチャの品質を最適化するために、フレームレートの最小範囲と最大範囲を必要とします。この範囲は bestFrameRate オブジェクトを使用してください。簡単のため、最小フレームレートと最大フレームレートは同じ数値に設定されていますが、ネットワークの速度に応じて、より良い画質を得るために独自の最小フレームレートと最大フレームレートを設定することもできます。このアプリケーションでは、フレームレートと解像度は固定です。

このメソッドは、OTVideoCaptureConsumer プロトコルで定義されたビデオ・キャプチャー・コンシューマーを設定する。

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

について [OTVideoCapture captureSettings] メソッドは、OTVideoFormatオブジェクトのプロパティを設定することによって、ビデオキャプチャで使用される画像のピクセルフォーマットとサイズを設定します。

について [OTVideoCapture currentDeviceOrientation] このメソッドは、AVFoundationフレームワークにおける画像の向きを照会し、OpenTok iOS SDKのOTVideoOrientation列挙型で定義されているそれに相当する値を返します。

パブリッシャーのビデオ用フレームのキャプチャ

を実施した。 [OTVideoCapture startCapture] メソッドはパブリッシャーが公開する動画のキャプチャを開始するときに呼び出されます。このメソッドは [AVCaptureSession startRunning] メソッドを呼び出します:

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

    return 0;
}

について [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didOutputSampleBuffer:fromConnection:] デリゲートメソッドは、カメラから新しいビデオフレームが利用可能になったときに呼び出されます。

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

この方法は次のようなものだ:

  • 新しいビデオフレームを定義する OTVideoFrame インスタンスを作成する。

  • 画像のサイズに基づき、メモリ上の画像バッファを保存する。

  • 2つのプレーンの画像データを1つのメモリバッファに書き込む。画像はNV12なので、そのデータは2つのプレーンに分散されている。Yデータ用のプレーンとUVデータ用のプレーンがある。forループが実行され、両方のプレーンを繰り返し処理し、それらのデータを1つのメモリバッファに書き込む。

  • キャプチャした画像にタグ付けするタイムスタンプを作成します。すべての画像にタイムスタンプが付けられるので、パブリッシャーもサブスクライバーも同じタイムラインを作成し、同じ順番でフレームを参照することができます。

  • を呼び出す。 [OTVideoCaptureConsumer consumeFrame:] メソッドに OTVideoFrame オブジェクトを渡します。これにより、パブリッシャはパブリッシュするストリームでフレームを送信します。

を実施した。 [AVCaptureVideoDataOutputSampleBufferDelegate captureOutput:didDropSampleBuffer:fromConnection] メソッドはフレームの受信に遅延が発生するたびに呼び出されます。中断することなくセッションへのパブリッシュを続けるために、フレームをドロップします:

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

アプリに関するその他の注意事項

OTVideoCapture プロトコルには他にも必須メソッドが含まれていますが、これらは OTKBasicVideoCapturerCamera クラスによって実装されています。ただし、このサンプルではこれらのメソッドで特に興味深い処理は行われていないため、本稿ではこれらについては触れません。

おめでとうございます!iOS向け「カスタム動画キャプチャ」チュートリアルを完了しました。
ここで作成したコードをさらに試したり調整したりすることもできますし、あるいは 次のステップ の下にある。

次のステップ

ここでの作業が終わったら、以下の役立つリソースを活用して、OpenTokアプリケーションの構築と機能強化を続けてください: