標籤:

GIF/MOV/Live Photo

我之前寫過一篇介紹 GIF 的文章:zhuanlan.zhihu.com/p/22

這次這篇文章來談談 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;n- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;n

這兩個代理方法得到圖片和時間的數組,接下來就是把它通過 AVFoundation 框架寫到一個 MOV 文件裡面去。有這麼幾步:創建 AVAssetWriter 和 AVAssetWriterInput,然後使用 WriterInput 的這個方法:

- (void)requestMediaDataWhenReadyOnQueue:(dispatch_queue_t)queue usingBlock:(void (^)(void))block;n

在這個 queue 中遍歷 decode 出來的 image 列表,將 image 轉換成 CVPixelBufferRef,並將 durations 裡面的時間間隔轉換成 CMTime,然後寫進去:

[self.writerInput requestMediaDataWhenReadyOnQueue:mediaInputQueue usingBlock:^{n n CMTime presentationTime = kCMTimeZero;n while (YES) {n n if (index >= frameCount) {n break;n }n n if ([self.writerInput isReadyForMoreMediaData]) {n @autoreleasepool {n UIImage *image = images[index];n CVPixelBufferRef buffer = [self newPixelBufferFromCGImage:image.CGImage];n if (buffer) {n double scale = averageScale;n if (index < durations.count) {n scale = 1.0 / [durations[index] doubleValue];n }n [self.bufferAdapter appendPixelBuffer:buffer withPresentationTime:presentationTime];n presentationTime = CMTimeAdd(presentationTime, CMTimeMake(1, scale));n CVBufferRelease(buffer);n }n index++;n }n }n } n n [self.writerInput markAsFinished];n [self.assetWriter finishWritingWithCompletionHandler:^{n dispatch_async(dispatch_get_main_queue(), ^{n self.completionBlock(self.fileURL);n });n }];n n CVPixelBufferPoolRelease(self.bufferAdapter.pixelBufferPool);n}];n

這裡有個大坑,如果 GIF 文件的長寬不是 16 的整數倍,生成出來的 MOV 文件會有奇奇怪怪的問題,所以在創建 outputSettings 的時候,我使用了下面的方法:

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

將解析度轉換到與原解析度最接近的 16 的整數倍,最後從 fileURL 裡面取出生成的結果即可。

# MOV -> GIF

我在上次的文章裡面提到過,NSGIF 這個項目提供了一個視頻轉到 GIF 的例子,但是寫的不怎麼好,所以這裡大致講一下原理是什麼。最核心的概念有兩個:使用 AVAssetImageGenerator 取關鍵幀,以及使用 CGImageDestinationAddImage 等函數往 GIF 文件裡面添加幀。這裡有一點可以提一下,AVAssetImageGenerator 有兩個參數:requestedTimeToleranceBefore 和 requestedTimeToleranceAfter,這兩個參數如果都填 kCMTimeZero 的話取出來的幀會精確無比,但同時也會因此而降低性能。同時這個過程中,時間的轉換是 GIF -> MOV 的反過程,也即 CMTime 轉換成幀與幀的時間間隔:

NSMutableArray *timePoints = [NSMutableArray array];nfor (int currentFrame = 0; currentFrame<frameCount; ++currentFrame) {n float seconds = (float)increment * currentFrame + offset;n CMTime time = CMTimeMakeWithSeconds(seconds, [timeInterval intValue]);n [timePoints addObject:[NSValue valueWithCMTime:time]];n}n

通過 CGImageDestinationCreateWithURL 創建 GIF 文件,CGImageDestinationAddImage 添加關鍵幀,最後 CGImageDestinationSetPropertiesCGImageDestinationFinalize 來結束文件寫入。

# MOV -> Live Photo

這一步可以直接看 LivePhotoDemo 這個代碼,基本上看完了也就懂了整個過程(但其實需要對 Live Photo 格式有所了解),這個代碼是適用於 iOS 9.1 及以上的系統的,否則即便可以創建也沒有 PHLivePhoto 這樣的類來提供應用內顯示的邏輯。簡單說就是需要創建兩個文件,一個 JPG 一個 MOV,但是兩個文件都寫進去了一些元數據,比如說創建 JPG 的時候 CGImageDestinationAddImageFromSource,這裡面居然鬼使神差的要寫入一個這樣的元數據:

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

這個東西其實是來自於作者對 Live Photo 文件的 EXIF 信息觀察。我嘗試用自己寫的 app 看了一下任何一個 Live Photo 的 EXIFF:

果不其然這裡面有個 17 對應過去是一個能夠用來找到 MOV 文件的 UUID 信息。

至於 MOV 文件,元數據就更多更複雜,有一些類似於 com.apple.quicktime.still-image-time 這樣的 metadata 在裡面,StackOverflow 上面能找到一些描述:stackoverflow.com/quest

之後我再對 Live Photo 的 MOV 文件進行一個格式的分析。

創建好這兩個文件之後,使用 Photos Framework 可以把他們寫到相冊,使用 PhotosUI Framework PHLivePhotoView 可以展示和播放 Live Photo,從而完成了整個過程:

[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{n PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAsset];n PHAssetResourceCreationOptions *options = [[PHAssetResourceCreationOptions alloc] init];n [request addResourceWithType:PHAssetResourceTypePairedVideo fileURL:[NSURL fileURLWithPath:outputMOVPath] options:options];n [request addResourceWithType:PHAssetResourceTypePhoto fileURL:[NSURL fileURLWithPath:outputJPEGPath] options:options];n} completionHandler:^(BOOL success, NSError * _Nullable error) {n if (finishBlock) {n finishBlock(success, error);n }n}];n

# Live Photo -> MOV

根據上面的內容我們已經知道,Live Photo 不用轉換到 MOV,它本身就包含一個 MOV,所以我們只要把它取出來,可以有兩個方法做這個事情:

合法的方案,使用 PHAssetResourceManager

[[PHAssetResourceManager defaultManager] writeDataForAssetResource:resource toFile:[NSURL fileURLWithPath:@""] options:nil completionHandler:^(NSError * _Nullable error) {n}];n

這個方法會把 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 -

參考項目:

  1. github.com/BradLarson/G

  2. github.com/ibireme/YYIm

  3. github.com/genadyo/Live

推薦閱讀:

MG動畫精英計劃第一周第二彈
盧中南|歐體楷書基本筆畫gif動態圖
除了PS,你可以在這些地方找到GIF圖
【PS教程】1.5校園平面設計實用案例-動圖!

TAG:iOS | iOS开发 | GIF |