iOS 视频编辑 - 视频合成

1,931 阅读5分钟

视频合成

AVMutableComposition

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

如上图所示,我们从本地加载两个媒体资源 AVAssetAVAsset 内各有三个轨道,其中两个为 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 属性。

整体流程

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

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

更多iOS 视频编辑分享