11.1 综述
大部分视频编辑应用程序的一个重要功能就是能够创建动态视频过渡效果。AVFoundation对于这一功能的支持 具有很高的可靠性,不过同时也被认为是学习媒体编辑API中最具挑战性的一个领域。这个功能是整个框架中文档介绍最少的几个功能之一,这一部分主要包括如何使用这个稍微复杂一点的API(而 且较难调试)。本章内容会逐步进行讲解让你对这个问题有一个比较深入的认识,避免一些常见的陷阱。下面从学习几个创建视频过渡的类开始学习吧,如图11-1所示。
11.1.1 AVVideoComposition
框架的视频过渡类API中最核心的类是AVVideoComposition。这个类对两个或多个视频轨道组合在一起的方法给出了一个总体描述。它由一组时间范围和描述组合行为的介绍内容组成,这些信息出现在组合资源内的任意时间点。除了包含描述输入视频层组合的信息之外,还提供了配置视频组合的渲染尺寸、缩放和帧时长等属性。视频组合的配置确定了委托对象处理时AVComposition的呈现方式,这里的委托对象比如AVPlayer或AVAssetlmageGenerator.
注意:
一个值得重点注意的是,不管名字是如何定义的,AVVideoComposition 并不是AVComposition的子类,甚至没有直接关联。在视频播放、导出或处理时会用这个类来控制资源视频轨道的视频组合行为。
11.1.2 AVVideoCompositionInstruction
AVVideoComposition是由一组AVVideoCompositionInstruction对 象格式定义的指令组成的。这个对象所提供的最关键的一段 数据是组合对象时间轴内的时间范围信息,这一时间范围是在某一组 合形式出现时的时间范围。要执行的组合特质是通过其layerInstructions集合定义的。
11.1.3 AVVideoCompositionL .ayerInstruction
AVVideoCompositionLayerInstruction用于定义对给定视频轨道应用的模糊、变形和裁剪效果。它提供了一些方法用 于在特定的时间点或在一个时间范围内对这些值进行修改。在一段时间内对这些值应用渐变操作可以让开发者创建出动态的过渡效果,比如溶解和渐淡效果。
与所有的AV Foundation媒体编辑类-样,视频组合API具有不可变和可变两种形式。不可变超类形式适用于客户端对象,比如AVAssetExportSession,不过当我们创建自己的视频组合应用程序时,应该使用不可变子类。
注意:
与AVAudioMix类似,AVVideoComposition 并不直接和AVCompoition相关。相反,这些对象是和类似AVPlayerltem 的客户端相关联,在播放组合或进行其他处理时使用这些对象。第一眼看起来可能会觉得奇怪,不过这么设计是有益的。未将AVComposition与输出行为强耦合,这样可以让开发者在播放、导出或处理视频时更灵活地确定如何使用这些行为。
11.2 概念阶段
在开发15 Seconds应用程序的过程中,我们所使用的一直是一个单独的视频轨道,其中按时间轴顺序排列了一系列的视频剪辑,如图11-2所示。
这一轨道的排列在创建一个cut-only编辑时很清楚地知道需要什么。不过当需要在独立的视频分段间实现一个动态过渡效果时,这一排列无法满足我们的需要。学习创建过渡效果的有效方法是将这一过程分解为相关的小步骤,这些步骤确实是概念步骤,用于帮助理解这一过程,不过当我们熟悉了这些概念后,会经常将两个或多个概念进行组合变成一个总体的概念了。
11.2.1 部署视频布局
要在剪辑间添加过渡,首先需要将两个轨道间的视频片段重新部署。我们已经知道如何将多个音频轨道组合在一起,那么视频的情况也是一样的。大多数情况下两个轨道就足够了,不过为了满足一些特殊需求而加入更多的轨道也是可以的,要注意同时添加过多的轨道会对性能产生负面影响。首先创建一个包 含两个视频轨道的组合,如图11-3所示。
构建上图中组合的代码如下所示。
AVMutableComposition *composition = [AVMutableComposition composition];
AVMutableCompositionTrack *trackA =
[composition addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *trackB =
[composition addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:kCMPersistentTrackID_Invalid];
NSArray *videoTracks = @[trackA, trackB];
上面的示例首先创建了一个新的可变组合并添加了两个AVMediaTypeVideo类型的组合轨道。将组合轨道添加到-一个NSArray中备以后使用。
所需的轨道安排好之后,需要错开部署这两个轨道的视频布局,如图11-4所示。
图11-4展示了一个三段视频的排列,不过在实际组合中的所有视频剪辑都可以遵循相同的A-B模式。当视频片段按照这种方式排列后,视频片段前面或后面的空间都会添加一个空片段。它们都是普通的AVCompositionTrackSegment实例,与视频是-样的,不过它们不包含何媒体。这个过程的实现如以下代码所示。
NSArray *videoAssets = nil;// array of loaded AVAsset instances;
CMTime cursorTime = kCMTimeZero;
for (NSUInteger i = 0; i < videoAssets.count; i++) {
NSUInteger trackIndex = I%2;
AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
AVAsset *asset = videoAssets[I];
AVAssetTrack *assetTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstobject];
CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
[currentTrack insertTimeRange:timeRange
ofTrack:assetTrack
atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, timeRange.duration);
}
示例对所有添加到组合中的视频资源进行遍历。迭代过程首先通过i % 2来计算A-B模式下目标轨道的索引号。将视频轨道从当前资源中提取出来,并将它们插入currentTrack中。 最后,更新cursorTime为 下一次迭代更新插入点。
轨道和剪辑的布局已经完成了,不过还有一个问题需要解决。知道是什么吗?虽然我们已经以交错方式排列这些轨道,不过每个片段的开始都还紧接着上一片段的结尾。这就意味着我们在两个可见剪辑间没有任何空间,也就无法将它们组合在一起。下一步来解决这一问题。
11.2.2 定义重叠区域
要在两个片段中应用视频过渡,需要根据期望的过渡持续时长来确定片段的重叠情况。解决这一问题很简单,只需对上例中计算cursorTime的方法稍加调整。
NSArray *videoAssets = nil;// array of loaded AVAsset instances;
CMTime cursorTime = kCMTimeZero;
CMTime transitionDuration = CMTimeMake(2, 1);
for (NSUInteger i = 0; i < videoAssets.count; i++) {
NSUInteger trackIndex = I%2;
AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
AVAsset *asset = videoAssets[I];
AVAssetTrack *assetTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstobject];
CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
[currentTrack insertTimeRange:timeRange
ofTrack:assetTrack
atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, timeRange.duration);
cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
}
示例中为期望的过渡持续时长定义了一个CMTime值,这里设为2秒。在循环结束时从cursorTime减去transitionDuration,这样就能考虑到过渡时长适度偏移下一个插入点。图11-5给出了这一变化的结果。
此时组合是可以播放的,不过输出结果可能并不理想。视频轨道具有内在的z索引行为,第一个视频轨道和第二个之前,第二个在第三个之前,诸如此类。如果在这种情况下播放组合,会发现第一段视频,第二段视频什么都没有,最后会看到最前面轨道中所包含的第三个片段。在我们看到第二个轨道的内容前,需要定义时间范围并向组合方法解释这两个轨道应该如何组合。下面继续下一个步骤并开始进行处理。
注意:
在创建这些重叠区域时要记住,我们巳经将视频时间轴减少了(videoCount-1)*transitionDuration的大小。如果组合中还有额外的轨道,则需要将它们的时长相应地缩短以满足新的视频时间轴。
11.2.3 计算通过和过渡的时间范围
AVVideoCompostion由一组AVVideoCompositionInstruction对 象组成。其中最重要的数据是时间范围,它用来表示某种出现的组合方式持续的时长。在开始创建AVVideoCompositionInstruction实例前,首先需要为组合对象计算一系列时间范围。
需要计算两个类型的时间范围。第一个通常被认为是通过(pass through)时间范围,在这个时间范围内希望一个轨道的所有帧序列都在不与其他轨道进行混合的情况下通过某一区域。第二个时间范围类型为过渡(ransition)时间范围。它定义了在组合中视频片段重叠的区域,并在时间抽上标记出应用过渡效果的区域。图11-6给 出了示例组合中的通过时间范围和过渡时间范围。
可通过多种方式计算这些时间范围,不过下面的示例给出了一种很好的通用方法。这一计算过程通常作为轨道和片段创建过程的一部分,不过为了更好地说明问题,在这个示例中我们单独来实现这个功能。
NSMutableArray *videoAssets = // loaded video assets
CMTime cursorTime = kCMTimeZero;
// 2 second transition duration
CMTime transDuration = CMTimeMake(2, 1);
NSMutableArray *passThroughTimeRanges = [NSMutableArray array];
NSMutableArray *transitionTimeRanges = [NSMutableArray array];
NSUInteger videoCount = [videoAssets count];
for (NSUInteger i = 0; i < videoCount; i++) {
AVAsset *asset = videoAssets[I];
CMTimeRange timeRange = CMTimeRangeMake(cursorTime, asset.duration) ;
if(i>0) {
timeRange.start = CMTimeAdd(timeRange.start, transDuration) ;
timeRange .duration = CMTimeSubtract (t imeRange . duration, transDuration) ;
}
if (i + 1 < videoCount){
timeRange.duration = CMTimeSubtract(timeRange.duration, transDuration);
}
[passThroughTimeRanges addobject: [NSValue valueWi thCMTimeRange :t imeRange]];
cursorTime = CMTimeAdd(cursorTime, asset.duration);
cursorTime = CMTimeSubtract (cursorTime, transDuration);
if (i + 1 < videoCount) {
timeRange = CMTimeRangeMake(cursorTime, transDuration);
NSValue *timeRangeValue = [NSValue valueWithCMTimeRange:timeRange];
[transitionTimeRanges add0bject:timeRangeValue];
}
}
示例中对组合的视频集进行了遍历。对于每个视频都创建了一个初始时间范围,之后根据其原始位置,对时间范围的起点和(或)持续时间进行修改。计算出cursorTime值后,基于cursorTime和transDuration创建相关的过渡时间范围。
注意:
CMTime和CMTimeRange都是结构,所以它们不能直接添加到NSArray或NSDictionary中。取而代之的是,我们使用AVTime.h头文件中定义的category方法将它们封装成NSValue实例。
提示:
上面示例中的效果很难用文字进行描述。我们发现随着代码的步骤在纸上绘制一些矩形框可能会对这一过程的理解有帮助。
创建所需的通过和过渡时间范围被认为是整个处理过程中最重要的一步操作。从面上看,它并不是最难解决的问题,不过它非常容易出错。在进行这一步处理时一定要记住两个关键点:
●我们计算的时间范围必须没有任何空隙或重叠。必须是紧接.上一个片段之后按需排列的最新时间范围。
●计算必须考虑组合对象持续时间。如果组合中还包含额外的轨道,就需要使它们遵循目前的视频时间轴,或者根据它们的持续时长扩展最终的时间范围。
如果没有注意到这两点,组合对象仍可播放,不过视频内容将不会被渲染,只会显示一个黑屏。这样调试起来会很麻烦,不过苹果公司发布过一个工具类,可以帮助开发者诊断这些问题。在Apple Developer Center中会找到一个项目AVCompositionDebugViewer(支持Mac和iOS版本),可以帮助我们呈现组合。图11-7给出了这个工具的截屏。
这个类帮助我们呈现组合及其相关的AVVideoComposition和AVAudioMix对象,使开发者可以很容易地找到问题。
现在我们创建了所需的通过和过渡时间范围,进入下一步,创建组合指令吧。
11.2.4 创建组合和层指令
下一步是创建AVVideoCompositionInstruction和AVVideoCompositionIayerInstruction实例,提供视频组合方法所执行的指令。下面的代码有些复杂, 所以我们给出了一些标注。
NSMutableArray *compositionInstructions = [NSMutableArray array];
// Look up all of the video tracks in the composition
NSArray *tracks = [composition tracksWithMediaType:AVMediaTypeVideo];
for (NSUInteger i = 0; i < passThroughTimeRanges.count; i++) { // 1
// Calculate the trackIndex to operate upon: 0,1, 0, 1, etc.
NSUInteger trackIndex = I%2;
AVMutableCompositionTrack *currentTrack = tracks[trackIndex];
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoComposi tionInstruction videoComposit ionInstruction]; // 2
instruction.timeRange = [passThroughTimeRanges[i] CMTimeRangeValue];
AVMutableVideoCompositionLayerInstruction *layerInstruction = // 3
[AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack: currentTrack];
instruction.layerInstructions = @[layerInstruction];
[compositionInstructions addobject:instruction];
if (i < transitionTimeRanges.count) {
AVCompositionTrack *foregroundTrack = tracks [trackIndex]; // 4
AVCompositionTrack *backgroundTrack = tracks[1 - trackIndex];
AVMutableVideoComposit ionInstruction *instruction =
[AVMutableVideoCompositionInstruction videoCompositionInstruction]; // 5
CMTimeRange timeRange = [transitionTimeRanges[i] CMTimeRangeValue];
instruction.timeRange = timeRange;
AVMutableVideoCompositionlayerInstruction *fromLayerInstruction = // 6
[AVMutableVideoCompos itionLayerInstructionvideoCompositionLayerInstructionWithAssetTrack:foregroundTrack];
AVMutableVideoCompositionLayerInstruction *toLayerInstruction =
[AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:backgroundTrack];
instruction.layerInstructions = @[fromLayerInstruction,toLayerInstruction]; // 7
[compositionInstructions addobject: instruction];
}
}
(1)首先遍历之前计算的所有通过时间范围。循环在两个需要创建所需指令的视频轨道间前后切换。
(2)创建一个新的AVMutableVideoCompositionInstruction实例并 设置当前通过CMTimeRange作为它的timeRange属性。
(3)之后为活动组合创建一个新的AVMutableVideoCompositionI ayerInstruction,将它添加到数组中并设置它作为组合指令的layerInstructions属性。组合的通过时间范围区域只需要一个与要呈现视频帧的轨道相关的单独层指令。
(4)要创建过渡时间范围指令,需要得到前一个轨道(过渡前的轨道)的引用和后一个轨道(过渡后的轨道)的引用。按这种方式查找轨道可以确保轨道的引用始终顺序正确。
(5)创建另一个AVMutableVideoCompositionInstruction实例,设置当前过渡时间范围为它的timeRange属性。
(6)为每个轨道创建一个AVMutableVideoCompositionI ayerInstruction实例。在这些层指令上定义从一个场景到另一个场景的过渡效果。本例中没有使用过渡效果。取而代之的是,我们将在本章示例应用程序中讨论这一问题。
(7)将两个层指令都添加到NSArray中,并设置它们作为当前组合指令的layerInstructions属性。对这一数组中的元素进行排序非常重要,因为它定义了组合输出中视频图层的z轴顺序。
所有需要的组合和层指令都成功创建了,现在继续完成最后一步,创建和配置AVVideoComposition。
11.2.5 创建和配置 AVVideoComposition
最困难的工作已经完成了,剩下的就是创建和配置一个 AVVideoComposition实例,如下面的代码所示:
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.instructions = compositionInstructions;
videoComposition.renderSize = CGSizeMake(1280.0f,720.0f);
videoComposition.frameDuration = CMTimeMake(1, 30);
videoComposition.renderScale = 1.0f;
示例中创建了一个AVMutableVideoComposition实例并设置了4个关键属性:
●instructions属性用于设置我们在第4步中创建的组合指令。这些指令向组合器描述时间范围和执行组合的种类。
●renderSize 属性是-一个CGSize值,定义组合应该被渲染的尺寸。这个值应该对应于组合中的视频原始大小,比如720p视频为1280 x 720,1080p 视频为1920 x1080。
●frameDuration用于设置有效的帧率。回顾一下AV Foundation很少处理帧率,而是经常用到帧时长。帧时长是帧率的倒数,所以要设置30FPS的帧率,则定义的帧时长应该是1/30 秒。
●renderScale 定义了视频组合应用的缩放。大部分情况下设置为1.0。
如何这是第一次在AV Foundation中创建视频过渡,那么可以假设你已经觉得有些头疼或已经晕了。我们知道开发者的痛苦,因为学习视频过渡的确是这个框架有关视频编辑功能中最难的一部分,不过我们还是有好消息告诉你的,继续往下看。
11.2.6 创建视频组合的捷径
在很多情况下有一种比上一节中介绍的方法 更简便的创建AVVideoComposition的方法。AVVideoComposition定义了一个便 捷初始化方法videoCompositionWithPropertiesOfAsset:,将AVComposition作为资源参数并创建了一个基础AVVideoComposition。这个方法创建了一个带有如下配置的AVVideoComposition对象:
●instructions属性包含- -组完整的基于组合视频轨道(以及其中包含的片段空间布局)的组合和层指令。
●renderSize 属性被设置为AVComposition对象的naturalSize,或者如果没有设置,则使用能够满足组合视频轨道中最大视频维度的尺寸值。
●frameDuration 设置为组合视频轨道中最大nominalFrameRate 的值。如果所有轨道的nominalFrameRate值都为0,则frameDuration设置成默认值1/30秒(30FPS)。
●renderScale 始终设置为1.0。
你可能会问为什么没有一开始就介绍这种方法, 而跳过哪些复杂的实现过程。实际上主要有以下两个重要原因:
●只有理解了前面讨论的步骤,才能发现学习这些对象的使用变容易了,因为我们仍需要对过渡效果配置一些指令。 如果不理解如何构建组合及其原因,则很难得到所需的结果,而且几乎不可能在应用程序出现问题时进行调试。
●创建的可 能并不是我们所需要的。以这种方式创建AVVideoComposition 可能适用于很多场景,不过这并不能解决全部问题。很多时候当需要对组合的时间范围和指令进行更多控制时,就需要从头开始手动创建组合对象。
总的来说,越了解这一过程, 离精通AV Foundation的过渡功能更近了一步。
11.3 为15 Seconds应用程序添加视频过渡
现在我们对创建视频过渡的概念已经比较了解了,下面讨论如何将这个功能放到15Seconds应用程序中。在Chapter 10目录中可以找到15 Seconds应用程序项目的启动版本,下面从上一章剩下的工作开始继续完善它。
首先创建一个新的类THTransitionComposition,并令它遵循THComposition协议,如代码清单11-1所示。
代码清单11-1 THTransitionComposition 接口
#import "THComposition.h"
@interface THTransitionComposition : NSObject
@property (strong, nonatomic, readonly) AVComposition *composition;
@property (strong, nonatomic, readonly) AVVideoComposition *videoComposition;
@property (strong, nonatomic, readonly) AVAudioMix *audioMix;
- (id)initWithComposition:(AVComposition *)composition
videoComposition:(AVVideoComposition *)videoComposition
audioMix:(AVAudioMix *)audioMix;
@end
这个类看起来和之前几章中创建的类类似;唯一的区别是增加了一个新的AVideoComposition属性和在初始化方法中增加了一个videoComposition参数。下面看一下该类的具体实现,如代码清单11-2所示。
代码清单11-2 THTransitionComposition实现
#import "THTransitionComposition.h"
@implementation THTransitionComposition
- (id)initWithComposition:(AVComposition *)composition
videoComposition:(AVVideoComposition *)videoComposition
audioMix:(AVAudioMix *)audioMix {
self = [super init];
if (self) {
_composition = composition;
_videoComposition = videoComposition;
_audioMix = audioMix;
}
return self;
}
- (AVPlayerItem *)makePlayable { // 1
AVPlayerItem *playerItem =
[AVPlayerItem playerItemWithAsset:[self.composition copy]];
playerItem.audioMix = self.audioMix;
playerItem.videoComposition = self.videoComposition;
return playerItem;
}
- (AVAssetExportSession *)makeExportable { // 2
NSString *preset = AVAssetExportPresetHighestQuality;
AVAssetExportSession *session =
[AVAssetExportSession exportSessionWithAsset:[self.composition copy]
presetName:preset];
session.audioMix = self.audioMix;
session.videoComposition = self.videoComposition;
return session;
}
@end
(1)首先在makePlayable方法中 创建一个带 有AVComposition实例的新的AVPlayerItem。像上一章中所做的那样设置播放器条目的audioMix和videoComposition属性。这样在播放时可以对播放器应用自定义音频处理和视频组合行为。
(2)在makeExportable方法中创建一个新的AVAssetExportSession,传递给它一个composition副本和一个 预设名。与makePlayable方 法类似,对导出会话的相应属性设置audioMix和videoComposition,这样在导出时就可以应用音频和视频处理了。
下一步为对象创建相关的THCompositionBuilder。在示例项目的Models/Builder/Private组中会发现一个名为THTransitionCompositionBuilder的存根类。代码清单11-3给出了这个类的接口。
代码清单11-3 THTransitionCompositionBuilder 接口
#import "THCompositionBuilder.h"
#import "THTimeline.h"
@interface THTransitionCompositionBuilder : NSObject
- (id)initWithTimeline:(THTimeline *)timeline;
@end
不看这个类的名字的话,这个接口内容和我们之前几章创建的其他类接口很相似,继续看这个类的具体实现,如代码清单11-4所示。
代码清单11-4 THTransitionCompositionBuilder 实现
#import "THTransitionCompositionBuilder.h"
#import "THVideoItem.h"
#import "THAudioItem.h"
#import "THVolumeAutomation.h"
#import "THTransitionComposition.h"
#import "THTransitionInstructions.h"
#import "THFunctions.h"
@interface THTransitionCompositionBuilder ()
@property (strong, nonatomic) THTimeline *timeline;
@property (strong, nonatomic) AVMutableComposition *composition;
@property (weak, nonatomic) AVMutableCompositionTrack *musicTrack;
@end
@implementation THTransitionCompositionBuilder
- (id)initWithTimeline:(THTimeline *)timeline {
self = [super init];
if (self) {
_timeline = timeline;
}
return self;
}
- (id )buildComposition {
self.composition = [AVMutableComposition composition];
[self buildCompositionTracks];
AVVideoComposition *videoComposition = [self buildVideoComposition];
AVAudioMix *audioMix = [self buildAudioMix];
return [[THTransitionComposition alloc] initWithComposition:self.composition
videoComposition:videoComposition
audioMix:audioMix];
}
- (void)buildCompositionTracks {
// To be implemented
}
- (AVVideoComposition *)buildVideoComposition {
// To be implemented
return nil;
}
@end
buildComposition方法的实现看起来与之前章节创建的实现类似,不过创建轨道的代码已经提取到专门的创建方法中,并添加了一个新方法buildVideoComposition,稍后会实现它们。buildAudioMix方法的实现被略去了,因为它和上一章的内容完全一致。
现在实现buildCompositionTracks方法来提供视频过渡所需的轨道和剪辑布局,如代码清单11-5所示。
代码清单11-5实现 buildCompositionTracks方法
- (void)buildCompositionTracks {
CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;
AVMutableCompositionTrack *compositionTrackA = // 1
[self.composition addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:trackID];
AVMutableCompositionTrack *compositionTrackB =
[self.composition addMutableTrackWithMediaType:AVMediaTypeVideo
preferredTrackID:trackID];
NSArray *videoTracks = @[compositionTrackA, compositionTrackB];
CMTime cursorTime = kCMTimeZero;
CMTime transitionDuration = kCMTimeZero;
if (!THIsEmpty(self.timeline.transitions)) { // 2
// 1 second transition duration
transitionDuration = THDefaultTransitionDuration;
}
NSArray *videos = self.timeline.videos;
for (NSUInteger i = 0; i < videos.count; i++) {
NSUInteger trackIndex = i % 2; // 3
THVideoItem *item = videos[I];
AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
AVAssetTrack *assetTrack =
[[item.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
[currentTrack insertTimeRange:item.timeRange
ofTrack:assetTrack
atTime:cursorTime error:nil];
// Overlap clips by transition duration // 4
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
}
// Add voice overs // 5
[self addCompositionTrackOfType:AVMediaTypeAudio
withMediaItems:self.timeline.voiceOvers];
// Add music track
NSArray *musicItems = self.timeline.musicItems;
self.musicTrack = [self addCompositionTrackOfType:AVMediaTypeAudio
withMediaItems:musicItems];
}
(1)首先创建两个新的AVMutableCompositionTrack对象,二者都是AVMediaTypeVideo类型,提供所需的A_B轨道排列。将它们保存到videoTracks数组,当需要向组合中插入视频时会遍历该集合并取出它们。
(2)判断THTimeline对象的transitions数组是否被填充。如果视频过渡在用户界面中被设置为激活,则填充描述过渡效果的THVideoTransition对象集合。如果过渡被激活,设置transitionDuration为THDefaultTransitionDuration,这是一个全局常量,是1秒的CMTime值。
(3)遍历准备添加到组合中的视频集合,首先确定剪辑应该插入的目标轨道。使用i % 2 的算法计算trackIndex值,得到的0,1,0,1 模式确保视频片段的排列为相应的A-B模式。
(4)与之前一样计算cursorTime,不过这里使用CMTimeSubtract函数重新定义它的值,回退插入点时将transitionDuration考虑在内。这样轨道的布局就提供了所需的过渡区域。
(5)调用前一章中编写的addCompositionTrackOfType: withMedialtems:方法来添加画外音和音乐轨道。出于简洁性的考虑,本章没有给出这个方法的具体实现。
建议现在运行应用程序,这样就可以看到这个新组合排列的结果。compositionTrackA中包含的视频将会照常展现,不过compositionTrackB中包含的视频则不可见。这是因为我们之前提到的当使用多视频轨道时会出现的z-索引行为。要修复这个行为,需要创建一个AVVideoComposition,组合呈现或导出时可以对使用的组合进行控制。首先创建buildVideoComposition方法,如代码清单11-6所示。
代码清单11-6实现 buildVideoComposition方法
- (AVVideoComposition *)buildVideoComposition {
AVVideoComposition *videoComposition = // 1
[AVMutableVideoComposition
videoCompositionWithPropertiesOfAsset:self.composition];
NSArray *transitionInstructions = // 2
[self transitionInstructionsInVideoComposition:videoComposition];
for (THTransitionInstructions *instructions in transitionInstructions) {
CMTimeRange timeRange = // 3
instructions.compositionInstruction.timeRange;
AVMutableVideoCompositionLayerInstruction *fromLayer =
instructions.fromLayerInstruction;
AVMutableVideoCompositionLayerInstruction *toLayer =
instructions.toLayerInstruction;
THVideoTransitionType type = instructions.transition.type;
// Apply Video Transition Effects // 4
if (type == THVideoTransitionTypeDissolve) {
// Listing 11.7
} else if (type == THVideoTransitionTypePush) {
// Listing 11.8
} else if (type == THVideoTransitionTypeWipe) {
// Listing 11.9
}
instructions.compositionInstruction.layerInstructions = @[fromLayer, // 5
toLayer];
}
return videoComposition;
}
- (NSArray *)transitionInstructionsInVideoComposition:(AVVideoComposition *)vc {
// To be implemented
return nil;
}
(1)首先使用videoCompositionWithPropertiesOfAsset方法创建一个新的AVVideoComposition实例。这个方法自动创建所需的组合对象和层指令,并设置renderSize、renderScale和frameDuration属性为相应的值。
(2)调用私有方法transitionInstructionsInVideoComposition:从AVVideoComposition中提取相关的层指令,这样就可以应用期望的视频过渡效果。这个方法返回一个THTransitionInstructions对象数组,这些对象保存着基础数据,用于简化组合和层指令的相关工作。稍后会给出这个方法的实现并创建这些对象。
(3)为从THTransitionInstructions实例中 得到的数据定义本地变量。在应用过渡效果时会用到这些变量。
(4)定义一个占位符条件语句来判断所执行的视频转换。应用程序的用户界面允许在剪辑之间使用溶解、推入或擦除过渡效果。代码清单11-7到代码清单11-9给出了这些效果的创建过程。
(5)配置组合指令的layerInstructions,按显示的顺序传递指令。在确保正确应用视频过渡时,这个顺序非常必要。
下一步需要具体实现transitionInstructionsInVideoComposition:方法,如代码清单11-7所示。这个方法负责将组合和层指令从之前创建的AVVideoCompoition中提取出来,并将它们与用户在时间轴上配置的THVideoTransition相关联。THVideoTransition定 义了需要应用的过渡类型。
代码清单11-7提取过渡指令
- (NSArray *)transitionInstructionsInVideoComposition:(AVVideoComposition *)vc {
NSMutableArray *transitionInstructions = [NSMutableArray array];
int layerInstructionIndex = 1;
NSArray *compositionInstructions = vc.instructions; // 1
for (AVMutableVideoCompositionInstruction *vci in compositionInstructions) {
if (vci.layerInstructions.count == 2) { // 2
THTransitionInstructions *instructions =
[[THTransitionInstructions alloc] init];
instructions.compositionInstruction = vci;
instructions.fromLayerInstruction = // 3
(AVMutableVideoCompositionLayerInstruction *)vci.layerInstructions[1 - layerInstructionIndex];
instructions.toLayerInstruction =
(AVMutableVideoCompositionLayerInstruction *)vci.layerInstructions[layerInstructionIndex];
[transitionInstructions addObject:instructions];
layerInstructionIndex = layerInstructionIndex == 1 ? 0 : 1;
}
}
NSArray *transitions = self.timeline.transitions;
// Transitions are disabled
if (THIsEmpty(transitions)) { // 4
return transitionInstructions;
}
NSAssert(transitionInstructions.count == transitions.count,
@"Instruction count and transition count do not match.");
for (NSUInteger i = 0; i < transitionInstructions.count; i++) { // 5
THTransitionInstructions *tis = transitionInstructions[I];
tis.transition = self.timeline.transitions[I];
}
return transitionInstructions;
}
(1)首先遍历从AVVideoComposition中得到的AVVideoCompositionInstruction对象。
(2)我们只关心包含两个层指令的组合指令,这表示这个指令定义了组合中的过渡区域。
(3) 自动创建的层指令通常保存于layerInstructions数组,第一个轨道的层指令作为数组的第一个元素,接下来是第二个轨道的层指令。代码进行了一些计算来确保过渡之前的层和过渡之后的层保持不变。很容易理解在创建过渡时需要对每个层都应用选定的效果。
(4)如果transitions数组是空的,则返回transitionInstructions, 因为这表示用户界面禁用了过渡。
(5)如果过渡已启用,则遍历transitionInstructions, 并将用户选定的THVideoTransition对象和它进行关联。THVideoTransition对象定 义了所应用的过渡类型,比如推入、溶解或擦除等。
应用过渡效果
应用程序支持3种过渡类型:溶解(默认)、推入和擦除。下面看下如何实现每种效果,先从溶解过渡开始。
1.溶解过渡效果
第一个需要实现的过渡效果是溶解过渡。这个过渡效果需要修改输入层的模糊度来获得两个层逐渐融合的效果,如图11-8所示。
代码清单11-8是这个方法具体的实现。
代码清单11-8应用溶解过渡效果
if (type == THVideoTransitionTypeDissolve) {
[fromLayer setopacityRampFromStartopacity:1.0
toEndOpacity:0.0
timeRange:timeRange];
}
实现一个简单溶解效果很容易。只需对fromLayer对象设置一个模糊渐变,在过渡时间内将模糊值默认的1.0(完全模糊)调整到0.0(完全透明)。这样就将当前视频以溶解的效果切换到下一个视频。另一个溶解过渡的形式是交叉溶解,对toLayer设置一个模糊渐变,将模糊值从0.0变为1.0。简单模糊效果更加细腻且通常效果很好,不过我们鼓励开发者尝试两种方式的溶解来选择自己喜欢的效果。
2.推入过渡效果
下一个要实现的过渡是推入过渡,如图11-9所示。 这是一个有方向的过渡,即下一段视频将当前视频推出视图区域。
代码清单11-9给出了向左推动效果的代码。
代码清单11-9应用推 入过渡效果
if (type == THVideoTransitionTypePush) {
// Define starting and ending transforms // 1
CGAffineTransform identityTransform = CGAffineTransformIdentity;
CGFloat videoWidth = videoComposition.renderSize.width;
CGAffineTransform fromDestTransform =
CGAffineTransformMakeTranslation(-videoWidth, 0.0);
CGAffineTransform toStartTransform =
CGAffineTransformMakeTranslation(videoWidth, 0.0);
[fromLayer setTransformRampFromStartTransform:identityTransform // 2
toEndTransform:fromDestTransform
timeRange:timeRange];
[toLayer setTransformRampFromStartTransform:toStartTransform // 3
toEndTransform:identityTransform
timeRange:timeRange];
}
(1)首先定义一个对于输入视频层的变换。CGAffineTransform可 以修改层的转化、旋转和缩放。对层应用一个渐变的变化可以衍生出许多有趣的效果。这里我们使用转化(translation)转换,修改图层的x、y坐标。fromDestTransform将fromL ayer移到左侧,使用视频宽度的负值,使对象完全移出视图。定义一个toStartTransform,将toLayer的起始位置向右移出视图。
(2)设置fromLayer的渐变效果,初始变换设置为identity Transform(未变换的状态),终点变换设置为fromDestTransform。这样就会在timeRange时间内实现fromLayer向左侧退出的动画效果。
(3)采用类似的方式在toLayer上设置-一个渐变效果,初始变换设置为toStartTransform,终点变换设置为identityTransform。这样就会在timeRange时间内实现toLayer从右侧进入的动画效果。
3.擦除过渡效果
最后一个要实现的过渡是擦除效果(如图11-10所示)。擦除过渡有多种形式,不过通常都以一种动画方式将当前视频移出,显示下一个视频。
代码清单11-10给出了实现简单向上擦除的实现代码,fromLayer向 上划过,显示toLayer层。
代码清单11-10应用擦除过渡效果
else (type == THVideoTransitionTypeWipe) {
CGFloat videoWidth = videoComposition.renderSize.width;
CGFloat videoHeight = videoComposition.renderSize.height;
CGRect startRect = CGRectMake(0.0f, 0.0f, videoWidth, videoHeight);
CGRect endRect = CGRectMake(0.0f, videoHeight, videoWidth, 0.0f);
[fromLayer setCropRectangleRampFromStartCropRectangle:startRect
toEndCropRectangle:endRect
timeRange:timeRange];
}
要实现这个效果,需要从视频组合的renderSize中获取宽和高。这些值用于创建擦除动画效果的开始和结束CGRect值。初始矩形为最大的宽度和高度,最终矩形在高度上有所削减,在fromLayer上生成一个向上擦除的效果。
运行应用程序并点击Settings菜单,可以看到一个新的Video区域出现在菜单中,并带有视频过渡效果的切换开关。激活这个开关可以看到视频轨道更新为一个 可见的过渡按钮,如图11-11所示。
点击这些按钮,尝试这些过渡类型并观察它们的实际效果。
11.4 小结
本章学习了如何使用AVVideoComposition、AVVideoCompositionInstruction和AVVideoCompositionL ayerInstruction对象创建视频过渡。深入了解了如何创建一个视频组合并学习了一些规避常 见陷阱的方法。学习创建视频过度的过程具有一定的挑战性,不过现在你已经克服重重困难,对创建过渡效果的过程有了比较深入的了解,这样会帮助开发者更好地使用框架的各种特性来优化视频编辑应用程序。