具有多个预览的AVCaptureSession

我有一个AVCaptureSession与AVCaptureVideoPreviewLayer运行。

我可以看到video,所以我知道它的工作。

但是,我想要一个集合视图,并在每个单元格中添加一个预览图层,以便每个单元格显示video的预览。

如果我尝试将预览图层传递到单元格中并将其添加为子图层,则会将该图层从其他单元格中移除,因此一次只能显示在一个单元格中。

是否有另一种(更好)的方式来做到这一点?

我遇到了同时需要显示多个实时视图的问题。 上面使用UIImage的答案对于我所需要的太慢了。 以下是我find的两个解决scheme:

CAReplicatorLayer

第一个选项是使用CAReplicatorLayer自动复制图层。 正如文档所说,它会自动创build“…其子层(源图层)的指定数量的副本,每个副本可能会应用几何,时间和颜色转换。

如果除了简单的几何或颜色转换(Think Photo Booth)之外,没有太多与实况预览的交互,这是非常有用的。 我经常看到CAReplicatorLayer被用来创build“reflection”效果。

以下是一些复制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并设置属性

注意:这将复制实时预览图层四次

 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的一个缺点是它处理层复制的所有位置。 所以它会将任何设置的转换应用到每个实例,并且它将全部包含在其内部。 例如,在两个单独的单元格上不可能有AVCaptureVideoPreviewLayer的复制。

2.手动渲染SampleBuffer

这种方法虽然稍微复杂一些,但解决了上述的CAReplicatorLayer的缺点。 通过手动渲染实时预览,您可以渲染尽可能多的视图。 当然,性能可能会受到影响。

注意:可能还有其他方法来渲染SampleBuffer,但是我selectOpenGL是因为它的性能。 代码是从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

注意:我已经删除了所有设备function检查,以使其更具可读性。

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

注意:以下代码是“魔术代码”。 这是我们创build的地方,并将一个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; 

由于来自后置摄像头的原始video图像位于UIDeviceOrientationLandscapeLeft(即主页button位于右侧),因此我们需要应用顺时针90度变换,以便我们可以绘制video预览,就好像我们处于横向视图; 如果您使用的是前置摄像头,并且您希望进行镜像预览(以便用户看到自己在镜像中),则需要应用额外的水平翻转(通过将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]; } } 

这个解决scheme可以让您拥有尽可能多的实时预览,您可以使用OpenGL渲染从AVCaptureVideoDataOutputSampleBufferDelegate接收到的图像的缓冲区。

3.示例代码

这是一个github项目,我把这两个soultions一起扔: https : //github.com/JohnnySlagle/Multiple-Camera-Feeds

实现这个AVCaptureSession委托方法

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

使用这个你可以得到每个video帧的采样缓冲区输出。 使用缓冲区输出,您可以使用下面的方法创build一个图像。

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

所以你可以添加几个imageViews到你的视图,并在我之前提到的委托方法中添加这些行:

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

你不能有多个预览。 Apple AVFundation说只有一个输出stream。 我尝试了很多方法,但是你却做不到。