苹果在 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描述。这样点击按钮开始录制,再次点击结束录制,结果就可以保存在相册中。
继续阅读与本文标签相同的文章
黎明前的黑夜静悄悄
无穷小微积分理论的“根”扎的有多深?
-
为什么它有典型FaaS能力,却是非典型FaaS架构? | 开发者必读(065期)
2026-05-18栏目: 教程
-
Mybatis执行SQL的4大基础组件详解
2026-05-18栏目: 教程
-
Java描述设计模式(08):桥接模式
2026-05-18栏目: 教程
-
Java描述设计模式(09):装饰模式
2026-05-18栏目: 教程
-
Java描述设计模式(10):组合模式
2026-05-18栏目: 教程
