web-dev-qa-db-fra.com

AVCaptureSession avec plusieurs aperçus

J'ai une session AVCapture en cours d'exécution avec une couche AVCaptureVideoPreviewLayer.

Je peux voir la vidéo, donc je sais que ça marche.

Cependant, j'aimerais avoir une vue de collection et ajouter dans chaque cellule un calque d'aperçu afin que chaque cellule affiche un aperçu de la vidéo.

Si j'essaie de transmettre le calque d'aperçu dans la cellule et de l'ajouter en tant que sous-calque, il supprime le calque des autres cellules afin qu'il ne s'affiche jamais que dans une cellule à la fois.

Y a-t-il une autre (meilleure) façon de faire cela?

27
Fogmeister

J'ai rencontré le même problème d'avoir besoin de plusieurs vues en direct affichées en même temps. La réponse à l’utilisation de UIImage ci-dessus était trop lente pour ce dont j'avais besoin. Voici les deux solutions que j'ai trouvées:

1. couche de réparation

La première option consiste à utiliser unCAReplicatorLayerpour dupliquer le calque automatiquement. Comme le dit la documentation, il créera automatiquement "... un nombre spécifié de copies de ses sous-couches (la couche source), chaque copie ayant potentiellement des transformations géométriques, temporelles et de couleur qui lui sont appliquées".

C'est super utile s'il n'y a pas beaucoup d'interaction avec les aperçus en direct en plus de simples transformations géométriques ou de couleur (Think Photo Booth). J'ai le plus souvent vu la couche CAReplicatorLayer utilisée pour créer l'effet de "réflexion".

Voici un exemple de code permettant de répliquer un CACaptureVideoPreviewLayer:

Init 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)];

Init CAReplicatorLayer et définir les propriétés

_ {Remarque: ceci répliquera la couche d'aperçu en direct quatre fois.} _

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

Ajouter des couches

Remarque: d'après mon expérience, vous devez ajouter la couche que vous souhaitez répliquer dans CAReplicatorLayer en tant que sous-couche.

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

Inconvénients

L’utilisation de CAReplicatorLayer présente un inconvénient: elle gère tout le placement des réplications de calques. Ainsi, il appliquera toutes les transformations d’ensembles à chaque instance et tout sera contenu en son sein. Par exemple, il n'y aurait aucun moyen d'avoir une réplication d'un AVCaptureVideoPreviewLayer sur deux cellules distinctes.


2. Rendre manuellement SampleBuffer

Cette méthode, bien qu’un peu plus complexe, résout l’inconvénient susmentionné de CAReplicatorLayer. En rendant manuellement les aperçus en direct, vous pouvez générer autant de vues que vous le souhaitez. Certes, les performances peuvent être affectées.

Remarque: il existe peut-être d'autres méthodes pour rendre le SampleBuffer, mais j'ai choisi OpenGL pour ses performances. Le code a été inspiré et modifié depuis CIFunHouse .

Voici comment je l'ai implémenté:

2.1 Contextes et Session

Configurer le contexte OpenGL et 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]}];

File d'attente de distribution

Cette file d'attente sera utilisée pour la session et le délégué.

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

Lancez votre AVSession & AVCaptureVideoDataOutput

_ {Remarque: j'ai supprimé toutes les vérifications de capacité du périphérique pour le rendre plus lisible.}

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;
    :

Remarque: le code suivant est le "code magique". C'est à cet endroit que nous créons et ajoutons un DataOutput à AVSession afin que nous puissions intercepter les images de la caméra à l'aide du délégué. C'est la percée qu'il me fallait pour comprendre comment résoudre le problème.

    :
    // 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.2 Vues OpenGL

Nous utilisons GLKView pour rendre nos aperçus en direct. Donc, si vous voulez 4 aperçus en direct, alors vous avez besoin de 4 GLKView.

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

Parce que l'image vidéo native de la caméra de recul est dans UIDeviceOrientationLandscapeLeft (le bouton d'accueil est à droite), nous devons appliquer une transformation à 90 degrés dans le sens des aiguilles d'une montre pour pouvoir tracer l'aperçu vidéo comme si nous étions dans une vue orientée paysage. ; Si vous utilisez la caméra frontale et que vous souhaitez créer un aperçu en miroir (afin que l'utilisateur se voit dans le miroir), vous devez appliquer un basculement horizontal supplémentaire (en concaténant CGAffineTransformMakeScale (-1.0, 1.0) à la rotation. transformer)

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

Liez le tampon d’image pour obtenir la largeur et la hauteur du tampon d’image. Les limites utilisées par CIContext lors de l'affichage de GLKView sont en pixels (et non en points), d'où la nécessité de lire la largeur et la hauteur du tampon de trame.

[self.livePreviewView bindDrawable];

De plus, comme nous allons accéder aux limites dans une autre file d'attente (_captureSessionQueue), nous souhaitons obtenir cette information afin de ne pas accéder aux propriétés de _videoPreviewView à partir d'un autre thread/file d'attente.

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

Remarque: si vous utilisez la caméra avant, vous pouvez inverser l'aperçu en direct horizontalement comme ceci:

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

2.3 Mise en œuvre déléguée

Une fois les contextes, les sessions et les vues GLK configurés, vous pouvez désormais afficher le rendu de nos vues à partir de la méthode 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;

Vous aurez besoin d'une référence à chaque GLKView et à son vidéoPreviewViewBounds. Pour plus de facilité, je supposerai qu'ils sont tous deux contenus dans un UICollectionViewCell. Vous devrez modifier cela pour votre propre cas d'utilisation.

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

Cette solution vous permet d’avoir autant d’aperçus en direct que vous le souhaitez, à l’aide d’OpenGL, pour restituer le tampon des images reçues à partir de AVCaptureVideoDataOutputSampleBufferDelegate.

3. Exemple de code

Voici un projet de github que j'ai jeté avec les deux soultions: https://github.com/JohnnySlagle/Multiple-Camera-Feeds

55
Johnny

implémenter la méthode AVCaptureSession delegate qui est

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

en utilisant ceci, vous pouvez obtenir la sortie de tampon échantillon de chaque image vidéo. En utilisant la sortie tampon, vous pouvez créer une image en utilisant la méthode ci-dessous.

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

afin que vous puissiez ajouter plusieurs imageViews à votre vue et ajouter ces lignes à l'intérieur de la méthode de délégué que j'ai déjà mentionnée:

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

Définissez simplement le contenu de la couche d'aperçu sur une autre couche CALayer:

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

Vous pouvez le faire avec le contenu de n'importe quelle couche Metal ou OpenGL. Il n'y avait pas non plus d'augmentation de l'utilisation de la mémoire ou de la charge du processeur. Vous ne dupliquez rien d'autre qu'un minuscule pointeur. Ce n'est pas le cas avec ces autres "solutions".

J'ai un exemple de projet que vous pouvez télécharger, qui affiche 20 calques d'aperçu en même temps à partir d'un seul flux de caméra. Chaque couche a un effet différent appliqué à notre.

Vous pouvez regarder une vidéo de l'application en cours d'exécution, ainsi que télécharger le code source à l'adresse:

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

1
James Bush