AVFoundation框架(七) 媒体数据的编辑 - 创建和编
AVFoundation 提供了一些API来创建 自由组合,修剪,重新排序无损的媒体编辑工具. 下面逐条介绍.
1. 自由组合媒体
有时我们需要把一些独立的视频镜头组合在一起再配上合适的音乐成为一个新的视频.
要完成这个首先需要把相关片段从各源媒体中取出, 再组合成一个临时排列. AVFoundation定义了这个功能,可以简单的将多个音视频资源组合成一个新资源. 下面介绍相关核心类结构.
AVComposition
合成文件时,我们是先需要从资源获取文件。虽然是操作AVAsset里面的东西,但是我们实际上使用的并不是AVAsset。而是使用一个它的子类AVComposition
,AVComposition
在AVAsset上为我们提供了更加强大的服务,它相当于包含了一个或多个给定类型的媒体轨道(AVCompositionTrack
)的容器,。如MP4有一个轨道用来盛放声音,也有一个用来盛放视频(当然还有其他复杂的)。而一个CompositionTrack本身是由一个或多个媒体片段(AVCompositionTrackSegment
)组成. 参考上图结构.
AVComposition扩展了AVAsset,所以常见的资源场景都可以使用这个对象. 但是AVComposition 更像是一组抽象说明,是描述多个资源建如何正确的呈现和处理, 它是轻量级的临时对象. 因此不需要保存; 如果你需要创建一个具有保存项目文件能力的音视频编辑应用, 就需要自定义数据模型来保存这个状态.
AVComposition
和AVCompositionTrack
都是不可变对象, 提供对资源的只读操作, 以及一些简单的接口来播放或处理媒体资源. 因此,当要创建自己的组合时, 就需要使用它们的可变对象AVMutableComposition
和AVMutableCompositionTrack
. 这些对象提供的接口操作Track和Segment.这样我们就能创建自定义的临时排列了.
创建自定义组合,还需要指定添加到Composition的时间范围,指定添加Segment的每个Track插入点位置信息.
CMTimeRange 它是有关资源编辑中重要角色,作为时间范围类型,由两个CMTime值创建组成.前面章节介绍过, CMTime是分数形式时间类型,可以表示时间点和持续时间.精度比double类型更准确. 具体使用方法查看它头文件.
1.1 基本流程.
- 创建AVAsset :
创建一个组合资源需要一个或多个可以等待处理AVAsset对象. 之前我们都是使用assetWithURL:
方法来直接创建资源实例.
而其实要创建资源用于Composition时, 应该使用URLAssetWithURL:options:
方法创建的AVURLAsset
对象(AVAsset的子类). options参数允许我们自定义资源初始化的方式.这样才方便我们创建适合编辑状态的资源对象.
// 0. 创建等待处理状态的AVAsset. (需设置AVURLAssetPreferPreferPreciseDurationAndTimingKey 为YES,可以确保当资源的属性异步载入时可以计算出准确的时间信息. )
NSDictionary *options = @{AVURLAssetPreferPreferPreciseDurationAndTimingKey:@YES};
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options: options];
AVAsset *asset2 .......
-
创建组合资源Composition容器.
这里需要添加其对应的CompositionTrack,且要指定类型. preferredTrackID只是轨道标识符.一般用kCMPersistentTrackID_Invalid常量.
composing-and-editing-media-with-av-foundation-20-638.jpg
// 1. 创建Composition 并添加资源轨道对象.
AVMutableComposition *composition = [AVMutableComposition Composition];
AVMutableCompositionTrack *videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
- 核心部分:将独立片段插入Composition的轨道中.
// 插入的时间点. 即插入光标点.
CMTime cursorTime = kCMTimeZero;
// 插入的时间段.这里是捕捉每个视频前5秒.
CMTime videoDuration = CMTime(5,1);
CMTimeRange videoTimeRange = CMTimeRangeMake(kCMTimeZero, videoDuration);
// 提取和插入资源轨道的Segment. 资源轨道是从前面创建的资源中提取的.
AVAssetTrack *assetTrack = [[asset tracksWithMediaTypeVideo] firstObject];
[videoTrack insertTimeRange:VideoTimeRange ofTrack:assetTrack atTime:cursorTime error:nil];
- 最后,导出组合后的资源.
// 1. 创建一个composition的可导出版本,并设置输出URL和输出文件类型.
NSString *preset = AVAssetExportPresetHighestQuality;
self.exportSession.status = [AVAssetExportSession exportSessionWithAsset:[self.composition copy]
presetName:preset];
self.exportSession.status.outputURL = xxxx;
self.exportSession.status.outputFileType = AVFileTypeMPEG4;
// 2.开始导出过程.
[self.exportSession.status exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(),^{
AVAssetExportSessionStatus status = self.exportSession.status;
if (status == AVAssetExportSessionStatusCompleted) {
// 导出完成时将视频文件写入AssetLibrary.
self.progress = self.exportSession.progress;
[self monitorExportProgress];
} else {
// error
}
});
}];
-(void)writeExportedVideoToAssetsLibrary {
NSURL *exportURL = self.exportSession.outputURL;
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:exportURL]) {
[library writeVideoAtPathToSavedPhotosAlbum:exportURL
completionBlock:^(NSURL *assetURL,
NSError *error) {
if (error) {
NSString *message = @"Unable to write to Photos library.";
NSLog(@"Write Failed%@",message);
}
[[NSFileManager defaultManager] removeItemAtURL:exportURL
error:nil];
}];
} else {
NSLog(@"Video could not be exported to assets library.");
}
self.exportSession = nil;
}
2. 组合媒体中的问题
2.1 混合音频的音量
上一节其实包含了音频的混合,但是实际应用中有一点小问题.组合后音频轨道在刚开始播放时音量就很大,并在组合资源结束时又突然停止.如果可以进行渐进变化会带来更好的体验. 另外就是画外音和音乐轨道同时播放时,会几乎听不到画外音.因此要进行一种闪避处理(在画外音持续期间将音乐调低). 框架提供了一个AVAudioMix来解决上面的问题.
AVAudioMix 用来在组合音频轨道中进行自定义音频的处理.结构如下图.
- 调节音量.
在音频混合问题上,最核心的处理就是调整 音频CompositionTrack 的音量. 当一个Composition播放或导出时,默认是以最大或正常音量播放音频轨道. 当只有一个单音轨道时,这样不会出现问题,但是当一个组合资源包含多个音频源时,会出现争夺,一些声音就无法听到.可以通过AVMutableAudioMixInputParameters
实例在一个指定的时间点或时间段(平滑)自动调节音量.
AVCompositionTrack *track = // audio track in composition
// 创建Parameters
AVMutableAudioMixInputParameters *parmeters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:track];
// 在时间点调节音量
[parmeters setVolume:0.5f atTime:kCMTimeZero];
// 在时间段调节.
CMTimeRange range = CMTimeRangeFromeTimeToTime(CMTimeMake(2,1),CMTimeMake(4,1));
[parmeters setVolumeRampFromStartVolume:0.5f toEndVolume:0.8f timeRange: range];
// 最后将参数赋予音频混合对象的inputParameters对象. (创建的audioMix可以根据需求设置为AVPlayerItem或AVAssetExportSession的audioMix属性进行播放或导出.)
AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];
audioMix.inputParameters = @[Parameters];
2.2 混合视频过渡效果
组合视频的两个场景快速切换,没有过渡.这样体验不是很好. 可以加一些渐隐,溶解,和擦除式的过渡效果.
这是AVFoundation最难得部分,主要是此功能API难以调试.先看下面几个创建视频过渡的类.
-
AVVideoComposition
框架中视频过渡的核心类. 他是对多个视频轨道组合(VideoCompositionTrack)在一起的方法的总体描述. 由一组时间方位和描述组合行为的介绍组成.除了包括视频层组合的信息之外,还提供了配置视频组合的渲染尺寸,缩放以及帧时长等属性.AVVideoComposition
的配置确定了委托对象处理AVComposition的呈现方式,这里的委托对象比如AVPlayer或AVAssetImageGenerator. -
AVVideoCompositionInstruction
AVVideoComposition
是由一组AVVideoCompositionInstruction
对象格式定义的指令组成的.这个对象所提供的关键数据是组合对象时间轴内的时间范围信息, 即某一组合形式出现的时间范围. -
AVVideoCompositionLayerInstruction
用于定义给定视频轨道应用的模糊,编写和裁剪效果. 它提供一些方法用于在特定的时间点或范围对这些值进行修改. 可以创建出动态的过渡效果.
与
AVAudioMix
类似.AVVideoComposition
并不直接和AVComposition
相关, 而是越类型AVPlayerItem的客户端相关联.
2.2.1过渡效果概念
要在剪辑间添加过渡,首先要将两个轨道间的视频片段重新部署.
-
先创建一个包含两个视频轨道的组合.结构如下:
WX20170502-152058@2x.png
AVMutableComposition *composition = [AVMutableComposition Composition];
AVMutableCompositionTrack *trackA = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *trackB = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
NSArray *videoTracks = @[trackA, trackB];
-
错开部署这两个轨道的视频布局. (下面每个片段都是AVCompositionTrackSegment实例,包括空片段,只是不包含任何媒体)
D0B6B288-A4E7-42C3-BE3F-9BB7AA918A41.png
NSArray *videoAssets = nil; // 从AVAsset资源载入的.
CMTime cursorTime = kCMTimeZero;
for (NSUInteger i = 0; i < videoAssets.count; i ++) {
NSUInteger trackIndex = i%2; // 计算出A-B组合模式目标轨道的索引号.
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);
}
- 上面一步有一个问题,就是虽然错开轨道,但是每个片段都是紧接着上一个片段结尾,这就是说我们在两个可见剪辑间没有任何空间,也就无法将他们组合在一起. 我们需要设定一个过渡时长.并计算出
AVVideoCompositionInstruction
对象的两个时间范围,第一个是pass-through timeRange,即通过时间范围,在这个时间范围希望一个轨道的所有帧序列都不与其他轨道进行混合. 第二个是transition时间范围. 它定义了组合中 视频片段重叠的区域.也是我们进行过渡的区域.
3D29BA7C-9BB6-4B65-B550-CC4BDD265F3A.png
NSArray *videoAssets = nil; // 从AVAsset资源载入的.
CMTime cursorTime = kCMTimeZero;
CMTime transDuration = CMTimeMake(2,1); // 设期望过渡时间2秒.
//
NSMutableArray *passThroughTimeRanges = [NSMutableArray array];
NSMutableArray *transitionTimeRanges = [NSMutableArray array];
NSUInteger videoCount = [videoAssets count];
for (NSUInteger i = 0; i < videoAssets.count; i ++) {
NSUInteger trackIndex = i%2; // 计算出A-B组合模式目标轨道的索引号.
AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
AVAsset *asset = videoAssets[i];
// 计算每个视频段的时间信息.
CMTimeRange timeRange = CMTimeRangeMake(cursorTime,asset.duration);
if (i>0) {
timeRange.start = CMTimeAdd(timeRange.start, transDuration);
timeRange.duration = CMTimeSubtract(timeRange.duration, transDuration);
}
if (i+1 <videoCount) {
timeRange.duration = CMTimeSubtract(timeRange.duration, transDuration);
}
[passThroughTimeRanges addObject:[NSValue valueWithCMTimeRange:timeRange]];
cursorTime = CMTimeAdd(cursorTime, timeRange.duration);
cursorTime = CMTimeSubtract(cursorTime,transDuration);
if (i+1 < videoCount) {
timeRange = CMTimeRangeMake (cursorTime, transDuration);
[transitionTimeRanges addObject:[NSValue valueWithCMTimeRange:timeRange]];
}
}
- 获得所需的pass和transition时间范围后,下一步就是创建组合指令
AVVideoCompositionInstruction
和AVVideoCompositionLayerInstruction
.
NSMutableArray *compositionInstructions = [NSMutableArray array];
// 找出组合中所有视频轨道.
NSArray *tracks = [composition tracksWithMediaType:AVMediaTypeVideo];
// 遍历之前计算的所有pass时间范围, 循环在两个需要创建 指令 的视频轨道间前后切换.
for (NSUInteger i = 0; i < passThroughTimeRanges.count; i++) {
NSUInteger trackIndex = i % 2;
AVMutableCompositionTrack *currentTrack = tracks[trackIndex];
// 创建pass时间范围相关compositionInstruction
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
instruction.timeRange = [passThroughTimeRanges[i] CMTimeRangeValue];
// pass的组合指令只需要一个与要呈现视频帧的轨道相关的单独 layerInstruction.
AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:currentTrack];
instruction.layerInstructions = @[layerInstruction];
[compositionInstructions addObject:instruction];
// 创建transition时间范围相关组合指令
if (i < transitionTimeRanges.count) {
AVCompositionTrack *foregroundTrack = tracks[trackIndex];
AVCompositionTrack *backgroungTrack = tracks[1-trackIndex];
AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
CMTimeRange timeRange = [transitionTimeRanges[i] CMTimeRangeValue];
instruction.timeRange = timeRange;
//transition 的组合指令需要两个相关视频帧轨道的layerInstruction,且顺序不能错.
AVMutableVideoCompositionLayerInstruction *fromLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:foregroundTrack];
AVMutableVideoCompositionLayerInstruction *toLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:backgroungTrack];
instruction.layerInstructions = @[fromLayerInstruction,toLayerInstruction];
[compositionInstructions addObject:instruction];
}
}
- 完成上面的准备,下面就可以进行
AVVideoComposition
实例的创建和配置了.
// 创建AVVideoComposition.并配置四个关键属性.
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.instructions = compositionInstructions; // 描述时间范围和执行组合种类
videoComposition.renderSize = CGSizeMake(1280.f, 720.f); // 定义应该被渲染的尺寸,应该对应视频原始大小.
videoComposition.frameDuration = CMTimeMake(1, 30); // 设置有效帧率. 30fps
videoComposition.renderScale = 1.0f; // 缩放.
AVVideoComposition的应用方式同AVAudioMix一样. 赋予AVPlayerItem即可.
AVPlayerItem *playerItem =
[AVPlayerItem playerItemWithAsset:[self.composition copy]];
playerItem.audioMix = self.audioMix;
playerItem.videoComposition = self.videoComposition;
2.2.2 快捷创建AVVideoComposition
其实很多情况下,有一种更简便创建AVVideoComposition
的方法,它有一个便捷的初始化方法videoCompositionWithPropertiesOfAsset:
,将AVComposition
作为资源参数. 但由于会自动配置四项关键属性. 因此可能无法满足我们的需要.
- instructions:包含一组完整基于
AVComposition
轨道(以及其中包含的片段空间布局)的组合指令和层指令. - renderSize: 被设置为
AVComposition
对象的naturalSize(如果这个没有设置,默认使用满足组合视频轨道中最大视频纬度尺寸). - frameDuration: 设置为组合视频轨道中最大nominalFrameRate值.(如果没有,那么默认30FPS);
- renderScale 始终为1.0;
2.2.3 过渡效果应用
实现过渡的常见3中类型. 具体是在创建AVVideoCompositionLayerInstruction
过程中,对涉及到的前后两个视频轨道片段fromLayerInstruction
和toLayerInstruction
的自定义设置.
- 溶解: 通过修改输入层的模糊度来获得两个layer逐渐融合的效果.
[fromLayerInstruction setOpacityRampFromStartOpacity:1.0
toEndOpacity:0.0
timeRange:timeRange];
- 推入: 下一段视频有方向的将当前视频推出视频区域.
CGAffineTransform identityTransform = CGAffineTransformIdentity; // 定义一个layer的变换. CGAffineTransform可以修改层的转化,旋转和缩放. 这里我们使用转化(transition)转换.
CGFloat videoWidth = videoComposition.renderSize.width;
// 设置fromLayer的渐变效果.
CGAffineTransform fromDestTransform =
CGAffineTransformMakeTranslation(-videoWidth, 0.0);
CGAffineTransform toStartTransform =
CGAffineTransformMakeTranslation(videoWidth, 0.0);
// 设置前后两个视频轨道的图层变化.
[fromLayer setTransformRampFromStartTransform:identityTransform
toEndTransform:fromDestTransform
timeRange:timeRange];
[toLayer setTransformRampFromStartTransform:toStartTransform
toEndTransform:identityTransform
timeRange:timeRange];
- 向上擦除效果:
CGFloat videoWidth = videoComposition.renderSize.width;
CGFloat videoHeight = videoComposition.renderSize.Height;
//
CGFloat startRect = CGRectMake(0,0,videoWidth,videoHeight);
CGFloat endRect = CGRectMake(0,videoHeight,videoWidth,0);
//
[fromLayer setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:timeRange];
3. Core Animation在AVFoundation中的应用
目前为止所有的工作都是基于轨道模式.接下来我们要使用Core Animation框架创建一些动画效果并整合到视频中. (了解本节前先要学习核心动画的知识).
使用Core Animation给视频创建叠加效果的方法和创建动画效果的方法几乎一样.最大的区别在于运行动画的时间模型. 创建实时动画时,CAAnimation实例从系统主机时钟获取执行时间.而视频动画基于"影片时间"
AVFoundation 提供了一个CALayer的子类AVSynchronizedLayer
用于与给定的AVPlayerItem实例同步时间. 这个图层本身不展示内容,仅用来与图层子树协同时间, 这样一来,所有基础关系中依附于该图层的动画都可以从播放的AVPlayerItem实例中获取执行时间. 最后通过AVVideoCompositionCoreAnimationTool
导出并渲染 叠加了视频图层和动画图层的组合,生成最终的视频帧.
用一个淡入淡出动画集成示例
// 1. 创建动画素材图层
CALayer *parentLayer = [CALayer layer]; // 创建一个父图层,包含相应的图片,文本图层.设置opacity=0先不可见.
parentLayer.frame = self.bounds;
parentLayer.opacity = 0.0f;
CALayer *imageLayer = [self makeImageLayer]; // 自定义方式创建图层.可以无视.
[parentLayer addSublayer:imageLayer];
CALayer *textLayer = [self makeTextLayer]; // 自定义方式创建图层.可以无视.
[parentLayer addSublayer:textLayer];
// 2. 创建动画并添加
CAAnimation *fadeInFadeOutAnimation = [self makeFadeInFadeOutAnimation];
CAKeyframeAnimation *animation =
[CAKeyframeAnimation animationWithKeyPath:@"opacity"];
animation.values = @[@0.0f, @1.0, @1.0f, @0.0f];
animation.keyTimes = @[@0.0f, @0.25f, @0.75f, @1.0f];
// 注意1: 动画的beginTime和duration为视频相关时间.ps.如果你想一开始就启动动画,不要直接设置beginTime成为; 因为CoreAnimation会转换为CACurrentMediaTime(),而这个是系统主机时间. 同影片无关. 设置为 AVCoreAnimationBeginTimeAtZero.
animation.beginTime = CMTimeGetSeconds(self.startTimeInTimeline);
animation.duration = CMTimeGetSeconds(self.timeRange.duration);
// 注意2: 因为是随影片播放进行动画,所以要修改这个默认属性.否则动画就是一次性的.
animation.removedOnCompletion = NO;
[parentLayer addAnimation:fadeInFadeOutAnimation forKey:nil];
// 3. 同步,在视频中添加动画.
AVSynchronizedLayer *syncLayer =
[AVSynchronizedLayer synchronizedLayerWithPlayerItem:playerItem];
[syncLayer addSublayer:parentLayer];
// 4. 导出最终视频帧
CALayer *animationLayer = [CALayer layer]; // 将被渲染导出的最终帧图层.
animationLayer.frame = TH720pVideoRect;
CALayer *videoLayer = [CALayer layer]; // 初始的视频层
videoLayer.frame = TH720pVideoRect;
[animationLayer addSublayer:videoLayer];
[animationLayer addSublayer:parentLayer];
animationLayer.geometryFlipped = YES; // 确保正确渲染,否则图片和文本位置会颠倒.
// 将视频帧和动画图层整合到一起. 最后导出AVComposition即可.
AVVideoCompositionCoreAnimationTool *animationTool =
[AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:animationLayer];
至此,AVFoundation框架基础功能都介绍完毕了. 这个文集参考了《AVFoundation开发秘籍》 ,仅供学习使用. 具体代码地址:github.