两个独立的视频拼接起来以后很有可能会出现衔接处过于生硬的问题,此时就需要给视频添加过渡效果,这一效果需要用到 AVVideoComposition 及其子类 AVMutableVideoComposition。
AVMutableVideoComposition 是过渡效果实现的核心,它能够表示多个视频轨道的合并,同时表达合并的方式,也就是过渡效果,同时提供了配置视频组合的渲染尺寸、缩放、帧时长等。AVMutableVideoComposition 由一组 AVMutableVideoCompositionInstruction 组成,AVMutableVideoCompositionInstruction 定义了时间范围信息,以及每一帧的层级,也就是 AVMutableVideoCompositionLayerInstruction,AVMutableVideoCompositionLayerInstruction 用于真正实现各类模糊、变形和裁剪效果。
总结一下就是:
- AVMutableVideoCompositionLayerInstruction 负责执行具体的过渡动画
- AVMutableVideoCompositionInstruction 负责管理过渡动画在何时执行
- AVMutableVideoComposition 代表最终修改过的视频组合对象
而 AVMutableVideoComposition 就可以被提供给 AVPlayerItem、AVAssetExportSession、AVAssetReaderVideoCompositionOutput 和 AVAssetImageGenerator 使用了,但是要注意的是,与 AVAudioMix 类似,AVMutableVideoComposition 并不能与 AVComposition 关联,这一点导致在编辑和传递 AVMutableVideoComposition 过程中,需要时时考虑附带 AVMutableVideoComposition 参数。
AVVideoComposition 与 AVComposition 没有关系。
实现过渡效果的基本步骤可以分为
- 合并视频和音频,生成多视频轨道的 AVMutableComposition
- 对视频过渡区域,生成过渡动画
- 组装 AVMutableVideoComposition,提供给 AVPlayerItem 或 AVAssetExportSession 使用
1. 合并媒体
由于 AVMutableVideoCompositionLayerInstruction 是与视频轨道绑定的,因此在处理过渡效果时,需要在不同轨道之间处理,常见的方式是交错放置多个视频,形成如下形式的视频布局
段1 | 段2 | 段3 |
---|---|---|
视频A | 视频C | |
视频B |
可以用一个视频轨道数组来表达多个轨道。同时,为了实现过渡效果,两个相邻的视频,如视频 A 和 B 之间,应当在时间轴上有重叠区域,因此需要对时间轴进行如下区分
视频 A | AB 过渡区 | 视频 B | BC 过渡区 | 视频 C |
---|
这样的划分也需要记录下来,所以最终合并媒体的步骤如下
1.1 初始化相关对象
AVMutableComposition *composition = [AVMutableComposition composition];
__block CMTime cursor = kCMTimeZero;
CMTime transitionTime = CMTimeMake(2, 1); // 过渡时间
NSMutableArray *passRanges = [NSMutableArray array];// 视频独立区时间数组
NSMutableArray *transitionRanges = [NSMutableArray array]; // 过渡区时间数组
AVMutableCompositionTrack *videoCompositionTrackA = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *videoCompositionTrackB = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
NSArray *videoTracks = @[videoCompositionTrackA, videoCompositionTrackB]; // 生成 AB 轨道
1.2 遍历资源
遍历资源过程中,首先需要将视频轨道和音频轨道加入到 AVMutableComposition 中,其次需要更独立区时间数组和过渡区时间数组
// 视频轨道
AVMutableCompositionTrack *videoCompositionTrack = videoTracks[idx % 2];
AVAssetTrack *videoTrack = [[targetAsset tracksWithMediaType:AVMediaTypeVideo] firstObject];
[videoCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, targetAsset.duration) ofTrack:videoTrack atTime:cursor error:nil];
// 音频轨道
AVMutableCompositionTrack *audioCompositionTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
AVAssetTrack *audioTracck = [[targetAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
[audioCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, targetAsset.duration) ofTrack:audioTracck atTime:cursor error:nil];
CMTimeRange timeRange = CMTimeRangeMake(cursor, targetAsset.duration);
// 去除每一个视频的头部过渡区
if (idx > 0) { // 第一个视频只需要裁剪尾部过渡区,不需要裁剪头部过渡区
timeRange.start = CMTimeAdd(timeRange.start, transitionTime);
timeRange.duration = CMTimeSubtract(timeRange.duration, transitionTime);
}
// 去除每一个视频的尾部过渡区
if (idx + 1 < mediaAssets.count) { // 末尾视频没有尾部过渡区,其他视频还需要去除尾部过渡区
timeRange.duration = CMTimeSubtract(timeRange.duration, transitionTime);
}
[passRanges addObject:[NSValue valueWithCMTimeRange:timeRange]];
cursor = CMTimeAdd(cursor, targetAsset.duration);
cursor = CMTimeSubtract(cursor, transitionTime);
if (idx + 1 < mediaAssets.count) { // 末尾一个视频没有尾部过渡区
timeRange = CMTimeRangeMake(cursor, transitionTime);
[transitionRanges addObject:[NSValue valueWithCMTimeRange:timeRange]];
}
这里我们将视频错开放入了两个视频轨道里,要注意由于视频轨道内具有 z 索引行为,因此目前是不能播放多个视频轨道的。
2. 过渡动画
现在我们有了两个视频轨道,一个表示独立区的时间数组,一个表示过渡区的时间数组,接下来需要在每一个过渡区里定义具体的过渡动画,并将所有
NSMutableArray *compositionInstructions = [NSMutableArray array];
NSArray *tracks = [composition tracksWithMediaType:AVMediaTypeVideo];
[passRanges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSUInteger trackIndex = idx % 2;
AVMutableCompositionTrack *currentTrack = tracks[trackIndex]; // 取出对应轨道
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
instruction.timeRange = [obj CMTimeRangeValue];// 取出独立分区的 duration
AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:currentTrack];// 取出当前轨道的 layerInstruction
instruction.layerInstructions = @[layerInstruction];
[compositionInstructions addObject:instruction];// 将 AVMutableVideoCompositionInstruction 加入到数组里
if (idx < transitionRanges.count) { // 过渡区处理
AVCompositionTrack *foregroundTrack = tracks[trackIndex];//当前的track
AVCompositionTrack *backgroundTrack = tracks[1 - trackIndex];// 下一个 track
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
instruction.timeRange = [transitionRanges[idx] CMTimeRangeValue];
AVMutableVideoCompositionLayerInstruction *frontLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:foregroundTrack];// 取出当前轨道的 layerInstruction
AVMutableVideoCompositionLayerInstruction *backLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:backgroundTrack];// 取出下一个轨道的 layerInstruction
// 实际过渡动画的定义
instruction.layerInstructions = @[frontLayerInstruction, backLayerInstruction];
[compositionInstructions addObject:instruction];
}
}];
要注意,compositionInstructions 数组必须按顺序组装 AVMutableVideoCompositionInstruction 对象。
AVMutableVideoCompositionLayerInstruction 本身支持三种过渡动画效果
- opacity 透明度变化、溶解效果
[frontLayerInstruction setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:[transitionRanges[idx] CMTimeRangeValue]];
[backLayerInstruction setOpacityRampFromStartOpacity:0.0 toEndOpacity:1.0 timeRange:[transitionRanges[idx] CMTimeRangeValue]];
- Transform 矩阵变换、推入效果
CGAffineTransform identityTransform = CGAffineTransformIdentity;
CGFloat videoWidth = 1280.f;
CGAffineTransform from = CGAffineTransformMakeTranslation(-videoWidth, 0);
CGAffineTransform to = CGAffineTransformMakeTranslation(videoWidth, 0.0);
[frontLayerInstruction setTransformRampFromStartTransform:identityTransform toEndTransform:from timeRange:[transitionRanges[idx] CMTimeRangeValue]];
[backLayerInstruction setTransformRampFromStartTransform:to toEndTransform:identityTransform timeRange:[transitionRanges[idx] CMTimeRangeValue]];
- CropRectangle 裁剪区域、擦除效果
CGFloat videoWidth = 1280.f;
CGFloat videoHeight = 720.f;
CGRect startRect = CGRectMake(0.0f, 0.0f, videoWidth, videoHeight);
CGRect endRect = CGRectMake(0.0f, 0.0f, videoWidth, 0.0f);
[frontLayerInstruction setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:[transitionRanges[idx] CMTimeRangeValue]];
组装媒体
获得了装有 AVMutableVideoCompositionInstruction 的 compositionInstructions 数组后,就可以组装 AVMutableVideoComposition 了
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.instructions = [compositionInstructions copy];
videoComposition.renderSize = CGSizeMake(1280.f, 720.f);
videoComposition.frameDuration = CMTimeMake(1, 30);
videoComposition.renderScale = 1.0;
这里定义了四个主要属性
- instructions 属性用于设置所有组合指令,也就是 AVMutableVideoCompositionInstruction
- renderSize 定义渲染尺寸
- frameDuration 定义有效帧率,30 FPS 的帧率对应 frameDuration 为 1/30
- renderScale 定义视频组合的缩放值
当然还可以用快捷方式来获取一个 AVMutableVideoComposition
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:composition];
这个方法所配置的属性如下所示
- instructions 属性包含一组完整的基于组合视频轨道的组合和层指令
- renderSize 被设置为 AVComposition 的 naturalSize,如果没有,则设置为能够满足最大视频维度的尺寸值
- frameDuration 设置为组合视频轨道的最大 nominalFrameRate 的值,如果都为 0 则设置为 1/30
- renderScale 设置为 1.0
生成了 AVMutableVideoComposition 以后就可以直接用于播放或导出了。
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:[composition copy]];
item.videoComposition = videoComposition;