AVCaptureVideoPreviewLayerで実行されているAVCaptureSessionがあります。
私はビデオを見ることができるので、それが機能していることがわかります。
ただし、コレクションビューを作成し、各セルにプレビューレイヤーを追加して、各セルにビデオのプレビューが表示されるようにします。
プレビューレイヤーをセルに渡してサブレイヤーとして追加しようとすると、他のセルからレイヤーが削除されるため、一度に1つのセルにしか表示されません。
これを行う別の(より良い)方法はありますか?
複数のライブビューを同時に表示する必要があるという同じ問題に遭遇しました。上記のUIImageを使用するという答えは、私が必要としていたものには遅すぎました。これが私が見つけた2つの解決策です:
最初のオプションは、CAReplicatorLayerを使用してレイヤーを自動的に複製することです。ドキュメントに記載されているように、「...サブレイヤー(ソースレイヤー)の指定された数のコピーが自動的に作成されます。各コピーには、幾何学的、時間的、色の変換が適用される可能性があります。」
これは、単純な幾何学的変換または色変換以外にライブプレビューとのやり取りがあまりない場合に非常に便利です(フォトブースを考えてください)。私は、「反射」効果を作成する方法として使用されるCAReplicatorLayerを最も頻繁に見ました。
CACaptureVideoPreviewLayerを複製するためのサンプルコードを次に示します。
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)];
注:これにより、ライブプレビューレイヤーが4回複製されます。
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を使用することの欠点は、レイヤーレプリケーションのすべての配置を処理することです。したがって、設定された変換を各インスタンスに適用し、すべてがそれ自体に含まれます。 例: 2つの別々のセルにAVCaptureVideoPreviewLayerを複製する方法はありません。
この方法は、少し複雑ですが、CAReplicatorLayerの上記の欠点を解決します。ライブプレビューを手動でレンダリングすることにより、必要な数のビューをレンダリングできます。確かに、パフォーマンスが影響を受ける可能性があります。
注:SampleBufferをレンダリングする方法は他にもあるかもしれませんが、パフォーマンスのためにOpenGLを選択しました。コードは CIFunHouse 。からインスピレーションを受けて変更されました
これが私がそれを実装した方法です:
_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);
注:これを読みやすくするために、すべてのデバイス機能チェックを削除しました。
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];
});
ライブプレビューのレンダリングには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];
フレームバッファをバインドして、フレームバッファの幅と高さを取得します。 GLKViewに描画するときにCIContextによって使用される境界はピクセル単位(ポイントではない)であるため、フレームバッファーの幅と高さから読み取る必要があります。
[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));
Contexts、Sessions、および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から受信した画像のバッファーをレンダリングするために必要な数のライブプレビューを作成できます。
これが私が両方の解決策と一緒に投げた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);
}
したがって、ビューにいくつかのimageViewを追加し、前述のデリゲートメソッド内にこれらの行を追加できます。
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レイヤーのコンテンツで実行できます。私の側でも、メモリ使用量やCPU負荷の増加はありませんでした。小さなポインタ以外は何も複製していません。これらの他の「ソリューション」ではそうではありません。
1つのカメラフィードから同時に20のプレビューレイヤーを表示するダウンロード可能なサンプルプロジェクトがあります。各レイヤーには、異なる効果が適用されます。
実行中のアプリのビデオを視聴したり、次の場所からソースコードをダウンロードしたりできます。
https://demonicactivity.blogspot.com/2017/05/developer-iphone-video-camera-wall.html?m=1
IOS13のSwift 5)で作業し、@ Ushan87による回答のやや単純なバージョンを実装しました。テスト目的で、既存のAVCaptureVideoPreviewLayerの上に新しい小さなUIImageViewをドラッグしました。そのウィンドウの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
}
私の目的では、パフォーマンスは良好でした。新しい見方では、遅れは見られませんでした。