AVCaptureSession с несколькими превью

У меня AVCaptureSession работает с AVCaptureVideoPreviewLayer.

Я вижу видео, поэтому знаю, что оно работает.

Тем не менее, я хотел бы иметь представление коллекции и в каждой ячейке добавить слой предварительного просмотра, чтобы каждая ячейка отображала предварительный просмотр видео.

Если я попытаюсь передать слой предварительного просмотра в ячейку и добавить его в качестве подслоя, он удалит этот слой из других ячеек, чтобы он отображался только в одной ячейке за раз.

Есть ли другой (лучший) способ сделать это?

4 ответа

Решение

Я столкнулся с той же проблемой необходимости одновременного отображения нескольких живых представлений. Ответ использования UIImage выше был слишком медленным для того, что мне было нужно. Вот два решения, которые я нашел:

1. CAReplicatorLayer

Первый вариант - использовать CAReplicatorLayer для автоматического дублирования слоя. Как говорят в документах, он автоматически создаст "... указанное количество копий своих подслоев (исходного слоя), к каждой копии могут быть применены геометрические, временные и цветовые преобразования".

Это очень полезно, если не существует большого взаимодействия с предварительным просмотром в реальном времени, кроме простых геометрических или цветовых преобразований (Think Photo Booth). Я чаще всего видел, как CAReplicatorLayer используется для создания эффекта "отражения".

Вот пример кода для репликации 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)];

Инициируйте CAReplicatorLayer и установите свойства

Примечание. Это будет повторять слой предварительного просмотра четыре раза.

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

Downsides

Недостатком использования CAReplicatorLayer является то, что он обрабатывает все размещения репликаций слоев. Таким образом, он будет применять любые преобразования набора к каждому экземпляру, и все это будет содержаться внутри себя. Например, не было бы возможности иметь репликацию AVCaptureVideoPreviewLayer на две отдельные ячейки.


2. Рендеринг SampleBuffer вручную

Этот метод, хотя и немного более сложный, решает упомянутую выше обратную сторону CAReplicatorLayer. Рендеринг живых предварительных просмотров вручную позволяет отображать столько представлений, сколько вы хотите. Конечно, производительность может пострадать.

Примечание. Могут быть и другие способы визуализации SampleBuffer, но я выбрал OpenGL из-за его производительности. Код был вдохновлен и изменен с CIFunHouse.

Вот как я это реализовал:

2.1 Контексты и сессия

Настройка OpenGL и CoreImage Context

_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.2 OpenGL Просмотров

Мы используем GLKView для визуализации наших предварительных просмотров. Так что если вы хотите 4 предварительных просмотра, вам нужно 4 GLKView.

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

Поскольку исходное видеоизображение с задней камеры находится в UIDeviceOrientationLandscapeLeft (т. Е. Кнопка home находится справа), нам нужно применить преобразование по часовой стрелке на 90 градусов, чтобы мы могли нарисовать предварительный просмотр видео, как если бы мы были в альбомно-ориентированном виде; если вы используете фронтальную камеру и хотите иметь зеркальный предварительный просмотр (чтобы пользователь видел себя в зеркале), вам нужно применить дополнительный горизонтальный переворот (путем конкатенации CGAffineTransformMakeScale(-1.0, 1.0) к повороту преобразование)

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

Привязать буфер кадров, чтобы получить ширину и высоту кадрового буфера. Границы, используемые CIContext при рисовании в GLKView, указаны в пикселях (а не в точках), поэтому необходимо считывать ширину и высоту буфера кадра.

[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 Делегирование реализации

После того как у нас настроены Контексты, Сеансы и GLKView, мы можем теперь визуализировать наши представления из метода 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

Реализовать метод делегата 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);
}

так что вы можете добавить несколько изображений в ваш вид и добавить эти строки в методе делегата, о котором я упоминал ранее:

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

Просто установите содержимое слоя предварительного просмотра на другой CALayer:

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

Вы можете сделать это с содержимым любого слоя Metal или OpenGL. Не было никакого увеличения использования памяти или загрузки ЦП с моей стороны, либо. Вы не дублируете ничего, кроме крошечного указателя. Это не так с этими другими "решениями".

У меня есть пример проекта, который вы можете скачать, который отображает 20 слоев предварительного просмотра одновременно с одного канала камеры. Каждый слой имеет свой эффект, применяемый к нашему.

Вы можете посмотреть видео работы приложения, а также скачать исходный код по адресу:

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

Работая в Swift 5 на iOS 13, я реализовал несколько более простую версию ответа @Ushan87. В целях тестирования я перетащил новый маленький UIImageView поверх моего существующего AVCaptureVideoPreviewLayer. В 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
    }

Для моих целей спектакль был прекрасен. В новом ракурсе задержек не заметил.

Вы не можете иметь несколько предварительных просмотров. Только один выходной поток, как говорит Apple AVFundation. Я пробовал много способов, но ты просто не можешь.

Другие вопросы по тегам