web-dev-qa-db-ja.com

複数のプレビューを含むAVCaptureSession

AVCaptureVideoPreviewLayerで実行されているAVCaptureSessionがあります。

私はビデオを見ることができるので、それが機能していることがわかります。

ただし、コレクションビューを作成し、各セルにプレビューレイヤーを追加して、各セルにビデオのプレビューが表示されるようにします。

プレビューレイヤーをセルに渡してサブレイヤーとして追加しようとすると、他のセルからレイヤーが削除されるため、一度に1つのセルにしか表示されません。

これを行う別の(より良い)方法はありますか?

28
Fogmeister

複数のライブビューを同時に表示する必要があるという同じ問題に遭遇しました。上記のUIImageを使用するという答えは、私が必要としていたものには遅すぎました。これが私が見つけた2つの解決策です:

1. CAReplicatorLayer

最初のオプションは、CAReplicatorLayerを使用してレイヤーを自動的に複製することです。ドキュメントに記載されているように、「...サブレイヤー(ソースレイヤー)の指定された数のコピーが自動的に作成されます。各コピーには、幾何学的、時間的、色の変換が適用される可能性があります。」

これは、単純な幾何学的変換または色変換以外にライブプレビューとのやり取りがあまりない場合に非常に便利です(フォトブースを考えてください)。私は、「反射」効果を作成する方法として使用されるCAReplicatorLayerを最も頻繁に見ました。

CACaptureVideoPreviewLayerを複製するためのサンプルコードを次に示します。

AVCaptureVideoPreviewLayerを初期化します

AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
[previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[previewLayer setFrame:CGRectMake(0.0, 0.0, self.view.bounds.size.width, self.view.bounds.size.height / 4)];

CAReplicatorLayerを初期化し、プロパティを設定します

注:これにより、ライブプレビューレイヤーが4回複製されます。

NSUInteger replicatorInstances = 4;

CAReplicatorLayer *replicatorLayer = [CAReplicatorLayer layer];
replicatorLayer.frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height / replicatorInstances);
replicatorLayer.instanceCount = instances;
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(0.0, self.view.bounds.size.height / replicatorInstances, 0.0);

レイヤーを追加する

注:私の経験から、複製するレイヤーをサブレイヤーとしてCAReplicatorLayerに追加する必要があります。

[replicatorLayer addSublayer:previewLayer];
[self.view.layer addSublayer:replicatorLayer];

欠点

CAReplicatorLayerを使用することの欠点は、レイヤーレプリケーションのすべての配置を処理することです。したがって、設定された変換を各インスタンスに適用し、すべてがそれ自体に含まれます。 例: 2つの別々のセルにAVCaptureVideoPreviewLayerを複製する方法はありません。


2.SampleBufferを手動でレンダリングする

この方法は、少し複雑ですが、CAReplicatorLayerの上記の欠点を解決します。ライブプレビューを手動でレンダリングすることにより、必要な数のビューをレンダリングできます。確かに、パフォーマンスが影響を受ける可能性があります。

注:SampleBufferをレンダリングする方法は他にもあるかもしれませんが、パフォーマンスのためにOpenGLを選択しました。コードは CIFunHouseからインスピレーションを受けて変更されました

これが私がそれを実装した方法です:

2.1コンテキストとセッション

OpenGLとCoreImageコンテキストを設定する

_eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

// Note: must be done after the all your GLKViews are properly set up
_ciContext = [CIContext contextWithEAGLContext:_eaglContext
                                       options:@{kCIContextWorkingColorSpace : [NSNull null]}];

ディスパッチキュー

このキューは、セッションとデリゲートに使用されます。

self.captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL);

AVSessionとAVCaptureVideoDataOutputを初期化します

注:これを読みやすくするために、すべてのデバイス機能チェックを削除しました。

dispatch_async(self.captureSessionQueue, ^(void) {
    NSError *error = nil;

    // get the input device and also validate the settings
    NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];

    AVCaptureDevice *_videoDevice = nil;
    if (!_videoDevice) {
        _videoDevice = [videoDevices objectAtIndex:0];
    }

    // obtain device input
    AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:self.videoDevice error:&error];

    // obtain the preset and validate the preset
    NSString *preset = AVCaptureSessionPresetMedium;

    // CoreImage wants BGRA pixel format
    NSDictionary *outputSettings = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};

    // create the capture session
    self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = preset;
    :

