视频合成
AVMutableComposition
AVFoundation 框架为音视频编辑提供了功能丰富的类集;其中的关键是 composition ,它将不同的 asset 相结合并形成一个新的 asset ,composition 是一个或多个媒体资源的 track 的集合。AVMutableComposition 类提供了插入和删除 track, 以及管理其时间顺序的的接口。下图展示了如何通过已存在的 assets 组合成为一个 composition。如果你需要顺序合并多个 asset 到一个文件中,这就刚刚够用。但是如果要对 track 执行任何自定义的音视频处理操作,,那么你需要分别对音频和视频进行合并。
如上图所示,我们从本地加载两个媒体资源 AVAsset
;AVAsset
内各有三个轨道,其中两个为 video,一个为 audio。
基于以上的两个媒体资源,我们创建 AVMutableComposition
,用于作为我们的合成输出;分别指定 AVMutableCompositionTrack
为对应的AVAssetTrack
。
AVMutableAudioMix
使用 AVMutableAudioMix
类可以对 composition 中的 audio track 进行自定义操作。你还可以指定 audio track 的最大音量以及为其设置渐变效果。
AVMutableVideoComposition
使用 AVMutableVideoComposition
类可以直接处理视频 track。从一个 video composition 输出视频时,还可以指定输出的尺寸、缩放比例、以及帧率。通过 video composition 的指令 (instructions,AVMutableVideoCompositionInstruction),可以修改视频背景色,以及设置 layer 的 instructions。Layer 的 instructions(AVMutableVideoCompositionLayerInstruction)可以对 video track 实现渐变、渐变变换、透明度、透明度变换等效果。Video composition 还允许通过 animationTool 属性在视频中应用 Core Animation 框架的一些效果。
AVAssetExportSession
对音视频进行组合, 可以使用 AVAssetExportSession
。使用 composition 初始化一个 export session,然后分别其设置 audioMix 和 videoComposition 属性。
整体流程
下面这是一个简单的视频合成例子,可以让我们很直观的了解视频编辑的关键步骤:
- 获取视频资源
AVAsset
。 - 创建自定义合成对象
AVMutableComposition
。 - 创建视频组件
AVMutableVideoComposition
,这个类是处理视频中要编辑的东西。可以设定所需视频的大小、规模以及帧的持续时间。以及管理并设置视频组件的指令。 - 创建遵循
AVVideoCompositing
协议的customVideoCompositorClass
,这个类主要用于定义视频合成器的属性和方法。 - 在可变组件中添加资源数据,也就是轨道
AVMutableCompositionTrack
(一般添加2种:音频轨道和视频轨道)。 - 创建视频组件的指令
AVMutableVideoCompositionInstruction
,这个类主要用于管理应用层的指令。 - 创建视频应用层的指令
AVMutableVideoCompositionLayerInstruction
用户管理视频框架应该如何被应用和组合,也就是说是子视频在总视频中出现和消失的时间、大小、动画等。 - 创建视频导出会话对象
AVAssetExportSession
,主要是根据 videoComposition 去创建一个新的视频,并输出到一个指定的文件路径中去。
使用例子
创建 AVAsset
创建两个视频资源用于后续合成
NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"cat.mp4" withExtension:nil];
NSURL *url2 = [[NSBundle mainBundle] URLForResource:@"girl.mp4" withExtension:nil];
self.assets = @[[AVAsset assetWithURL:url1], [AVAsset assetWithURL:url2]];
self.editor = [[SimpleEditor alloc] initWithClips:self.assets];
创建 AVMutableComposition
可以使用 AVMutableComposition
类创建一个自定义的 Composition。可以使用 AVMutableCompositionTrack
类在自定义的 Composition 中添加一个或多个 composition tracks。
AVAssetTrack *clipVideoTrack = [[[self.clips objectAtIndex:0] tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
CGSize videoSize = clipVideoTrack.naturalSize;
AVMutableComposition *composition = [AVMutableComposition composition];
// 采用第一个视频作为画幅尺寸
composition.naturalSize = videoSize;
创建 AVMutableVideoComposition
使用AVMutableVideoComposition对象可以对 composition 中的 video tracks 执行自定义处理操作。使用 video composition,还可以为 video tracks 指定尺寸、缩放比例、以及帧率。
AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.customVideoCompositorClass = [CustomVideoCompositor class];
创建 CustomVideoCompositor
CustomVideoCompositor
遵循AVVideoCompositing
协议,主要有以下几个方法:
@protocol AVVideoCompositing<NSObject>
// 指示视频合成器可以接受作为输入的源帧像素缓冲区属性的类型。
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, id> *sourcePixelBufferAttributes;
// 指示视频合成器为其创建的新缓冲区所需的像素缓冲区属性
@property (nonatomic, readonly) NSDictionary<NSString *, id> *requiredPixelBufferAttributesForRenderContext;
// 调用以通知自定义合成器合成将切换到其他渲染上下文
- (void)renderContextChanged:(AVVideoCompositionRenderContext *)newRenderContext;
// 当前视频帧回调
- (void)startVideoCompositionRequest:(AVAsynchronousVideoCompositionRequest *)asyncVideoCompositionRequest;
// 取消
- (void)cancelAllPendingVideoCompositionRequests;
创建 CustomVideoCompositor
// 指示视频合成器可以接受作为输入的源帧像素缓冲区属性的类型。
- (NSDictionary *)sourcePixelBufferAttributes {
return @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
(NSString*)kCVPixelBufferOpenGLESCompatibilityKey : [NSNumber numberWithBool:YES]};
}
// 指示视频合成器为其创建的新缓冲区所需的像素缓冲区属性
- (NSDictionary *)requiredPixelBufferAttributesForRenderContext {
return @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
(NSString*)kCVPixelBufferOpenGLESCompatibilityKey : [NSNumber numberWithBool:YES]};
}
// 调用以通知自定义合成器合成将切换到其他渲染上下文
- (void)renderContextChanged:(nonnull AVVideoCompositionRenderContext *)newRenderContext {
}
// 当前视频帧回调
- (void)startVideoCompositionRequest:(nonnull AVAsynchronousVideoCompositionRequest *)request {
@autoreleasepool {
dispatch_async(_renderingQueue, ^{
if (self.shouldCancelAllRequests) {
[request finishCancelledRequest];
} else {
NSError *err = nil;
// Get the next rendererd pixel buffer
CVPixelBufferRef resultPixels = [self newRenderedPixelBufferForRequest:request error:&err];
if (resultPixels) {
CFRetain(resultPixels);
// The resulting pixelbuffer from OpenGL renderer is passed along to the request
[request finishWithComposedVideoFrame:resultPixels];
CFRelease(resultPixels);
} else {
[request finishWithError:err];
}
}
});
}
}
- (CVPixelBufferRef)newRenderedPixelBufferForRequest:(AVAsynchronousVideoCompositionRequest *)request error:(NSError **)errOut {
CVPixelBufferRef dstPixels = nil;
CustomVideoCompositionInstruction *currentInstruction = request.videoCompositionInstruction;
// 获取指定 track 的 pixelBuffer
CVPixelBufferRef currentPixelBuffer = [request sourceFrameByTrackID:currentInstruction.trackID];
// 获取到当前视频渲染帧 currentPixelBuffer 之后
// 在这里进行后续的图像渲染处理,例如转场动画、特效、滤镜等
dstPixels = currentPixelBuffer;
return dstPixels;
}
// 取消
- (void)cancelAllPendingVideoCompositionRequests {
_shouldCancelAllRequests = YES;
dispatch_barrier_async(_renderingQueue, ^() {
self.shouldCancelAllRequests = NO;
});
}
素材填充,创建 AVVideoCompositionInstruction
- (void)buildTransitionComposition:(AVMutableComposition *)composition andVideoComposition:(AVMutableVideoComposition *)videoComposition {
NSUInteger clipsCount = self.clips.count;
CMTime nextClipStartTime = kCMTimeZero;
// 添加两个视频轨道和音频轨道
AVMutableCompositionTrack *compositionVideoTracks[2];
AVMutableCompositionTrack *compositionAudioTracks[2];
compositionVideoTracks[0] = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
compositionVideoTracks[1] = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
compositionAudioTracks[0] = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
compositionAudioTracks[1] = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
CMTimeRange *timeRanges = alloca(sizeof(CMTimeRange) * clipsCount);
// 使用视频素材 AVAssetTrack,分别填充轨道
for (int i = 0; i < clipsCount; i++) {
AVAsset *asset = [self.clips objectAtIndex:i];
CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, [asset duration]);
AVAssetTrack *clipVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
[compositionVideoTracks[i] insertTimeRange:timeRange ofTrack:clipVideoTrack atTime:nextClipStartTime error:nil];
AVAssetTrack *clipAudioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
[compositionAudioTracks[i] insertTimeRange:timeRange ofTrack:clipAudioTrack atTime:nextClipStartTime error:nil];
timeRanges[i] = CMTimeRangeMake(nextClipStartTime, timeRange.duration);
nextClipStartTime = CMTimeAdd(nextClipStartTime, timeRange.duration);
}
NSMutableArray *instructions = [NSMutableArray array];
for (int i = 0; i < clipsCount; i++) {
// 创建 AVVideoCompositionInstruction
CustomVideoCompositionInstruction *videoInstruction = [[CustomVideoCompositionInstruction alloc] initTransitionWithSourceTrackIDs:@[@(compositionVideoTracks[i].trackID)] forTimeRange:timeRanges[i]];
videoInstruction.trackID = compositionVideoTracks[i].trackID;
[instructions addObject:videoInstruction];
}
videoComposition.instructions = instructions;
}
输出视频
AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:self.editor.composition
presetName:AVAssetExportPresetHighestQuality];
exporter.outputFileType = AVFileTypeQuickTimeMovie;
exporter.timeRange = CMTimeRangeMake(kCMTimeZero, duration);
exporter.outputURL = [NSURL fileURLWithPath:cachesDir];
exporter.shouldOptimizeForNetworkUse = YES;
exporter.videoComposition = self.editor.videoComposition;
[exporter exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
if (exporter.status == AVAssetExportSessionStatusCompleted) {
NSLog(@"合成成功");
}else {
NSLog(@"合成失败 ---- -%@",exporter.error);
}
});
}];
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。