Ответ 1
Я столкнулся с той же проблемой, что и одновременное отображение нескольких живых просмотров. Ответ использования 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
_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 Views
Мы используем 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];
Связать буфер кадра, чтобы получить ширину и высоту буфера кадра. Границы, используемые 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 Выполнение делегирования
После того, как мы установили контексты, сеансы и 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