iOS 使用图片制作视频(一)

因为近期项目需要实现类似iOS 10相册中的回忆功能:利用照片或图片合成制作视频的功能。
这篇文章的目的是做一个简单的分享,如何利用CALayer、CAAnimation和视频合成的相关知识点来完成这一功能的开发。
设计思路:


流程.png

流程上首先我们需要获取系统相册里的照片和视频,选择好后经过一些列处理将他们合成为一个视频,我们还需要对原视频进行逐帧分解,为什么这样做我会再后面进行解答。
省略掉从系统相册获取视频和图片的部分,我们分别从处理图片和视频开始。
我需要做的是将图片合成视频并能够和系统相册中的视频再合并成一个视频,并能够在预览的时候有一个很好的交互。我在这里考虑使用CALayer和CAAnimation。通过给一个view的layer添加Animation实现内容切换大小改变甚至是淡入淡出的渐变效果。

CALayer

CALayer是一个管理基于图像的内容的对象,它可以使我们在该内容上执行动画的对象。图层的主要工作是管理我们的视觉内容,但图层本身具有可设置的视觉属性,例如背景颜色,边框和阴影。除了管理可视内容之外,该层还可以用于维护在屏幕上呈现的几何信息(例如其位置,大小和变换)。一个图层对象包含了持续时间和它的走向,它基于支持CAMediaTiming协议的动画,定义了这个图层的时间信息。

CAAnimation

CAAnimation 是核心动画的抽象超类。它为CAMediaTiming和CAAction协议提供基本的支持。
我们要做的事情就是利用CAAnimation在CALayer上执行各个动画。
我们有一个UIImageView,我们将会把我们的图片按照顺序依次显示在这个UIImageView上。

图片处理

我们首先是定义一个动画组Group来放入我们所有的动画。

CAAnimationGroup *group = [CAAnimationGroup animation];

设置某一帧的关键动画显示这张图片

CAKeyframeAnimation * contentsAnimation;
contentsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
contentsAnimation.duration = 0.5f;
contentsAnimation.removedOnCompletion = NO;
contentsAnimation.fillMode = kCAFillModeForwards;
UIImage *image = /**你的图片**/;
contentsAnimation.values = @[(__bridge UIImage*)image.CGImage];
contentsAnimation.beginTime = totalDuration;
[animations addObject:contentsAnimation];

这是某一张图片的淡入效果:

CAKeyframeAnimation * showAnimation;
showAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
showAnimation.duration = 0.5;
showAnimation.removedOnCompletion = NO;
showAnimation.fillMode = kCAFillModeForwards;
showAnimation.values = @[[NSNumber numberWithFloat:0.0],[NSNumber numberWithFloat:1.0]];
showAnimation.beginTime = AVCoreAnimationBeginTimeAtZero;
[animations addObject:showAnimation];

为了防止图片变形和拉伸,记得设置此时显示图片的Layer的大小

CAKeyframeAnimation * boundsAnimation;
boundsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"bounds"];
boundsAnimation.duration = 0.5f;
boundsAnimation.removedOnCompletion = NO;
boundsAnimation.fillMode = kCAFillModeForwards;
boundsAnimation.values = @[[NSValue valueWithCGRect:CGRectMake(xPoint,yPoint,imageWidth,imageHeight)]];
boundsAnimation.beginTime = AVCoreAnimationBeginTimeAtZero;
[animations addObject:boundsAnimation];

这之后0.5s后给它加个效果吧,比如放大

CAKeyframeAnimation *scaleAnimation;
scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.duration = 1.0f;
scaleAnimation.removedOnCompletion = NO;
scaleAnimation.fillMode = kCAFillModeForwards;
scaleAnimation.values = @[[NSNumber numberWithFloat:1],[NSNumber numberWithFloat:2.0]];
scaleAnimation.beginTime = 0.5f;
[animations addObject:scaleAnimation];

在展示了这一张图片后,我们该让它淡出舞台了

CAKeyframeAnimation * dissAnimation;
dissAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
dissAnimation.duration = 0.5;
dissAnimation.removedOnCompletion = NO;
dissAnimation.fillMode = kCAFillModeForwards;
dissAnimation.values = @[[NSNumber numberWithFloat:1.0],[NSNumber numberWithFloat:0.8],[NSNumber numberWithFloat:0.4],[NSNumber numberWithFloat:0.0]];
dissAnimation.beginTime = totalDuration;
[animations addObject:dissAnimation];

如此循环,我们就可以展示一系列的图片了。

PS:补充一下 animationWithKeyPath可以使用的值:
transform.scale
transform.scale.x
transform.scale.y
transform.rotation.z
opacity
margin
zPosition
backgroundColor
cornerRadius
borderWidth
bounds
contents
contentsRect
cornerRadius
frame
hidden
mask
masksToBounds
opacity
position
shadowColor
shadowOffset
shadowOpacity
shadowRadius

视频处理

