视频合成

AVMutableComposition

AVFoundation 框架为音视频编辑提供了功能丰富的类集;其中的关键是 composition ,它将不同的 asset 相结合并形成一个新的 asset ,composition 是一个或多个媒体资源的 track 的集合。AVMutableComposition 类提供了插入和删除 track, 以及管理其时间顺序的的接口。下图展示了如何通过已存在的 assets 组合成为一个 composition。如果你需要顺序合并多个 asset 到一个文件中,这就刚刚够用。但是如果要对 track 执行任何自定义的音视频处理操作,,那么你需要分别对音频和视频进行合并。
iOS 视频编辑 -  视频合成 - 图1

如上图所示,我们从本地加载两个媒体资源 AVAssetAVAsset 内各有三个轨道,其中两个为 video,一个为 audio。
基于以上的两个媒体资源,我们创建 AVMutableComposition,用于作为我们的合成输出;分别指定 AVMutableCompositionTrack为对应的AVAssetTrack

AVMutableAudioMix

使用 AVMutableAudioMix 类可以对 composition 中的 audio track 进行自定义操作。你还可以指定 audio track 的最大音量以及为其设置渐变效果。
iOS 视频编辑 -  视频合成 - 图2

AVMutableVideoComposition

使用 AVMutableVideoComposition 类可以直接处理视频 track。从一个 video composition 输出视频时,还可以指定输出的尺寸、缩放比例、以及帧率。通过 video composition 的指令 (instructions,AVMutableVideoCompositionInstruction),可以修改视频背景色,以及设置 layer 的 instructions。Layer 的 instructions(AVMutableVideoCompositionLayerInstruction)可以对 video track 实现渐变、渐变变换、透明度、透明度变换等效果。Video composition 还允许通过 animationTool 属性在视频中应用 Core Animation 框架的一些效果。
iOS 视频编辑 -  视频合成 - 图3

AVAssetExportSession

对音视频进行组合, 可以使用 AVAssetExportSession。使用 composition 初始化一个 export session,然后分别其设置 audioMix 和 videoComposition 属性。
iOS 视频编辑 -  视频合成 - 图4

整体流程

下面这是一个简单的视频合成例子,可以让我们很直观的了解视频编辑的关键步骤:

  1. 获取视频资源AVAsset
  2. 创建自定义合成对象AVMutableComposition
  3. 创建视频组件AVMutableVideoComposition,这个类是处理视频中要编辑的东西。可以设定所需视频的大小、规模以及帧的持续时间。以及管理并设置视频组件的指令。
  4. 创建遵循AVVideoCompositing协议的customVideoCompositorClass,这个类主要用于定义视频合成器的属性和方法。
  5. 在可变组件中添加资源数据,也就是轨道AVMutableCompositionTrack(一般添加2种:音频轨道和视频轨道)。
  6. 创建视频组件的指令AVMutableVideoCompositionInstruction,这个类主要用于管理应用层的指令。
  7. 创建视频应用层的指令AVMutableVideoCompositionLayerInstruction 用户管理视频框架应该如何被应用和组合,也就是说是子视频在总视频中出现和消失的时间、大小、动画等。
  8. 创建视频导出会话对象AVAssetExportSession,主要是根据 videoComposition 去创建一个新的视频,并输出到一个指定的文件路径中去。

使用例子

创建 AVAsset

创建两个视频资源用于后续合成

  1. NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"cat.mp4" withExtension:nil];
  2. NSURL *url2 = [[NSBundle mainBundle] URLForResource:@"girl.mp4" withExtension:nil];
  3. self.assets = @[[AVAsset assetWithURL:url1], [AVAsset assetWithURL:url2]];
  4. self.editor = [[SimpleEditor alloc] initWithClips:self.assets];

创建 AVMutableComposition

