保存ARKit预览画面输出AR视频

苹果在 WWDC2017 中推出了 ARKit,新的AR框架给应用开发带来了更多可能性。值得注意的是iOS11正式版刚更新不久,就能看到市面上已经有了很多AR相关的应用。这些应用大多集中在游戏,短视频,工具应用中,比如最近很火的抖音就更新了AR相关的新玩法。可以预见,未来AR视频会是视频领域的又一个热点。
苹果原生的API做的非常完善,新的ARKit和AVFoundation,SceneKit等框架的结合也非常紧密,AR视频的保存也不难实现。

生成模板工程

首先,打开Xcode9创建新项目,选择Augmented Reality App,下一步Content Technology选择默认的SceneKit。SceneKit是苹果自带的3D游戏开发框架,用于完成渲染。
生成项目后,直接运行,可以看到一架飞机出现在了屏幕中。
关于ARKit框架,这里先不深入介绍,笔者也在学习摸索中。我们的任务是保存AR视频,所以接下来就直接在模板工程里面修改吧。

获取当前渲染的图像帧数据

要想保存视频,最重要一点就是得到当前渲染好的帧数据。得到帧数据后,下面的工作交给AVFoundation就可以轻松搞定了。
那么,如何得到当前画面的帧数据呢?
可以看到渲染的视图ARSCNView最终是继承自UIView,从UIView截取画面是很容易的。但是这样得到的画面,分辨率和当前视图的frame是一致的,如果要保存高分辨率就得缩放,这样肯定会模糊。所以这个方法最先排除。
再来看看ARSCNView这个类,它的直接父类是SCNView。前面提到SceneKit是苹果自带的游戏框架,这个框架里面或许有API能直接获取。查找了相关资料,确实发现SCNRenderer有个snapshotAtTime:withSize:antialiasingMode:方法可以截取UIImage,而SCNRenderer所需要的场景scene属性可以从ARSCNView中直接获取。既然可以得到UIImage,就可以转换为CVPixelBufferRel扔给AVFoundation框架处理。
(笔者目前只找到了这个方法,如果有更好的方法,请不吝赐教)

使用AVAssetWriter输出视频

得到每一帧数据,下面的工作就需要AVFoudation完成了。ARSessionDelegate有个session:didUpdateFrame:回调会在每次识别完成后调用,我们可以在这里控制截取新的图像帧数据与保存逻辑。
首先,我们需要在ViewController中添加需要的属性:

@property (nonatomic, strong) AVAssetWriter *writer;
@property (nonatomic, strong) AVAssetWriterInput *videoInput;
@property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor;

@property (nonatomic, strong) SCNRenderer *renderer;
@property (nonatomic, assign) WZRecordStatus status;
@property (nonatomic, strong) dispatch_queue_t videoQueue;
@property (nonatomic, copy) NSString *outputPath;
@property (nonatomic, assign) CGSize outputSize;
@property (nonatomic, assign) int count;

具体每个属性的作用,后面看代码就知晓了。需要注意的是,我们需要一个状态控制录制状态:

typedef NS_ENUM(NSInteger, WZRecordStatus)  {
    WZRecordStatusIdle,
    WZRecordStatusRecording,
    WZRecordStatusFinish,
};

viewDidLoad中,初始化相关资源:

