苹果在 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截取画面是很容易的。但是这样得到的画面,分辨率和当前视图的 是一致的,如果要保存高分辨率就得缩放,这样肯定会模糊。所以这个方法最先排除。
再来看看ARSCNView这个类,它的直接父类是SCNView。前面提到SceneKit是苹果自带的游戏框架,这个框架里面或许有API能直接获取。查找了相关资料,确实发现SCNRenderer有个snapshotAtTime:withSize:antialiasingMode:方法可以截取UIImage,而SCNRenderer所需要的场景scene属性可以从ARSCNView中直接获取。既然可以得到UIImage,就可以转换为CVPixelBufferRel扔给AVFoundation框架处理。
(笔者目前只找到了这个方法,如果有更好的方法,请不吝赐教)

使用AVAssetWriter输出视频

得到每一帧数据,下面的工作就需要AVFoudation完成了。ARSessionDelegate有个session:didUpdate :回调会在每次识别完成后调用,我们可以在这里控制截取新的图像帧数据与保存逻辑。
首先,我们需要在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] initWith :CGRectMake(bounds.size.width/2-60, bounds.size.height - 200, 120, 100)];    [button set :@"tap to record" forState:UIControlStateNormal];    [button set :@"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:didUpdate :回调方法中,控制录制逻辑:

  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);    CVPixelBufferLock Address(pixelBuffer, 0);    void *data  = CVPixelBufferGet Address(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);    CVPixelBufferUnlock Address(pixelBuffer, 0);    CGContextRelease(context);    return pixelBuffer;}

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

Demo地址

收藏 打印