2018-08-15 如何在iPhone6s以下设备上使用LivePhoto

转载:https://blog.csdn.net/hmh007/article/details/54408764

事情的经过是这样的:昨晚,一个朋友问我怎么越狱,我说越狱干嘛,他说他想用LivePhoto功能,但是他的手机是iPhone6,没有这个功能,说是越狱后装一个插件就可以。听完后,我说最好不要越狱,虽然可以用一些插件,但是手机里面信息的安全性非常差,说不定别人插件后台开个什么线程你也不知道,就好像你家里装了门,你还在门上开个洞,谁想来都可以。做为一名iOS程序猿,我觉得苹果如今开源性很强,很多功能都是可以实现的。于是,我很装逼的说, 我说你等着,明天上班我给你做一个。

今早一到办公室就开始查资料,说到LivePhoto就不得不提MOV,这两个都是苹果开发的格式,LivePhoto其实就是一个JPG加上一个MOV,只不过这个MOV里面写入了一些元数据,能让相册正确的识别。对应iOS中的框架是ImageIO/MobileCoreServices/Photos/AVFoundation 等。

下面附上资料,文中有很详细的解释,也有Demo地址,相信大家仔细看下就会明白的,毕竟,Talk is cheap, show me the code~

GIF/MOV/Live Photo

这次这篇文章来谈谈GIF/MOV/Live Photo两两之间的格式转换,这个需求来自于我最近在做的一个小项目,里面提供了类似的功能。

这三个格式有一个共同点,他们都可以由一个连续的图片序列得到。其中 MOV 和 Live Photo 都是苹果开发的格式,更进一步的讲 Live Photo 其实是一个 JPG 加上一个 MOV,只不过这个 MOV 里面写入了一些元数据,能让相册正确的识别。本文涉及到的核心框架有:ImageIO/MobileCoreServices/Photos/AVFoundation等。

# GIF -> MOV

顾名思义,将 GIF 文件 decode 成 image array 和 duration array 的过程,通过ImageIO Framework去做,当然我这里还是会推荐 ibireme 的YYImage,他已经做了大部分的工作。我们在显示了 imageView 之余可以使用:

