视频合成
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 bufferCVPixelBufferRef 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 的 pixelBufferCVPixelBufferRef 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++) {// 创建 AVVideoCompositionInstructionCustomVideoCompositionInstruction *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.compositionpresetName: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来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