注:次のコードは「マジックコード」です。ここで、DataOutputを作成してAVSessionに追加し、デリゲートを使用してカメラフレームをインターセプトできるようにします。これは、問題を解決する方法を理解するために必要なブレークスルーです。

    :
    // create and configure video data output
    AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    videoDataOutput.videoSettings = outputSettings;
    [videoDataOutput setSampleBufferDelegate:self queue:self.captureSessionQueue];

    // begin configure capture session
    [self.captureSession beginConfiguration];

    // connect the video device input and video data and still image outputs
    [self.captureSession addInput:videoDeviceInput];
    [self.captureSession addOutput:videoDataOutput];

    [self.captureSession commitConfiguration];

    // then start everything
    [self.captureSession startRunning];
});

2.2OpenGLビュー

ライブプレビューのレンダリングにはGLKViewを使用しています。したがって、4つのライブプレビューが必要な場合は、4つのGLKViewが必要です。

self.livePreviewView = [[GLKView alloc] initWithFrame:self.bounds context:self.eaglContext];
self.livePreviewView = NO;

背面カメラからのネイティブビデオ画像はUIDeviceOrientationLandscapeLeftにあるため(つまり、ホームボタンは右側にあります)、時計回りに90度の変換を適用して、横向きのビューにいるかのようにビデオプレビューを描画できるようにする必要があります。 ;フロントカメラを使用していて、ミラーリングされたプレビューが必要な場合(ユーザーがミラーで自分自身を見ることができるようにするため)、回転に追加の水平フリップを適用する必要があります(CGAffineTransformMakeScale(-1.0、1.0)を連結することにより)変換)

self.livePreviewView.transform = CGAffineTransformMakeRotation(M_PI_2);
self.livePreviewView.frame = self.bounds;    
[self addSubview: self.livePreviewView];

フレームバッファをバインドして、フレームバッファの幅と高さを取得します。 GLKViewに描画するときにCIContextによって使用される境界はピクセル単位(ポイントではない)であるため、フレームバッファーの幅と高さから読み取る必要があります。

[self.livePreviewView bindDrawable];

さらに、別のキュー(_captureSessionQueue)の境界にアクセスするため、この情報を取得して、別のスレッド/キューから_videoPreviewViewのプロパティにアクセスしないようにします。

_videoPreviewViewBounds = CGRectZero;
_videoPreviewViewBounds.size.width = _videoPreviewView.drawableWidth;
_videoPreviewViewBounds.size.height = _videoPreviewView.drawableHeight;

dispatch_async(dispatch_get_main_queue(), ^(void) {
    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_2);        

    // *Horizontally flip here, if using front camera.*

    self.livePreviewView.transform = transform;
    self.livePreviewView.frame = self.bounds;
});

注:フロントカメラを使用している場合は、次のようにライブプレビューを水平方向に反転できます:

transform = CGAffineTransformConcat(transform, CGAffineTransformMakeScale(-1.0, 1.0));

2.3デリゲートの実装

Contexts、Sessions、およびGLKViewsを設定したら、AVCaptureVideoDataOutputSampleBufferDelegateメソッドcaptureOutput:didOutputSampleBuffer:fromConnection:からビューにレンダリングできます。

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);

    // update the video dimensions information
    self.currentVideoDimensions = CMVideoFormatDescriptionGetDimensions(formatDesc);

    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:(CVPixelBufferRef)imageBuffer options:nil];

    CGRect sourceExtent = sourceImage.extent;
    CGFloat sourceAspect = sourceExtent.size.width / sourceExtent.size.height;

各GLKViewとそのvideoPreviewViewBoundsへの参照が必要になります。簡単にするために、両方ともUICollectionViewCellに含まれていると仮定します。独自のユースケースに合わせてこれを変更する必要があります。

    for(CustomLivePreviewCell *cell in self.livePreviewCells) {
        CGFloat previewAspect = cell.videoPreviewViewBounds.size.width  / cell.videoPreviewViewBounds.size.height;

        // To maintain the aspect radio of the screen size, we clip the video image
        CGRect drawRect = sourceExtent;
        if (sourceAspect > previewAspect) {
            // use full height of the video image, and center crop the width
            drawRect.Origin.x += (drawRect.size.width - drawRect.size.height * previewAspect) / 2.0;
            drawRect.size.width = drawRect.size.height * previewAspect;
        } else {
            // use full width of the video image, and center crop the height
            drawRect.Origin.y += (drawRect.size.height - drawRect.size.width / previewAspect) / 2.0;
            drawRect.size.height = drawRect.size.width / previewAspect;
        }

        [cell.livePreviewView bindDrawable];

        if (_eaglContext != [EAGLContext currentContext]) {
            [EAGLContext setCurrentContext:_eaglContext];
        }

        // clear eagl view to grey
        glClearColor(0.5, 0.5, 0.5, 1.0);
        glClear(GL_COLOR_BUFFER_BIT);

        // set the blend mode to "source over" so that CI will use that
        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

        if (sourceImage) {
            [_ciContext drawImage:sourceImage inRect:cell.videoPreviewViewBounds fromRect:drawRect];
        }

        [cell.livePreviewView display];
    }
}