// 设置代理
    self.sceneView.session.delegate = self;
    // 添加一个录制按钮
    CGRect bounds = [UIScreen mainScreen].bounds;
    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(bounds.size.width/2-60, bounds.size.height - 200, 120, 100)];
    [button setTitle:@"tap to record" forState:UIControlStateNormal];
    [button setTitle:@"recording" forState:UIControlStateSelected];
    [button addTarget:self action:@selector(clicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    // 创建SCNRenderer
    self.renderer = [SCNRenderer rendererWithDevice:nil options:nil];
    // 将sceneView的sceneView传给renderer
    self.renderer.scene = scene;
    // 创建图像处理队列
    self.videoQueue = dispatch_queue_create("com.worthy.video.queue", NULL);
    // 设置输出分辨率
    self.outputSize = CGSizeMake(720, 1280);

添加按钮的响应方法:

-(void)clicked:(UIButton *)sender {
    sender.selected = !sender.selected;
    if (sender.selected) {
        // 开始录制
        [self startRecording];
    }else {
        // 结束录制
        [self finishRecording];
    }
}

调用开始录制,需要创建和配置AVAssetWriter相关资源并设置状态。结束录制只需要设置状态:

- (void)setupWriter {
    // 设置输出路径
    self.outputPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"out.mp4"];
    [[NSFileManager defaultManager] removeItemAtPath:self.outputPath error:nil];
    // 创建AVAssetWriter
    self.writer = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:self.outputPath] fileType:AVFileTypeQuickTimeMovie error:nil];
    // 创建AVAssetWriterInput
    self.videoInput = [[AVAssetWriterInput alloc]
                       initWithMediaType:AVMediaTypeVideo outputSettings:
                       @{AVVideoCodecKey:AVVideoCodecTypeH264,
                         AVVideoWidthKey: @(self.outputSize.width),
                         AVVideoHeightKey: @(self.outputSize.height)}];
    [self.writer addInput:self.videoInput];
    // 创建AVAssetWriterInputPixelBufferAdaptor
    self.pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] initWithAssetWriterInput:self.videoInput sourcePixelBufferAttributes:
                               @{(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA),
                                 (id)kCVPixelBufferWidthKey:@(self.outputSize.width),
                                 (id)kCVPixelBufferHeightKey:@(self.outputSize.height)}];
}

- (void)startRecording {
    [self setupWriter];
    [self.writer startWriting];
    [self.writer startSessionAtSourceTime:kCMTimeZero];
    self.status = WZRecordStatusRecording;
}

- (void)finishRecording {
    self.status = WZRecordStatusFinish;
}

最后,我们在session:didUpdateFrame:回调方法中,控制录制逻辑:

  if (self.status == WZRecordStatusRecording) {
        dispatch_async(self.videoQueue, ^{
            @autoreleasepool {
                // 渲染一秒钟60次,视频帧只需要一秒钟30次
                // 这里帧率60是写死的,更好的实践是获取当前渲染帧率再后做计算
                if (self.count % 2 == 0) {
                    // 获取当前渲染帧数据
                    CVPixelBufferRef pixelBuffer = [self capturePixelBuffer];
                    if (pixelBuffer) {
                        @try {
                            // 添加到录制源
                            [self.pixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:CMTimeMake(self.count/2*1000, 30*1000)];
                        }@catch (NSException *exception) {
                            NSLog(@"%@",exception.reason);
                        }@finally {
                            CFRelease(pixelBuffer);
                        }
                    }
                }
                self.count++;
            }
        });
    }else if (self.status == WZRecordStatusFinish) {
        // 完成录制
        self.status = WZRecordStatusIdle;
        self.count = 0;
        [self.videoInput markAsFinished];
        [self.writer finishWritingWithCompletionHandler:^{
            UISaveVideoAtPathToSavedPhotosAlbum(self.outputPath, nil, nil, nil);
            NSLog(@"record finish, saved to alblum.");
        }];
    }

获取帧数据方法如下:

-(CVPixelBufferRef)capturePixelBuffer {
     UIImage *image = [self.renderer snapshotAtTime:1 withSize:CGSizeMake(self.outputSize.width, self.outputSize.height) antialiasingMode:SCNAntialiasingModeMultisampling4X];
    CVPixelBufferRef pixelBuffer = NULL;
    CVPixelBufferPoolCreatePixelBuffer(NULL, [self.pixelBufferAdaptor pixelBufferPool], &pixelBuffer);
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    void *data  = CVPixelBufferGetBaseAddress(pixelBuffer);
    CGContextRef context = CGBitmapContextCreate(data, self.outputSize.width, self.outputSize.height, 8, CVPixelBufferGetBytesPerRow(pixelBuffer), CGColorSpaceCreateDeviceRGB(),  kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    CGContextDrawImage(context, CGRectMake(0, 0, self.outputSize.width, self.outputSize.height), image.CGImage);
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    CGContextRelease(context);
    return pixelBuffer;
}

现在,我们还差一步可以连真机调试了,需要在Info.plist中添加NSCameraUsageDescription描述。这样点击按钮开始录制,再次点击结束录制,结果就可以保存在相册中。

Demo地址

你可能感兴趣的:(保存ARKit预览画面输出AR视频)