AVFoundation开发秘籍笔记:第12章 动图层内容
12.1 Core Animation应用
Core Animation是OS X和iOS平台提供的用于合成和制作动画的框架,苹果平台上面漂亮、流畅的动画效果都是通过这个框架实现的。它提供- -种简 单、声明式应用程序模型使得在不需要使用OpenGL或OpenGL ES框架的前提下就可以很容易地创建高性能、基于GPU的动画效果。我们已经在书中见过AV Foundation是如何使用Core Animation框架提供硬件加速视频渲染效果的,其中就用到了AVPlayerL ayer和AVVideoCapturePreviewLayer两个类。稍后讨论如何使用这些类为视频播放和导出操作添加动画效果。
从高层次视角看,Core Animation包含两类对象:
●Layers: 图层对象由CALayer类定义,并用于管理屏幕中可视化内容的元素。这里所说的内容一般都是图片或Bezier路径,不过图层本身具有可被设置的可视化特征,比如它的背景颜色、透明度和角半径。图层定义了自身的几何属性,比如bounds和position,并且可以将这些元素组合到图层的层次结构中用于创建更复杂的界面。除了基础CALayer类,框架还定义了很多实用的子类,比如用于渲染贴图内容的CATextLayer类和用于渲染Bezier路径的CAShapeLayer类,这两个类在创建动画叠加效果时都非常重要。
●Animations: 动画对象是抽象类CAAnimation 的实例,定义所有动画类型所共有的一些核心动画行为。该框架定义了CAAnimation的许多具体子类,最常用的就是CABasicAnimation和CAKeyFrameAnimation。这些类将动画状态变为单独的图层属性,以便创建简单和复杂的动画效果。CABasicAnimation 可以让你创建简单的单关键帧动画,意味着在一段时 间内将属性状态以动画方式由一种状态变为另一种状态。这个类在实现简单动画时非常实用,比如动态调整图层的尺寸、位置或背景色。CAKeyFrameAnimation用于实现更高级的功能,它对动画中的“关键帧”有着更多的控制。比如,当一个图层沿着Bezier路径动态显示,可以用到关键帧动画来指定具体的时间和节奏。
下面看一个有关CALayer和CABasicAnimation的简单示例。将本书的封面图片作为一个图层,放置于父视图的中间,使它绕z轴动态显示,如图12-1所示。
CALayer *parentLayer = // parent layer
UIImage *image = [UIImage imageNamed:@"lavf_ cover"];
CALayer *imageLayer = [CALayer layer];
// Set the layer contents to the book cover image
imageLayer.contents = (id)image.CGImage;
imageLayer.contentsScale = [UIScreen mainScreen].scale;
// Size and position the layer
CGFloat midX = CGRectGetMidX(parentLayer.bounds);
CGF1oat midY = CGRectGetMidY(parentLayer.bounds);
imageLayer.bounds = CGRectMake(0, 0,image.size.width, image.size.height);
imageLayer .position = CGPointMake (midX,midY);
// Add the image layer as a sublayer of the parent layer
[parentLayer addSublayer:imageLayer];
// Basic animation to rotate around z-axis
CABasicAnimation * rotationAnimation =
[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
// Rotate 360 degrees over a three-second duration, repeat indefinitely
rotationAnimation.toValue = @(2 * M_PI);
rotationAnimation.duration = 3.0f;
rotationAnimation.repeatCount = HUGE_VALF;
// Add and execute animation on the image layer
[imageLayer addAnimation:rotationAnimation forKey:@"rotateAnimation"];
Core Animation的效果通过静态代码的展示和描述很难看得出来;在Chapter 12目录中可以找到一个名为CoreAnimationExample的项目,可以实际运行这个应用程序查看效果。
有关Core Animation框架 更深入的内容不在本书的讨论范围内,不过建议大家阅读NickLockwood撰写的iOS Core Animation (2014, Boston: Addison- Wesley)一书来了解如何有效使用Core Animation框架。
12.2 在AV Foundation中使用CoreAnimation
使用Core Animation为视频应用程序创建叠加效果的方法同使用它在iOS或OS X平台创建实时动画效果的方法几乎一样。最大的区别在于运行动画的时间模型,当创建实时动画时,CAAnimation实例从系统主机时钟获取执行时间,如图12-2所示。
主机时间从系统启动开始计算并单向向前推进,将动画执行时间同主机时间相关联在实时动画方面非常适用,不过对于创建视频动画就不合适。视频动画需要基于“影片时间”来操作,开始的时间应该是影片开始的时间直到影片持续时间结束。另外主机时间是一直向前推进,从不会停止,而影片时间可以停止、暂停、回退或快进。因为动画需要紧密地与视频时间轴绑定,所以需要使用不同的执行时间模式。AV Foundation根据不同的用例给出了两种解决方案,下面先看一下播放场景。
12.2.1 使用 AVSynchronizedL ayer播放
AV Foundation提供了一个专门的CALayer子类AVSynchronizedLayer, 用于与给定的AVPlayerftem实例同步时间。这个图层本身不展示任何内容,仅用来与图层子树协同时间。这样所有在继承关系中附属于该图层的动画都可以从激活的AVPlayerltem实例中获取相应的执行时间,如图12-3所示。
通常使用AVSynchronizedL ayer时会将其整合到播放器视图的图层继承关系中,同步图层直接呈现在视频图层之上,如图12-4所示。这样就可以添加动画标题、水印或下沿字幕到播放视频中,并与播放器的播放栏行为保持完美同步。
下面来看一个小秘密,我们一直都在使用AVSynchronizedLayer类,虽然AVSynchronizedLayer最初用于在视频内容上加入动画图层内容,不过这并不是唯一的用处。当在15 Seconds 应用程序中播放一个组合时就已经见到过它的用法了,即一“个动态的播放头随着视频的播放同步运动。这一功能的实现得益于AVSynchronizedLayer。在项目的Views组中可以找到一个THPlayheadView类。每当有新的AVPlayertem创建时,应用程序就会调用视图的synchronizeWithPlayrtem:方法。这个方法创建两个CAShapeL ayer实例来绘制垂直线条,还有一个CABasicAnimation实例用于在视频播放时动态调整这些图层的x轴位置。这里不对视图进行更深入的分析,不过建议大家去看一下这个类的具体实现代码,更好地理解它的工作原理。.
12.2.2 使用 AVideoCompositionCoreAnimationTool导出
要将Core Animation 图层和动画整合到导出视频中,需要使用AVVideoCompositionCoreAnimationTool类。AVVideoComposition使用这个类将CoreAnimation效果作为视频组合的后期处理阶段纳入,如图12-5所示。
AVVideoCompositionCoreAnimationTool就好像一个魔力 黑盒,不过幸运的是当我们知道如何使用这个类时,它就会发挥出很好的作用。它可将组合视频帧置于视频图层中,并在之后渲染包含自定义叠加效果的动画图层来生成最终的视频帧。
稍后会讨论AVSynchronizedI ayer和AVVideoCompositionCoreAnimationTool的详细用法,不过首先我希望提几点在创建叠加效果时应该注意的问题:
●Core Animation框架的默认行为是执行动画并在动画行为完成后进行处理。通常这些行为就是我们希望在实时案例中使用的,因为时间一旦过去就没法返回了。不过对于视频动画就会有问题,所以需要设置动画的removedOnCompletion属性为NO来禁用这一行为。如果没有这样做,则动画效果就是一次性的,如果用户重新播放视频或在时间轴上移动搓擦条也不会再次看到动画。
●动画的beginTime属性被设置为0.0的话是不会看到动画效果的。Core Animation 将值为0.0的beginTime对象转换为CACurrentMediaTime(),这是当前主机时间,同影片时间轴中的有效时间没有关系。如果希望在影片开头加入动画,将动画的beginTime属性设置成AVCoreAnimationBeginTimeAtZero常量。
12.3 15 Seconds应用程序:添加动画标题
在Core Animation中使用AVComposition的一个挑战就是协调不同的概念和时间模型。在使用AVComposition时,考虑的是轨道以及CMTime和CMTimeRange值。Core Animation没有轨道的概念并使用浮点型数值来表示时间。在一个简单场景中我们可以使用CoreAnimation自己的概念,不过当需要创建一个 更复杂的案例时,最好在两个框架之间定义一个通用的抽象概念来使创建组合资源和动画时具有标准化的模型。下面看一下如何在15 Seconds应用程序中实现这一功能。
在第9章中我们第一次接触到应用程序的数据模型。下面还用之前的示例看看能否将它进行扩展,最终整合Core Animation的功能,如图12-6所示。
应用程序时间轴区域所呈现的元素都是从一个通用基类THTimelineItem中扩展而来。这个类定义了条目的时间范围和它在时间轴中的起始位置。THMedialtem以及它的子类THVideoItem和THAudioItem都是这个基类的子类,分别用于处理项目的视频和音频资源。用同样的方法处理叠加动画是可行的,因为这样就可以在应用程序的时间轴视图中以可视化的形式描述和排列它们了。应用程序的动画行为可以在THTimelineItem子类中实现。用这种方式封装动画代码可以使相应的动画操作被视为时间轴视图中的一个条目,并且在创建组合时可以为动画代码提供一个通用接口。
从Core Animation框架的角度看,应用程序继续保持简单的特征,所以你不需要成为一个CoreAnimation框架的专家也能理解这些内容。创建一个简单的THTimelineItem对象THTitleItem,它用于将动画标题添加到项目中。在本章初学者项目的Models组中可以找到这个类的存根实现版本。下面先来看一下它的接口,如代码清单12-1所示。
代码清单12-1 THTitleItem 接口
#import "THTimelineItem.h"
#import <QuartzCore/QuartzCore.h>
@interface THTitleItem : THTimelineItem
+ (instancetype)titleItemWithText:(NSString *)text image:(UIImage *)image;
- (instancetype)initWithText:(NSString *)text image:(UIImage *)image;
@property (copy, nonatomic) NSString *identifier;
@property (nonatomic) BOOL animateImage;
@property (nonatomic) BOOL useLargeFont;
- (CALayer *)buildLayer;
@end
该对象的初始化用到了一个测试字符串和一个由展示标题元素组成的图片,并提供了一些属性来自定义呈现效果。该对象的核心方法是buildL ayer方法,它负责创建Core Animation图层和动画。下面实现这个功能,第一步要创建一个Core Animation图层,如代码清单12-2所示。
代码清单12-2 THTitleItem 创建图层
#import "THTitleItem.h"
#import "THConstants.h"
@interface THTitleItem ()
@property (copy, nonatomic) NSString *text;
@property (strong, nonatomic) UIImage *image;
@property (nonatomic) CGRect bounds;
@end
@implementation THTitleItem
+ (instancetype)titleItemWithText:(NSString *)text image:(UIImage *)image {
return [[self alloc] initWithText:text image:image];
}
- (instancetype)initWithText:(NSString *)text image:(UIImage *)image {
self = [super init];
if (self) {
_text = [text copy];
_image = image;
_bounds = TH720pVideoRect; // 1
}
return self;
}
- (CALayer *)buildLayer {
// --- Build Layers
CALayer *parentLayer = [CALayer layer]; // 2
parentLayer.frame = self.bounds;
parentLayer.opacity = 0.0f;
CALayer *imageLayer = [self makeImageLayer];
[parentLayer addSublayer:imageLayer];
CALayer *textLayer = [self makeTextLayer];
[parentLayer addSublayer:textLayer];
return parentLayer;
}
- (CALayer *)makeImageLayer { // 3
CGSize imageSize = self.image.size;
CALayer *layer = [CALayer layer];
layer.contents = (id) self.image.CGImage;
layer.bounds = CGRectMake(0.0f, 0.0f, imageSize.width, imageSize.height);
layer.position = CGPointMake(CGRectGetMidX(self.bounds) - 20.0f, 270.0f);
return layer;
}
- (CALayer *)makeTextLayer { // 4
CGFloat fontSize = self.useLargeFont ? 64.0f : 54.0f;
UIFont *font = [UIFont fontWithName:@"GillSans-Bold" size:fontSize];
NSDictionary *attrs =
@{NSFontAttributeName : font,
NSForegroundColorAttributeName : (id) [UIColor whiteColor].CGColor};
NSAttributedString *string =
[[NSAttributedString alloc] initWithString:self.text attributes:attrs];
CGSize textSize = [self.text sizeWithAttributes:attrs];
CATextLayer *layer = [CATextLayer layer];
layer.string = string;
layer.bounds = CGRectMake(0.0f, 0.0f, textSize.width, textSize.height);
layer.position = CGPointMake(CGRectGetMidX(self.bounds), 470.0f);
layer.backgroundColor = [UIColor clearColor].CGColor;
return layer;
}
@end
(1)定义一个bounds变量,它的初始化值为TH720pVideoRect。 它是THConstants.h中定义的一个CGRect常量值,用来表示一个原点为(0, 0),尺寸为(1280, 720)大小的矩形区域。这一尺寸符合应用程序视频的维度要求。
(2)下一步创建Core Animation层对象。创建一个父图层,它包含相应的图片、文本图层和尺寸。还希望设置初始opacity的值为(0.0),这样它就是不可见的。之后通过动态调整这个属性值来控制可见性。
(3)创建一个新的CALayer用于展示图片内容,设置它的contents属性为标题项图片的CGImageRef值。Core Animation只能处理Core Graphics类型,所以需要获取UlImage对象的基础GGImageRef。设置图层的allowsEdgeAntialiasing属性为YES,这样当图片动态显示时边缘就会应用一个抗锯齿效果。最后,根据需求设置图层的bounds和position并返回图层。
(4)创建一个新的CATextLayer用于渲染 字符串内容。处理CATextLayer的最简单方法就是使用NSAttributedString,所以创建一个 需要的NSAttributedString属性字典,其中包含期望的字体和前景色参数,并且为标题项的文本内容创建一个NSAtributedString。 设置属性字符串作为图层的string属性,调整它的bounds和position属性, 并返回图层。
创建完图层后,标题叠加的结构就完成了,不过如果尝试在当前状态下使用这个图层是不会看到任何效果的,因为父图层的模糊值被设置为(0.0)。这是一个正确的初始状态,不过在应用这一图层之前需要添加一些动画来进行融合。首先添加一个淡入淡出动画,如代码清单12-3所示。.
代码清单12-3添加淡 入淡出动画效果
@implementation THTitleItem
...
- (CALayer *)buildLayer {
// --- Build Layers
CALayer *parentLayer = [CALayer layer];
parentLayer.frame = self.bounds;
parentLayer.opacity = 0.0f;
CALayer *imageLayer = [self makeImageLayer];
[parentLayer addSublayer:imageLayer];
CALayer *textLayer = [self makeTextLayer];
[parentLayer addSublayer:textLayer];
// --- Build and Attach Animations
CAAnimation *fadeInFadeOutAnimation = [self makeFadeInFadeOutAnimation];
[parentLayer addAnimation:fadeInFadeOutAnimation forKey:nil]; // 1
return parentLayer;
}
...
- (CAAnimation *)makeFadeInFadeOutAnimation {
CAKeyframeAnimation *animation =
[CAKeyframeAnimation animationWithKeyPath:@"opacity"];
animation.values = @[@0.0f, @1.0, @1.0f, @0.0f]; // 2
animation.keyTimes = @[@0.0f, @0.25f, @0.75f, @1.0f];
animation.beginTime = CMTimeGetSeconds(self.startTimeInTimeline); // 3
animation.duration = CMTimeGetSeconds(self.timeRange.duration);
animation.removedOnCompletion = NO; // 4
return animation;
}
@end
(1)父图层的模糊度被设置为(0.0),所以我们希望创建一个动画效果将图层的模糊值在一个相应时间范围内淡出淡入。通过调用makeFadeInFadeOutAnimation创建这个动画,并使用addAnimation:forKey:方法将它添加到父图层中。如果需要在之后识别或取回这个动画则需要指定一个NSString给forKey:参数,不过本例中不需要,所以这里给这个参数赋值nil即可。
(2)对于每个标题我们都希望快速淡入,在一个给定的时间段完全显示,并在最后渐渐淡出。这是关键帧动画的一个 很好的用例,所以我们创建一个新的CAKeyframeAnimation实例操作图层对象的opacity属性。我们定义一个模糊值组成的数组及其相关keyTimes值组成的数组,keyTimes用于表示模糊值被设置的时间。时间值被标准化为同动画持续时间相关的(0.0)到(1.0)区间。可以对这些时间进行更精确的计算,不过对于示例应用程序我们使用20%的时间淡入和20%的时间淡出。
(3)使用THTimelineItem定义的两个时间相关属性来确定动画的beginTime和duration。使用CMTimeGetSeconds函数将startTimeInTimeline属性转换为浮点型值来设置动画的beginTime属性。同样,取得对象timeRange. duration以秒计算的值并设置它作为动画的duration。
(4)要记得经常设置动画的removedOnCompletion属性为NO,这样动画不会在执行之后被移除。如果没有明确设置这个属性,则意味着动画只能出现一次。
上述方法实现了所有标题项需要的核心淡入淡出效果。如果希望更有趣一些的话,可以对标题项的图片也使用动画效果。特别地,如果标题项的animateImage属性被设置为YES,则可以对图片应用一个旋转或弹出动画。下面看一下如何实现方法来创建这些动画效果,如代码清单12-4所示。
代码清单12-4为标题图片添加动画
- (CALayer *)buildLayer {
// --- Build Layers
CALayer *parentLayer = [CALayer layer];
parentLayer.frame = self.bounds;
parentLayer.opacity = 0.0f;
CALayer *imageLayer = [self makeImageLayer];
[parentLayer addSublayer:imageLayer];
CALayer *textLayer = [self makeTextLayer];
[parentLayer addSublayer:textLayer];
// --- Build and Attach Animations
CAAnimation *fadeInFadeOutAnimation = [self makeFadeInFadeOutAnimation];
[parentLayer addAnimation:fadeInFadeOutAnimation forKey:nil];
if (self.animateImage) {
parentLayer.sublayerTransform = THMakePerspectiveTransform(1000); // 1
CAAnimation *spinAnimation = [self make3DSpinAnimation];
NSTimeInterval offset = // 2
spinAnimation.beginTime + spinAnimation.duration - 0.5f;
CAAnimation *popAnimation =
[self makePopAnimationWithTimingOffset:offset];
[imageLayer addAnimation:spinAnimation forKey:nil]; // 3
[imageLayer addAnimation:popAnimation forKey:nil];
}
return parentLayer;
}
...
static CATransform3D THMakePerspectiveTransform(CGFloat eyePosition) {
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / eyePosition;
return transform;
}
- (CAAnimation *)make3DSpinAnimation {
// To be implemented
return nil;
}
- (CAAnimation *)makePopAnimationWithTimingOffset:(NSTimeInterval)offset {
// To be implemented
return nil;
}
(1) 应用一个3D动画效果绕y轴旋转。要实现这个效果,需要使用父图层的sublayerTransform属性设置一个透视变化。我们在第7章讨论人脸识别时也用到了类似效果。这个方法将图片的二维平面映射到三维空间。
(2)创建一个CAAnimation来应用旋转动画并计算一个偏移量, 用于创建弹出动画。偏移 量会用到旋转动画的beginTime和duration,之后从这个值减掉半秒。这样就在旋转动作最后半秒加入了一个弹出动画。
(3)使用addAnimation:forKey:方法将两个动画添加到imageLayer.
注意:
核心动画提供了一个CAAnimationGroup类,用于将多个动画组合在一起同步执行。如果你是一个有经验的Core Animation开发者,可能就会问为什么不把旋转和弹出动画放在一起。虽然我们从来没有得到一个明确的说明,指出动画组不能用于AVSynchronizedLayer,不过还是建议大家在创建视频动画时不要尝试这么做。
下面具体实现这两个动画方法,来完成THTitleItem类, 如代码清单12-5所示。
代码清单12-5图片 动画方法
- (CAAnimation *)make3DSpinAnimation {
CABasicAnimation *animation = // 1
[CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
animation.toValue = @((4 * M_PI) * -1); // 2
animation.beginTime = CMTimeGetSeconds(self.startTimeInTimeline) + 0.2; // 3
animation.duration = CMTimeGetSeconds(self.timeRange.duration) * 0.4;
animation.removedOnCompletion = NO;
animation.timingFunction = // 4
[CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
return animation;
}
- (CAAnimation *)makePopAnimationWithTimingOffset:(NSTimeInterval)offset {
CABasicAnimation *animation = // 5
[CABasicAnimation animationWithKeyPath:@"transform.scale"];
animation.toValue = @1.3f; // 6
animation.beginTime = offset; // 7
animation.duration = 0.35f;
animation.autoreverses = YES; // 8
animation.removedOnCompletion = NO;
animation.timingFunction =
[CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
return animation;
}
(1)首先创建一个CABasicAnimation为图片的变形添加动画效果。CoreAnimationProgramming Guide文档给出了一些Key-Value Coding Extensions的介绍,可以帮助我们很容易地实现上述功能。本例使用transform.rotation.y键路径让图片绕y轴旋转。
(2)我们要实现的效果是图片以逆时针的方向绕y轴旋转两圈。一圈360度即2r弧度,所以我们设置4 * M_PI实现旋转2周。
(3)采用与淡入淡出动画类似的方式计算beginTime和duration。需要进行一些小的计算来调整这些时间。对于这些计算没有神奇的公式,只有看起来最适合的方法,所以开发者只需选择自己熟悉的方法即可。
(4) 为动画应用一个时间函数。默认情况下计算插值的时候动画会使用线性曲线。这么做的线条会略显僵硬,通常我们会使用一些柔和的处理方法让曲线看起来更自然。本例中使用缓入缓出曲线。
(5)对于弹出动画,需要创建一个CABasicAnimation为图片的变形过程应用一个小的缩放效果。
(6)设置动画的toValue值为@1.3,将图片放大30%。
(7)由于这个动画是作为前一个动画的一部分, 所以设置beginTime为传递给方法的偏移量,并设置持续时间为0.35。duration值可以任意取,所以根据首选项进行设置。
(8)动画对象具有自动回溯功能。这是我们在执行动画时一个非常方便的方法,可以反向运行动画,使它返回到初始状态。设置autoreverses属性为YES,在动画最后快速缩放图片生成一个小的跳动效果。
THTitleItem类已经完成了,不过在看到这一-效果前还需要做一点工作。 建议大家运行本章的FifteenSeconds_Final项 目来看实际的完成效果。一些新的用户界面元素被加入到这个应用程序中,可以让用户实验多种动画效果。Settings 菜单现在包含一个Video Titles开关用于激活动画轨道。在播放栏区域添加了一个UISlider,这样在播放组合时就可以来回调节,并且可以看到动画完美地与时间轴锁定,如图12-7所示。
注意:
当在菜单中激活了Video Titles开关时,会在时间轴区域添加两个预置标题。构建这些标题项的代码可以在THTimelineViewController类的addTitleltems 方法中找到。
在下一节中,我们会介绍如何在组合上使用THTitleItem。
12.4 准备组合
不需要在本章创建新的THComposition和THCompositionBuilder对象,因为前一章中创建的对象已经可以满足我们的需求了。在示例项目中找到THOverlayComposition和THOverlayCompositionBuilder类,它们都是从之前一章中实现的方法复制过来的,只不过稍加修改,整合了上一节中创建的Core Animation相关的代码,如代码清单12-6所示。
代码清单12-6 THOverlayComposition 接口
#import "THComposition.h"
@interface THOverlayComposition : NSObject <THComposition>
@property (strong, nonatomic, readonly) AVComposition *composition;
@property (strong, nonatomic, readonly) AVVideoComposition *videoComposition;
@property (strong, nonatomic, readonly) AVAudioMix *audioMix;
@property (strong, nonatomic, readonly) CALayer *titleLayer;
- (id)initWithComposition:(AVComposition *)composition
videoComposition:(AVVideoComposition *)videoComposition
audioMix:(AVAudioMix *)audioMix
titleLayer:(CALayer *)titleLayer;
@end
上面的代码同前一章中创建的一样,不过在初始化方法中增加了一个新的CALayer属性和一个新的titleLayer参数。在我们讨论对类实现进行改动来使用THTitleItem内容前,首先看一下在THOverlayCompositionBuilder类中增加的内容。
组合创建方法需要进行一些小改动。 最关键的一点是 在THTimeline中获取THTitleItem实例,将它们汇总到一个单独的CALayer对象中,并将它添加到THOverlayComposition中,如代码清单12-7所示。
代码清单12-7创建视频图层
@implementation THOverlayCompositionBuilder
...
- (id <THComposition>)buildComposition {
self.composition = [AVMutableComposition composition];
[self buildCompositionTracks];
AVVideoComposition *videoComposition = [self buildVideoComposition];
return [[THOverlayComposition alloc] // 1
initWithComposition:self.composition
videoComposition:videoComposition
audioMix:[self buildAudioMix]
titleLayer:[self buildTitleLayer]];
}
- (CALayer *)buildTitleLayer {
if (!THIsEmpty(self.timeline.titles)) {
CALayer *titleLayer = [CALayer layer]; // 2
titleLayer.bounds = TH720pVideoRect;
titleLayer.position = CGPointMake(CGRectGetMidX(TH720pVideoRect),
CGRectGetMidY(TH720pVideoRect));
for (THTitleItem *compositionLayer in self.timeline.titles) { // 3
[titleLayer addSublayer:[compositionLayer buildLayer]];
}
return titleLayer;
}
return nil;
}
...
@end
(1)创建并返回一个THOverlayComposition实例。与上一章的代码相比,唯一的变化时调用了私有方法buildTtleLayer,用这个方法返回一个包含动画图层效果的CALayer实例。
(2)创建一个新的CALayer实例并设置它的bounds和position。在计算这些值的时候我们仍然使用TH720pVideoRect常量。15 Seconds中只用到720p视频剪辑,不过在我们自己开发的应用程序中,这一图层的尺寸要根据实际情况设置。
(3)遍历时间轴包含的所有标题数据,对于每一个标题调用它的buildI ayer方法并将返回的图层添加到titleLayer。
THOverlayCompositionBuilder类已经完成了,最后一件事是讨 论在THOverlayComposition类中创建AVPlayerltem和AVAssetExportSession时如何使用这些图层。
12.4.1 播放时应用 Core Animation
要将这些层整合到应用程序的播放功能中,需要使用AVSynchronizedLayer类。在组合初始化方法中创建的图层将被添加到同步图层,确保动画的播放与视频时间轴同步。下面看一下THOverlayComposition中的makePlayable方法的具体实现,如代码清单12-8所示。
代码清单12-8播放功能 中使用Core Animation
- (AVPlayerItem *)makePlayable {
AVPlayerItem *playerItem =
[AVPlayerItem playerItemWithAsset:[self.composition copy]];
playerItem.videoComposition = self.videoComposition;
playerItem.audioMix = self.audioMix;
if (self.titleLayer) { // 1
AVSynchronizedLayer *syncLayer =
[AVSynchronizedLayer synchronizedLayerWithPlayerItem:playerItem];
[syncLayer addSublayer:self.titleLayer];
// WARNING: This the 'titleLayer' property is NOT part of AV Foundation
// Provided by AVPlayerItem+THAdditions category.
playerItem.syncLayer = syncLayer; // 2
}
return playerItem;
}
(1)如果一个有效的titlelayer存在,则使用当前的AVPlayertem创建一个新的AVSynchronizedLayer实例。添加ntitlelLayer作为子图层,确保所有动画都和AVPlayerltem实例保持同步。
(2)为AVPlayerltem添加一个分类方法将AVSynchronizedLayer传递到应用程序的THPlayerViewController中。这么做是为了将AVSynchronizedL ayer的使用限制在这个类中以简化讨论。在实际应用程序中,AVSynchronizedL ayer的创建和管理最好都在THPlayerViewController自身进行处理。
载入默认的组合,在Setting菜单激活Video Titles开关,并播放组合。现在我们已经实现了和视频内容同步播放的动画标题。播放组合时,可以使用播放按钮旁的滑动条调整视频进度来检查动画效果是否和时间轴保持同步。播放场景的工作已经完成了,下 面继续讨论导出组合时如何应用这些效果。
12.4.2 导出时应用Core Animation
在导出场景中整合Core Animation图层,需要用到AVVideoCompositionCoreAnimationTool对象。这个对象允许将Core Animation图层作为视频组合的后期处理阶段纳入。这个对象用起来比较简单,不过并不是特别直观,所以下面看一下如何使用这个工具,如代码清单12-9所示。
代码清单12-9导出功能中使用Core Animation
- (AVAssetExportSession *)makeExportable {
if (self.titleLayer) {
CALayer *animationLayer = [CALayer layer]; // 1
animationLayer.frame = TH720pVideoRect;
CALayer *videoLayer = [CALayer layer];
videoLayer.frame = TH720pVideoRect;
[animationLayer addSublayer:videoLayer]; // 2
[animationLayer addSublayer:self.titleLayer];
animationLayer.geometryFlipped = YES; // 3
AVVideoCompositionCoreAnimationTool *animationTool = // 4
[AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer
inLayer:animationLayer];
AVMutableVideoComposition *mvc =
(AVMutableVideoComposition *)self.videoComposition;
mvc.animationTool = animationTool; // 5
}
NSString *presetName = AVAssetExportPresetHighestQuality;
AVAssetExportSession *session =
[[AVAssetExportSession alloc] initWithAsset:[self.composition copy]
presetName:presetName];
session.audioMix = self.audioMix;
session.videoComposition = self.videoComposition;
return session;
}
(1)首先创建两个CALayer实例。组合的视频帧被放在videoLayer中,animationI ayer将被渲染并生成导出使用的最终视频帧。
(2)设置动画图层的geometryFlipped属性为YES来确保标题被正确渲染。如果没有设置这个值将导致图片和文本的位置发生颠倒。
(3)使用videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:inLayer初始化方法创建一个新的AVVideoCompositionCoreAnimationTool实例,为其传递动画和视频图层。这样做的目的是在视频组合的后处理阶段将视频帧和Core Animation图层整合到一起。
(4)设置AVVideoCompositionCoreAnimationTool作 为视频组合的animationTool属性,供在导出时使用。
在设备上运行该应用程序并载入默认的组合。确保在Settings菜单中激活了Video Titles并点击Export Composition菜单项。在一个简短的导出过程之后就可以切换到iOS的相机应用程序,并播放视频。可以看到我们设置的动画标题图层成功地在导出的视频内容中渲染,如图12-8所示。
12.5 小结
本章我们完成了15 Seconds应用程序的全部内容,结果看起来非常棒!我们学习了如何使用AVSynchronizedL ayer在视频播放中添加Core Animation效果, 还有如何在导出过程中使用AVVideoCompositionCoreAnimationTool整合这些效果。AV Foundation向视频项目中添加标题的方法最初看起来比较奇怪,不过当你习惯了这种方法后就会非常适应。本章还大致介绍了一种用于简化AV Foundation和Core Animation整合过程的解决方案。相信类似的设计模式在开发者创建更大或更复杂的视频应用程序时也会提供很大的帮助。