このソリューションでは、OpenGLを使用して、AVCaptureVideoDataOutputSampleBufferDelegateから受信した画像のバッファーをレンダリングするために必要な数のライブプレビューを作成できます。

3.サンプルコード

これが私が両方の解決策と一緒に投げたgithubプロジェクトです: https://github.com/JohnnySlagle/Multiple-Camera-Feeds

59
Johnny

AVCaptureSessionデリゲートメソッドを実装します。

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection

これを使用すると、すべてのビデオフレームのサンプルバッファ出力を取得できます。バッファ出力を使用すると、以下の方法でイメージを作成できます。

- (UIImage *) imageFromSampleBuffer:(CMSampleBufferRef) sampleBuffer 
{
    // Get a CMSampleBuffer's Core Video image buffer for the media data
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); 
    // Lock the base address of the pixel buffer
    CVPixelBufferLockBaseAddress(imageBuffer, 0); 

    // Get the number of bytes per row for the pixel buffer
    void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer); 

    // Get the number of bytes per row for the pixel buffer
    size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); 
    // Get the pixel buffer width and height
    size_t width = CVPixelBufferGetWidth(imageBuffer); 
    size_t height = CVPixelBufferGetHeight(imageBuffer); 

    // Create a device-dependent RGB color space
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 

    // Create a bitmap graphics context with the sample buffer data
    CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, 
                                                 bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); 
    // Create a Quartz image from the pixel data in the bitmap graphics context
    CGImageRef quartzImage = CGBitmapContextCreateImage(context); 
    // Unlock the pixel buffer
    CVPixelBufferUnlockBaseAddress(imageBuffer,0);

    // Free up the context and color space
    CGContextRelease(context); 
    CGColorSpaceRelease(colorSpace);

    // Create an image object from the Quartz image
      UIImage *image = [UIImage imageWithCGImage:quartzImage scale:1.0 orientation:UIImageOrientationRight];

    // Release the Quartz image
    CGImageRelease(quartzImage);

    return (image);
}

したがって、ビューにいくつかのimageViewを追加し、前述のデリゲートメソッド内にこれらの行を追加できます。

UIImage *image = [self imageFromSampleBuffer:sampleBuffer];
imageViewOne.image = image;
imageViewTwo.image = image;
8
Ushan87

プレビューレイヤーのコンテンツを別のCALayerに設定するだけです。

CGImageRef cgImage =(__ bridge CGImage)self.previewLayer.contents; self.duplicateLayer.contents =(__ bridge id)cgImage;

これは、任意のMetalまたはOpenGLレイヤーのコンテンツで実行できます。私の側でも、メモリ使用量やCPU負荷の増加はありませんでした。小さなポインタ以外は何も複製していません。これらの他の「ソリューション」ではそうではありません。

1つのカメラフィードから同時に20のプレビューレイヤーを表示するダウンロード可能なサンプルプロジェクトがあります。各レイヤーには、異なる効果が適用されます。

実行中のアプリのビデオを視聴したり、次の場所からソースコードをダウンロードしたりできます。

https://demonicactivity.blogspot.com/2017/05/developer-iphone-video-camera-wall.html?m=1

1
James Bush

IOS13のSwift 5)で作業し、@ Ushan87による回答のやや単純なバージョンを実装しました。テスト目的で、既存のAVCaptureVideoPreviewLayerの上に新しい小さなUIImageViewをドラッグしました。そのウィンドウのViewControllerで、新しいビューのIBOutletと、使用されているカメラの正しい向きを記述する変数を追加しました。

    @IBOutlet var testView: UIImageView!
    private var extOrientation: UIImage.Orientation = .up

次に、AVCaptureVideoDataOutputSampleBufferDelegateを次のように実装しました。

// MARK: - AVCaptureVideoDataOutputSampleBufferDelegate
extension CameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ captureOutput: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {

        let imageBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
        let ciimage : CIImage = CIImage(cvPixelBuffer: imageBuffer)
        let image : UIImage = self.convert(cmage: ciimage)

        DispatchQueue.main.sync(execute: {() -> Void in
            testView.image = image
        })

    }

    // Convert CIImage to CGImage
    func convert(cmage:CIImage) -> UIImage
    {
        let context:CIContext = CIContext.init(options: nil)
        let cgImage:CGImage = context.createCGImage(cmage, from: cmage.extent)!
        let image:UIImage = UIImage.init(cgImage: cgImage, scale: 1.0, orientation: extOrientation)
        return image
    }

私の目的では、パフォーマンスは良好でした。新しい見方では、遅れは見られませんでした。

0
Melissa