AVFoundation开发秘籍笔记-11创建视频过渡效果
一、常用的类
1、AVVideoComposition
对两个或多个视频轨道组合在一起的方法给出了总体描述。由一组时间范围和描述组合行为的介绍内容组成,这些信息出现在组合资源内的任意时间点。
除了包含描述输入视频层组合的信息之外,还提供了配置视频组合的渲染尺寸、缩放和帧时长等属性。视频组合配置确定了委托对象处理时AVComposition
的呈现方式。这里的委托对象比如AVPlayer或AVAssetImageGenerator。
AVVideoComposition
并不是AVComposition
的子类,没有直接关联。在视频播放、淡出或处理时会用AVVideoComposition
来控制资源视频轨道的视频组合行为。
2、AVVideoCompositionInstruction
AVVideoComposition
是由一组AVVideoCompositionInstruction
对象格式定义的指令组成的。这个对象所提供的最关键的一段数据是组合对象时间轴内的时间范围信息,这一时间范围是在某一组合形式出现时的时间范文,要执行的组合特质是通过其layerInstructions
集合定义的。
3、AVVideoCompositionLayerInstruction
用于定义对给定视频轨道应用的模糊、变形和裁剪效果。
它提供了一些方法用于在特定的时间点火灾一个时间范围内对这些值进修修改。在一段时间内对这些值应用渐变操作可以让开发者创建出动态的过渡效果,比如溶解和渐淡效果。
与所有AVFoundation媒体编辑类一样,视频组合API具有不可变和可变两种形式。不可变超类形式使用于客户端对象,比如AVAssetExportSession,不过当创建自己的视频组合应用程序时,应该使用不可变子类。
AVVideoComposition并不直接和AVComposition相关。相反,这些对象和雷系AVPlayerItem的客户端相关联,在播放组合或进行其他处理时使用这些对象。
不将AVComposition与输出行为强耦合,可以再播放、导出或处理视频时更灵活地确定如何使用这些行为。
二、概念理解
1、部署视频布局
要在剪辑间添加过过渡,首先需要将两个轨道间的视频片段重新部署。大多数情况下两个轨道就足够,为满足一些特殊需求加入更多轨道也是可以的,同时添加过多的轨道会对性能产生负面影响。
// 创建新的可变组合,添加两个AVMediaTypeVideo类型的组合轨道
AVMutableComposition *composition = [AVMutableComposition composition];
AVMutableCompositionTrack *trackA = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *trackB = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
// 将组合轨道添加到一个NSArray中备用。
NSArray *videoTracks = @[trackA,trackB];
安排好所需轨道之后,需要错开部署两个轨道的视频布局(A-B交错模式)。当视频按照这种方式排列后,视频片段前面或后面的空间都会添加一个空片段。都是普通的AVCompositionTrackSegment实例,与视频一样,不过不包含任何媒体。
// 视频剪辑
NSArray *videoAssets ;
CMTime cursorTime = kCMTimeZero;
for (NSUInteger i = 0; i < videoAssets.count; i ++) {
//通过i%2来计算A-B模式下目标轨道的索引号
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中
[currentTrack insertTimeRange:timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, timeRange.duration);
}
这个时候已经采用交错方式排列这些轨道,但是每个片段都还紧接着上一篇段的片尾,两个可见剪辑间没有任何空间,还无法将他们组合在一起。
2、定义重叠区域
要在两个片段中应用视频过度,需要根据期望的过度持续时长来确定片段的重叠情况。
设置添加一个重叠时间来处理。
NSArray *videoAssets ;
CMTime cursorTime = kCMTimeZero;
// 定义一个2秒的CMTIme 作为期望的过渡持续时长。
CMTime transitionDuration = CMTimeMake(2, 1);
for (NSUInteger i = 0; i < videoAssets.count; i ++) {
//通过i%2来计算A-B模式下目标轨道的索引号
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中
[currentTrack insertTimeRange:timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, timeRange.duration);
// 减去过渡时长,过渡时长适度偏移下一个插入点
cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
}
此时的组合可以播放,但是视频轨道怒又内在的z索引行为,第一个视频轨道和第二个之前,第二个在第三个之前,一次类推。
在这种情况下播放组合,会发现第一、第二段视频什么都没有,最后会看到最前面轨道中所包含的第三个片段。在看到第二个轨道的内容前,需要定义时间范围并向组合方法解释这两个轨道应该如何组合。
创建重叠区域时,已经将视频时间轴减少了(videoCount-1)*transitionDuration的大小。如果组合中有额外的轨道,则需要将它们的时长相应的缩短以满足新的视频时间轴。
3、计算通过和过渡的时间范围
AVVideoComposition由一组AVVideoCompositionInstruction对象组成。其中最重要的数据是时间范围,用来表示某种出现的组合方式持续的时长。在开始创建AVVideoCompositionInstruction实例前,首先需要为组合对象计算一系列时间范围。
需要计算两个类型的时间范围。第一个通常被认为是通过(pass-through)时间范围,在这个时间范围内希望一个轨道的所有帧序列都在不与其他轨道进行混合的情况下通过某一区域。
第二个时间范围类型是过渡(transition)时间范围。定义了在组合中视频片段重叠的区域,并在时间轴上标记处应用过渡效果的区域。
可以通过多种方式计算这些时间范围,一种很好的通用方法。
NSArray *videoAssets ;
CMTime cursorTime = kCMTimeZero;
// 定义一个2秒的CMTIme 作为期望的过渡持续时长。
CMTime transitionDuration = CMTimeMake(2, 1);
NSMutableArray *passThroughTiemRanges = [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, transitionDuration);
timeRange.duration = CMTimeSubtract(timeRange.duration, transitionDuration);
}
if (i + 1 < videoCount) {
timeRange.duration = CMTimeSubtract(timeRange.duration, transitionDuration);
}
[passThroughTiemRanges addObject:[NSValue valueWithCMTimeRange:timeRange]];
cursorTime = CMTimeAdd(cursorTime, asset.duration);
cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
if (i + 1 < videoCount) {
timeRange = CMTimeRangeMake(cursorTime, transitionDuration);
NSValue *timeRangeValue = [NSValue valueWithCMTimeRange:timeRange];
[transitionTimeRanges addObject:timeRangeValue];
}
}
创建所需要的通过和过渡时间范围被认为是整个处理过程中最重要的一步操作,他并不是最难解决的问题,不过容易出错。进行这一步处理的时候,有两个关键点:
- 1、计算的时间范围必须没有任何空隙或重叠。必须是紧接上一个片段之后按需排列的最新时间范围。
- 2、计算必须考虑组合对象持续时间。如果组合中还包含额外的轨道,就需要使它们遵循目前的视频时间轴,或者根据他们的持续时长扩展最终的时间范围。
如果没有注意到这两点,组合对象仍可播放,但视频内容不会被渲染,只会显示黑屏。
Apple Developer Center中AVCompositionDebugViewer可以帮助呈现组合。
4、创建组合和层指令
创建AVVideoCompositionInstruction
和AVVideoCompositionLayerInstruction
示例,提供视频组合方法所执行的指令。
NSMutableArray *compositionInstructions = [NSMutableArray array];
//遍历所有的通过时间范围
for (NSUInteger i = 0; i > passThroughTiemRanges.count; i ++) {
// 循环在连个需要创建所需指令的视频轨道间前后切换。
NSInteger trackIndex = i % 2;
AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
// 创建AVMutableVideoCompositionInstruction实例,设置当前通过时间范围作为timeRange
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
instruction.timeRange = [passThroughTiemRanges[i] CMTimeRangeValue];
// 为活动组合创建一个新的AVMutableVideoCompositionLayerInstruction
AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:currentTrack];
// 添加到数组
instruction.layerInstructions = @[layerInstruction];
// 设置为组合指令的layerInstructions属性。
[compositionInstructions addObject:instruction];
//组合的通过时间范围区域只需要一个与要呈现视频帧的轨道相关的单独层指令。
if (i < transitionTimeRanges.count) {
//要创建过渡时间的指令,需要得到前一个轨道(过渡前的轨道)的引用和后一个轨道(过渡后的轨道)的引用
//确保轨道的引用始终顺序正确。
AVCompositionTrack *foregroundTrack = videoTracks[trackIndex];
AVCompositionTrack *backgroundTrack = videoTracks[1-trackIndex];
//创建一个AVMutableVideoCompositionInstruction实例
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
//设置当前过渡时间范围为它的timeRange属性。
CMTimeRange timeRange = [transitionTimeRanges[i] CMTimeRangeValue];
instruction.timeRange = timeRange;
// 为每个轨道创建一个AVMutableVideoCompositionLayerInstruction实例
// 在这些层指令上定义从一个场景到另一个场景的过渡效果
AVMutableVideoCompositionLayerInstruction *fromLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:foregroundTrack];
AVMutableVideoCompositionLayerInstruction *toLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:backgroundTrack];
// 将两个层指令添加到数组,并设置他们作为当前组合指令的layerInstructions属性
// 这一数组中的元素进行排序非常重要,因为它定义了组合输出中视频图层的z轴顺序。
instruction.layerInstructions = @[fromLayerInstruction,toLayerInstruction];
[compositionInstructions addObject:instruction];
}
}
5、创建和配置AVVideoComposition
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.instructions = compositionInstructions;
// 定义组合应该被渲染尺寸。对应于组合中视频原始大小720p-1280*720,1080p-1920*1080
videoComposition.renderSize = CGSizeMake(1280.f, 720.f);
// 设置有效帧率
videoComposition.frameDuration = CMTimeMake(1, 30);
// 视频组合应用的缩放
videoComposition.renderScale = 1.0f;
6、创建视频组合的捷径
// 创建一个基础的AVMutableVideoComposition
AVMutableVideoComposition *co = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:composition];
包含基础配置:
- instructions 属性包含一组完整的基于组合视频轨道(以及其中包含的片段空间布局)的组合和层指令
- renderSize 属性被设置为AVComposition对象的naturalSize,如果没有设置,则使用能够满足组合视频轨道中最大视频纬度的尺寸值。
- frameDuration 设置为组合视频轨道中最大nominalFrameRate的值。如果所有轨道的nominalFrameRate值都为0,则frameDuration设置层默认值1/30秒(30FPS)。
- renderScale 始终设置为1.0
三、过渡效果
1、溶解
[fromLayer setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:timeRange];
// 对fromLayer对象设置一个模糊渐变
2、推出
//定义个对于输入视频层的变换。
//CGAffineTransForm可以修改层的转化、旋转、缩放。对层应用一个渐变的变化可以衍生出很多效果。
CGAffineTransform identityTransform = CGAffineTransformIdentity;
CGFloat videoWidth = videoCompostion.renderSize.width;
// 这里通过转化(translation)转换,修改图层的x,y坐标。
CGAffineTransform fromDestTransform = CGAffineTransformMakeTranslation(-videoWidth, 0);
CGAffineTransform toStartTransform = CGAffineTransformMakeTranslation(videoWidth, 0);
// 设置fromLayer的渐变效果,初始变换设置为identityTransform,终点变换为fromDestTransform
[fromLayer setTransformRampFromStartTransform:identityTransform toEndTransform:fromDestTransform timeRange:timeRange];
[toLayer setTransformRampFromStartTransform:toStartTransform toEndTransform:identityTransform timeRange:timeRange];
3、擦除
CGFloat videoWidth = videoCompostion.renderSize.width;
CGFloat videoHeight = videoCompostion.renderSize.height;
CGRect startRect = CGRectMake(0, 0, videoWidth, videoHeight);
CGRect endRect = CGRectMake(0, videoHeight, videoWidth, 0);
[fromLayer setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:timeRange];
完整代码.m
#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 <THComposition>)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 {
CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;
// 创建两个AVMediaTypeVideo类型的轨道,提供所需的A-B轨道排列
AVMutableCompositionTrack *compositionTrackA = [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)) {
//判断transitions是否被填充,即视频过渡是否被设置为激活。
//如果激活过渡效果,设置常量值
transitionDuration = THDefaultTransitionDuration;
}
NSArray *videos = self.timeline.videos;
for (NSUInteger i = 0; i < videos.count; i ++) {
NSUInteger trackIndex = i%2;
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];
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
}
//添加画外音和音乐轨道
[self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:self.timeline.voiceOvers];
NSArray *musicItems = self.timeline.musicItems;
self.musicTrack = [self addCompositionTrackOfType:AVMediaTypeVideo withMediaItems:musicItems];
}
- (AVVideoComposition *)buildVideoComposition {
// 创建一个新的AVVideoComposition实例,自动创建所需的组合对象和层指令
// 设置renderSize,renderScale和frameDuration属性为相应的值。
AVVideoComposition *videoCompostion = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:self.composition];
// 从AVVideoComposition中提取相关的层指令,可以应用视频过渡效果。
// 返回一个THTransitionInstructions对象数组
NSArray *transitionInstructions = [self transitionInstructionsInVideoComposition:videoCompostion];
for (THTransitionInstructions *instructions in transitionInstructions) {
CMTimeRange timeRange = instructions.compositionInstruction.timeRange;
AVMutableVideoCompositionLayerInstruction *fromLayer = instructions.fromLayerInstruction;
AVMutableVideoCompositionLayerInstruction *toLayer = instructions.toLayerInstruction;
THVideoTransitionType type = instructions.transition.type;
if (type == THVideoTransitionTypeDissolve) { // 溶解过渡效果
[fromLayer setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:timeRange];
// 对fromLayer对象设置一个模糊渐变
} else if (type == THVideoTransitionTypePush) { //推入过渡效果
//定义个对于输入视频层的变换。
//CGAffineTransForm可以修改层的转化、旋转、缩放。对层应用一个渐变的变化可以衍生出很多效果。
CGAffineTransform identityTransform = CGAffineTransformIdentity;
CGFloat videoWidth = videoCompostion.renderSize.width;
// 这里通过转化(translation)转换,修改图层的x,y坐标。
CGAffineTransform fromDestTransform = CGAffineTransformMakeTranslation(-videoWidth, 0);
CGAffineTransform toStartTransform = CGAffineTransformMakeTranslation(videoWidth, 0);
// 设置fromLayer的渐变效果,初始变换设置为identityTransform,终点变换为fromDestTransform
[fromLayer setTransformRampFromStartTransform:identityTransform toEndTransform:fromDestTransform timeRange:timeRange];
[toLayer setTransformRampFromStartTransform:toStartTransform toEndTransform:identityTransform timeRange:timeRange];
} else if (type == THVideoTransitionTypeWipe) { //擦除过渡效果
CGFloat videoWidth = videoCompostion.renderSize.width;
CGFloat videoHeight = videoCompostion.renderSize.height;
CGRect startRect = CGRectMake(0, 0, videoWidth, videoHeight);
CGRect endRect = CGRectMake(0, videoHeight, videoWidth, 0);
[fromLayer setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:timeRange];
}
// 配合组合指令的layerInstructions,按显示的顺序传递指令,确保正确应用视频过渡效果
instructions.compositionInstruction.layerInstructions = @[fromLayer,toLayer];
}
return videoCompostion;
}
// Extract the composition and layer instructions out of the
// prebuilt AVVideoComposition. Make the association between the instructions
// and the THVideoTransition the user configured in the timeline.
- (NSArray *)transitionInstructionsInVideoComposition:(AVVideoComposition *)vc {
NSMutableArray *transitionInstructions = [NSMutableArray array];
int layerInstructionIndex = 1;
NSArray *compositionInstructions = vc.instructions;
// 遍历从AVVideoComposition中得到的AVVideoCompositionInstruction对象
for (AVMutableVideoCompositionInstruction *vci in compositionInstructions) {
//只关心包含两个层指令的组合指令,表示这个指令定义了组合中的过渡区域。
if (vci.layerInstructions.count == 2) {
THTransitionInstructions *instructions = [[THTransitionInstructions alloc] init];
instructions.compositionInstruction = vci;
// 自动创建的层指令通常保存于layerInstructions数组
//第一个轨道的层指令作为数组的第一个元素,接下来是第二个轨道的层指令
//
instructions.fromLayerInstruction = [vci.layerInstructions[1-layerInstructionIndex] mutableCopy];
instructions.toLayerInstruction = [vci.layerInstructions[layerInstructionIndex] mutableCopy];
[transitionInstructions addObject:instructions];
layerInstructionIndex = layerInstructionIndex == 1 ? 0:1;
}
}
NSArray *transitions = self.timeline.transitions;
if (THIsEmpty(transitions)) {
//禁用了过渡
return transitionInstructions;
}
// 如果过渡已启用,遍历transitionInstructions,并将用户选定的THVideoTransition对象和它进行关联。
for (NSUInteger i = 0; i < transitionInstructions.count; i ++) {
THTransitionInstructions *tis = transitionInstructions[i];
tis.transition = self.timeline.transitions[i];
}
return transitionInstructions;
}
- (AVMutableCompositionTrack *)addCompositionTrackOfType:(NSString *)mediaType
withMediaItems:(NSArray *)mediaItems {
AVMutableCompositionTrack *compositionTrack = nil;
if (!THIsEmpty(mediaItems)) {
compositionTrack =
[self.composition addMutableTrackWithMediaType:mediaType
preferredTrackID:kCMPersistentTrackID_Invalid];
CMTime cursorTime = kCMTimeZero;
for (THMediaItem *item in mediaItems) {
if (CMTIME_COMPARE_INLINE(item.startTimeInTimeline, !=, kCMTimeInvalid)) {
cursorTime = item.startTimeInTimeline;
}
AVAssetTrack *assetTrack = [[item.asset tracksWithMediaType:mediaType] firstObject];
[compositionTrack insertTimeRange:item.timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
// Move cursor to next item time
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
}
}
return compositionTrack;
}
- (AVAudioMix *)buildAudioMix {
NSArray *items = self.timeline.musicItems;
// Only one allowed
if (items.count == 1) {
THAudioItem *item = self.timeline.musicItems[0];
AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];
AVMutableAudioMixInputParameters *parameters =
[AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:self.musicTrack];
for (THVolumeAutomation *automation in item.volumeAutomation) {
[parameters setVolumeRampFromStartVolume:automation.startVolume
toEndVolume:automation.endVolume
timeRange:automation.timeRange];
}
audioMix.inputParameters = @[parameters];
return audioMix;
}
return nil;
}
@end