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. Я пробовал много способов, но ты просто не можешь.