-(UIImage*)animatedImageFrameAtIndex:(NSUInteger)index;-(NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;

这两个代理方法得到图片和时间的数组,接下来就是把它通过 AVFoundation 框架写到一个 MOV 文件里面去。有这么几步:创建 AVAssetWriter 和 AVAssetWriterInput,然后使用 WriterInput 的这个方法:

-(void)requestMediaDataWhenReadyOnQueue:(dispatch_queue_t)queueusingBlock:(void(^)(void))block;

在这个 queue 中遍历 decode 出来的 image 列表,将 image 转换成 CVPixelBufferRef,并将 durations 里面的时间间隔转换成 CMTime,然后写进去:

[self.writerInputrequestMediaDataWhenReadyOnQueue:mediaInputQueueusingBlock:^{CMTimepresentationTime=kCMTimeZero;while(YES){if(index>=frameCount){break;}if([self.writerInputisReadyForMoreMediaData]){@autoreleasepool{UIImage*image=images[index];CVPixelBufferRefbuffer=[selfnewPixelBufferFromCGImage:image.CGImage];if(buffer){doublescale=averageScale;if(index

这里有个大坑,如果 GIF 文件的长宽不是16的整数倍,生成出来的 MOV 文件会有奇奇怪怪的问题,所以在创建 outputSettings 的时候,我使用了下面的方法:

+(NSDictionary*)videoSettingsWithCodec:(NSString*)codecwidth:(CGFloat)widthheight:(CGFloat)height{intw=(int)((int)(width/16.0)*16);inth=(int)(height*w/width);NSDictionary*videoSettings=@{AVVideoCodecKey:codec,AVVideoWidthKey:@(w),AVVideoHeightKey:@(h)};returnvideoSettings;}

将分辨率转换到与原分辨率最接近的 16 的整数倍,最后从 fileURL 里面取出生成的结果即可。

# MOV -> GIF

我在上次的文章里面提到过,NSGIF 这个项目提供了一个视频转到 GIF 的例子,但是写的不怎么好,所以这里大致讲一下原理是什么。最核心的概念有两个:使用AVAssetImageGenerator取关键帧,以及使用CGImageDestinationAddImage 等函数往 GIF 文件里面添加帧。这里有一点可以提一下,AVAssetImageGenerator 有两个参数:requestedTimeToleranceBefore 和 requestedTimeToleranceAfter,这两个参数如果都填 kCMTimeZero 的话取出来的帧会精确无比,但同时也会因此而降低性能。同时这个过程中,时间的转换是 GIF -> MOV 的反过程,也即 CMTime 转换成帧与帧的时间间隔:

NSMutableArray*timePoints=[NSMutableArrayarray];for(intcurrentFrame=0;currentFrame

通过CGImageDestinationCreateWithURL创建 GIF 文件,CGImageDestinationAddImage添加关键帧,最后CGImageDestinationSetProperties和CGImageDestinationFinalize来结束文件写入。

# MOV -> Live Photo

这一步可以直接看LivePhotoDemo这个代码,基本上看完了也就懂了整个过程(但其实需要对 Live Photo 格式有所了解),这个代码是适用于iOS 9.1及以上的系统的,否则即便可以创建也没有PHLivePhoto这样的类来提供应用内显示的逻辑。简单说就是需要创建两个文件,一个 JPG 一个 MOV,但是两个文件都写进去了一些元数据,比如说创建 JPG 的时候 CGImageDestinationAddImageFromSource,这里面居然鬼使神差的要写入一个这样的元数据:

metadata[(id)kCGImagePropertyMakerAppleDictionary]=@{@"17":@"UUID"};

这个东西其实是来自于作者对 Live Photo 文件的 EXIF 信息观察。我尝试用自己写的 app 看了一下任何一个 Live Photo 的 EXIFF:

果不其然这里面有个 17 对应过去是一个能够用来找到 MOV 文件的 UUID 信息。

至于 MOV 文件,元数据就更多更复杂,有一些类似于com.apple.quicktime.still-image-time这样的 metadata 在里面,StackOverflow 上面能找到一些描述:http://stackoverflow.com/questions/32508375/apple-live-photo-file-format

之后我再对 Live Photo 的 MOV 文件进行一个格式的分析。

创建好这两个文件之后,使用Photos Framework 可以把他们写到相册,使用PhotosUI Framework 的PHLivePhotoView可以展示和播放 Live Photo,从而完成了整个过程:

[[PHPhotoLibrarysharedPhotoLibrary]performChanges:^{PHAssetCreationRequest*request=[PHAssetCreationRequestcreationRequestForAsset];PHAssetResourceCreationOptions*options=[[PHAssetResourceCreationOptionsalloc]init];[requestaddResourceWithType:PHAssetResourceTypePairedVideofileURL:[NSURLfileURLWithPath:outputMOVPath]options:options];[requestaddResourceWithType:PHAssetResourceTypePhotofileURL:[NSURLfileURLWithPath:outputJPEGPath]options:options];}completionHandler:^(BOOLsuccess,NSError*_Nullableerror){if(finishBlock){finishBlock(success,error);}}];

# Live Photo -> MOV

根据上面的内容我们已经知道,Live Photo 不用转换到 MOV,它本身就包含一个 MOV,所以我们只要把它取出来,可以有两个方法做这个事情:

合法的方案,使用PHAssetResourceManager:

[[PHAssetResourceManagerdefaultManager]writeDataForAssetResource:resourcetoFile:[NSURLfileURLWithPath:@""]options:nilcompletionHandler:^(NSError*_Nullableerror){}];

这个方法会把 Asset 写到你指定的路径,然后通过路径取出来即可。另外一个方案不太合法,PHAsset 有个叫做fileURLForVideoComplementFile的方法,这是个私有的,他可以直接取到上述描述中的 MOV 文件的 URL,甚至还有另外一个叫做fileURLForVideoPreviewFile能取到预览文件的 URL。至于怎么用的话,我还是不说了罢,反正都有合法的方法了。

# GIF <-> Live Photo

其实 Live Photo 到 GIF 已经不用讲了,通过上述方法拿到 MOV 再跑 MOV -> GIF 的流程即可。

理论上,GIF -> Live Photo 的过程也不太用讲,最笨的方法可以通过GIF -> MOV -> Live Photo来实现,这是一定可行的。但是这样有个缺陷就是性能,因为这个过程中相当于做了两次写入视频文件的操作。

我尝试过直接将 GIF decode 之后去写 Live Photo 的视频文件,遗憾的是没有成功,根本原因还是因为 GIF 转视频时候使用的 buffer 和 MOV 转 Live Photo 时候的 buffer 是不同的,是一个 pixel buffer,貌似没办法插入符合 Live Photo 的 metadata,这个我不是特别确定,尚存疑。目前我才用的方法就是做了两次转换,一个正常 5s 以内的视频在 iPhone 6s Plus 上面大概在 2s 以内,也算可以接受。

# 后续的点

后面有两点想做的,首先想完全搞清楚 Live Photo 的文件格式,搞清楚那些 metadata 如何得到的。另外就是想要优化一下 GIF 转换到 Live Photo 的性能,看能不能一步到位。

今天的文章很长,感谢看到这里的朋友,下次再见。

- EOF -

参考项目:

https://github.com/BradLarson/GPUImage

https://github.com/ibireme/YYImage

https://github.com/genadyo/LivePhotoDemo

你可能感兴趣的:(2018-08-15 如何在iPhone6s以下设备上使用LivePhoto)