可以使用 AVMutableComposition 类创建一个自定义的 Composition。可以使用 AVMutableCompositionTrack 类在自定义的 Composition 中添加一个或多个 composition tracks。

  1. AVAssetTrack *clipVideoTrack = [[[self.clips objectAtIndex:0] tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
  2. CGSize videoSize = clipVideoTrack.naturalSize;
  3. AVMutableComposition *composition = [AVMutableComposition composition];
  4. // 采用第一个视频作为画幅尺寸
  5. composition.naturalSize = videoSize;

创建 AVMutableVideoComposition

使用AVMutableVideoComposition对象可以对 composition 中的 video tracks 执行自定义处理操作。使用 video composition,还可以为 video tracks 指定尺寸、缩放比例、以及帧率。

  1. AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
  2. videoComposition.customVideoCompositorClass = [CustomVideoCompositor class];

创建 CustomVideoCompositor

CustomVideoCompositor遵循AVVideoCompositing协议,主要有以下几个方法:

  1. @protocol AVVideoCompositing<NSObject>
  2. // 指示视频合成器可以接受作为输入的源帧像素缓冲区属性的类型。
  3. @property (nonatomic, readonly, nullable) NSDictionary<NSString *, id> *sourcePixelBufferAttributes;
  4. // 指示视频合成器为其创建的新缓冲区所需的像素缓冲区属性
  5. @property (nonatomic, readonly) NSDictionary<NSString *, id> *requiredPixelBufferAttributesForRenderContext;
  6. // 调用以通知自定义合成器合成将切换到其他渲染上下文
  7. - (void)renderContextChanged:(AVVideoCompositionRenderContext *)newRenderContext;
  8. // 当前视频帧回调
  9. - (void)startVideoCompositionRequest:(AVAsynchronousVideoCompositionRequest *)asyncVideoCompositionRequest;
  10. // 取消
  11. - (void)cancelAllPendingVideoCompositionRequests;

创建 CustomVideoCompositor
  1. // 指示视频合成器可以接受作为输入的源帧像素缓冲区属性的类型。
  2. - (NSDictionary *)sourcePixelBufferAttributes {
  3. return @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
  4. (NSString*)kCVPixelBufferOpenGLESCompatibilityKey : [NSNumber numberWithBool:YES]};
  5. }
  6. // 指示视频合成器为其创建的新缓冲区所需的像素缓冲区属性
  7. - (NSDictionary *)requiredPixelBufferAttributesForRenderContext {
  8. return @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
  9. (NSString*)kCVPixelBufferOpenGLESCompatibilityKey : [NSNumber numberWithBool:YES]};
  10. }
  11. // 调用以通知自定义合成器合成将切换到其他渲染上下文
  12. - (void)renderContextChanged:(nonnull AVVideoCompositionRenderContext *)newRenderContext {
  13. }
  14. // 当前视频帧回调
  15. - (void)startVideoCompositionRequest:(nonnull AVAsynchronousVideoCompositionRequest *)request {
  16. @autoreleasepool {
  17. dispatch_async(_renderingQueue, ^{
  18. if (self.shouldCancelAllRequests) {
  19. [request finishCancelledRequest];
  20. } else {
  21. NSError *err = nil;
  22. // Get the next rendererd pixel buffer
  23. CVPixelBufferRef resultPixels = [self newRenderedPixelBufferForRequest:request error:&err];
  24. if (resultPixels) {
  25. CFRetain(resultPixels);
  26. // The resulting pixelbuffer from OpenGL renderer is passed along to the request
  27. [request finishWithComposedVideoFrame:resultPixels];
  28. CFRelease(resultPixels);
  29. } else {
  30. [request finishWithError:err];
  31. }
  32. }
  33. });
  34. }
  35. }
  36. - (CVPixelBufferRef)newRenderedPixelBufferForRequest:(AVAsynchronousVideoCompositionRequest *)request error:(NSError **)errOut {
  37. CVPixelBufferRef dstPixels = nil;
  38. CustomVideoCompositionInstruction *currentInstruction = request.videoCompositionInstruction;
  39. // 获取指定 track 的 pixelBuffer
  40. CVPixelBufferRef currentPixelBuffer = [request sourceFrameByTrackID:currentInstruction.trackID];
  41. // 获取到当前视频渲染帧 currentPixelBuffer 之后
  42. // 在这里进行后续的图像渲染处理,例如转场动画、特效、滤镜等
  43. dstPixels = currentPixelBuffer;
  44. return dstPixels;
  45. }
  46. // 取消
  47. - (void)cancelAllPendingVideoCompositionRequests {
  48. _shouldCancelAllRequests = YES;
  49. dispatch_barrier_async(_renderingQueue, ^() {
  50. self.shouldCancelAllRequests = NO;
  51. });
  52. }