下面我将开始处理视频。
因为之前在利用CALayer和CAAnimation处理一些动画效果时,用上了gif图片来展示一系列的动画效果。这里我们也运用类似的方式把视频放入我们的动画Group中。
处理视频首先我们是需要逐帧分解这个视频:

CGSize targetSize = CGSizeMake(600,600);
int fps = 20;
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
imageGenerator.requestedTimeToleranceBefore = kCMTimeZero;
imageGenerator.requestedTimeToleranceAfter = kCMTimeZero;
imageGenerator.apertureMode = AVAssetImageGeneratorApertureModeProductionAperture;
imageGenerator.appliesPreferredTrackTransform = YES;
imageGenerator.maximumSize =  CGSizeMake(targetSize.width/2.0, targetSize.height/2.0);
CMTime cmtime = asset.duration; //视频时间信息结构体
Float64 durationSeconds = CMTimeGetSeconds(cmtime)>3?3:CMTimeGetSeconds(cmtime); //视频总秒数,这里我们只取3秒以内的部分。
NSMutableArray *times = [NSMutableArray array];
Float64 totalFrames = durationSeconds * fps; //获得视频总帧数
CMTime timeFrame;
for (int i = 1; i <= totalFrames; i++) {
    timeFrame = CMTimeMake(i, fps); //第i帧 帧率
    NSValue *timeValue = [NSValue valueWithCMTime:timeFrame];
    [times addObject:timeValue];
}
NSMutableArray *videoThumbArray = [NSMutableArray array];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times
                                     completionHandler:^(CMTime requestedTime,
                                                         CGImageRef  _Nullable image,
                                                         CMTime actualTime,
                                                         AVAssetImageGeneratorResult result,
                                                         NSError * _Nullable error)
 {
     if (result == AVAssetImageGeneratorSucceeded)
     {
         UIImage *tempImage = [UIImage imageWithCGImage:image];
         [videoThumbArray addObject:tempImage];
         if (requestedTime.value == times.count)
         {
               NSLog(@"搞定");
         }
     }
     else
     {
         NSLog(@"获取视频截图出错");
     }
 }];

然后我们便拿到了分解出来的视频逐帧图片数组。类似图片处理的部分,我们将它放入我们的Group并设定好beginTime。

CAKeyframeAnimation * contentsAnimation;
contentsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
contentsAnimation.duration = /**视频的时长**/;
contentsAnimation.removedOnCompletion = NO;
contentsAnimation.fillMode = kCAFillModeForwards;
NSMutableArray *values = [NSMutableArray array];
for (int v = 0; v<videoThumbArray.count; v++)
{
    UIImage *tempImage = videoThumbArray[v];
    [values addObject:(__bridge UIImage*)tempImage.CGImage];
}
contentsAnimation.values = values;
contentsAnimation.beginTime = /**开始时间**/;
[animations addObject:contentsAnimation];

我们也可以像图片那样给这段视频的前后加入淡入淡出的转换动画。完成后我们便可以把这个Group加入我们想要用于显示的Layer中了。

播放

接下来就是要实现如何播放这一些列的动画了。但这之前还是要对几个属性和概念进行了解。

CAMediaTiming协议

该协议通过涂层和动画实现,它构建了一个分层的计时系统,当中的所有对象都描述了其从父对象中的时间值到本地时间的映射。
进行这样一个映射或转换,需要两个步骤:
(1)转换为活动的本地时间。该活动时间包括了该对象出现在父级时间轴上的点,以及与父级的相对速度。
(2)从活动时间转换为本地基本时间。时间模型允许对象多次重复其基本持续时间,并且可选地在重复之前向后播放。
当把这个让人摸不着头脑的协议拿出来谈着后,我们实际需要的是它的属性,speed和timeOffset。

speed

图层的速度,用于将父类时间缩放至本地时间。例如,若当前对象或图层速度为2,那么它相对于父类其速度是父类的2倍。

timeOffset

活动时间的偏移值。父类时间转换至活动的本地时间存在着这么一个公式:t = (tp - begin) * speed + offset.
提到这两个属性是因为我们需要它们来暂停和播放我们的预览视频。我们将所有的动画放入Group中,把它添加在一个speed为0的图层上,然后运用类似Timer的方式,以60帧速率播放每一帧的动画,即设置这个图层的timeOffset,从而实现我们对这些动画的播放、暂停和定位。

CADisplayLink

在这里,我们使用CADisplayLink来作为Timer来刷新我们的动画。CADisplayLink是一个与屏幕刷新率相同的Timer类。之所以使用它是因为相较于NSTimer,CADisplayLink的触发时间更加精准,更适合用于一帧一帧地播放我们Group中的动画。在播放时我们只需要如上文所述调整图层的timeOffset即可。

target.layer.timeOffset += 1.0/60.0;

CADisplayLink也可以很方便地进行暂停等操作。
在完成了图片合成视频的预览之后,我们可以通过AVComposition和AVAssetExportSession真正地合成完整的视频文件,还可以加入音乐等处理。
这个是本文章DEMO的地址:Github

参考资料

Core Animation

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容