素材填充,创建 AVVideoCompositionInstruction
  1. - (void)buildTransitionComposition:(AVMutableComposition *)composition andVideoComposition:(AVMutableVideoComposition *)videoComposition {
  2. NSUInteger clipsCount = self.clips.count;
  3. CMTime nextClipStartTime = kCMTimeZero;
  4. // 添加两个视频轨道和音频轨道
  5. AVMutableCompositionTrack *compositionVideoTracks[2];
  6. AVMutableCompositionTrack *compositionAudioTracks[2];
  7. compositionVideoTracks[0] = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
  8. compositionVideoTracks[1] = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
  9. compositionAudioTracks[0] = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
  10. compositionAudioTracks[1] = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
  11. CMTimeRange *timeRanges = alloca(sizeof(CMTimeRange) * clipsCount);
  12. // 使用视频素材 AVAssetTrack,分别填充轨道
  13. for (int i = 0; i < clipsCount; i++) {
  14. AVAsset *asset = [self.clips objectAtIndex:i];
  15. CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, [asset duration]);
  16. AVAssetTrack *clipVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
  17. [compositionVideoTracks[i] insertTimeRange:timeRange ofTrack:clipVideoTrack atTime:nextClipStartTime error:nil];
  18. AVAssetTrack *clipAudioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
  19. [compositionAudioTracks[i] insertTimeRange:timeRange ofTrack:clipAudioTrack atTime:nextClipStartTime error:nil];
  20. timeRanges[i] = CMTimeRangeMake(nextClipStartTime, timeRange.duration);
  21. nextClipStartTime = CMTimeAdd(nextClipStartTime, timeRange.duration);
  22. }
  23. NSMutableArray *instructions = [NSMutableArray array];
  24. for (int i = 0; i < clipsCount; i++) {
  25. // 创建 AVVideoCompositionInstruction
  26. CustomVideoCompositionInstruction *videoInstruction = [[CustomVideoCompositionInstruction alloc] initTransitionWithSourceTrackIDs:@[@(compositionVideoTracks[i].trackID)] forTimeRange:timeRanges[i]];
  27. videoInstruction.trackID = compositionVideoTracks[i].trackID;
  28. [instructions addObject:videoInstruction];
  29. }
  30. videoComposition.instructions = instructions;
  31. }

输出视频
  1. AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:self.editor.composition
  2. presetName:AVAssetExportPresetHighestQuality];
  3. exporter.outputFileType = AVFileTypeQuickTimeMovie;
  4. exporter.timeRange = CMTimeRangeMake(kCMTimeZero, duration);
  5. exporter.outputURL = [NSURL fileURLWithPath:cachesDir];
  6. exporter.shouldOptimizeForNetworkUse = YES;
  7. exporter.videoComposition = self.editor.videoComposition;
  8. [exporter exportAsynchronouslyWithCompletionHandler:^{
  9. dispatch_async(dispatch_get_main_queue(), ^{
  10. if (exporter.status == AVAssetExportSessionStatusCompleted) {
  11. NSLog(@"合成成功");
  12. }else {
  13. NSLog(@"合成失败 ---- -%@",exporter.error);
  14. }
  15. });
  16. }];

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

更多精彩分享