AVFoundation框架解析(六)—— 视频音频的合成(一)

版本记录

版本号 时间
V1.0 2017.08.30

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
2. AVFoundation框架解析(二)—— 实现视频预览录制保存到相册
3. AVFoundation框架解析(三)—— 几个关键问题之关于框架的深度概括
4. AVFoundation框架解析(四)—— 几个关键问题之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 几个关键问题之AVFoundation探索(二)

AVFoundation 编辑

AVFoundation框架提供了一组功能丰富的类,以便于编辑视听资源。 AVFoundation的编辑API的核心是组合。 组合仅仅是来自一个或多个不同媒体资产的轨道的集合。 AVMutableComposition类提供了一个用于插入和删除轨道以及管理其时间顺序的接口。 下图显示了如何将新组合从现有资产的组合中拼凑起来形成新的资产。 如果您想要做的是将多个资产合并到单个文件中,那么就是您需要的细节。 如果您想在合成的轨道上执行任何自定义音频或视频处理,则需要分别包含音频混合或视频合成。

使用AVMutableAudioMix类,您可以对合成中的音轨执行自定义音频处理,如下图所示。 目前,您可以指定最大音量或设置音轨的音量斜坡。

您可以使用AVMutableVideoComposition类直接与组合中的视频轨进行编辑,如下图所示。 使用单个视频构图,您可以为输出视频指定所需的渲染大小和缩放以及帧持续时间。 通过视频作品的指示(由AVMutableVideoCompositionInstruction类表示),您可以修改视频的背景颜色并应用图层说明。 这些层指令(由AVMutableVideoCompositionLayerInstruction类表示)可用于将转换,变换斜坡,不透明度和不透明度斜坡应用于组合中的视频轨道。 视频构图类还使您能够使用animationTool属性将Core Animation框架的效果引入到视频中。

要将您的合成与音频混合和视频合成相结合,请使用AVAssetExportSession对象,如下图所示。 您可以使用您的合成初始化导出会话,然后分别将音频混合和视频构图分配给audioMixvideoComposition属性。


Creating a Composition

要创建自己的组合,可以使用AVMutableComposition类。 要向组合添加媒体数据,您必须添加一个或多个由AVMutableCompositionTrack类表示的组合轨道。 最简单的情况是创建一个具有一个视频轨道和一个音轨的可变组合:

AVMutableComposition *mutableComposition = [AVMutableComposition composition];
// Create the video composition track.
AVMutableCompositionTrack *mutableCompositionVideoTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
// Create the audio composition track.
AVMutableCompositionTrack *mutableCompositionAudioTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

1. Options for Initializing a Composition Track

在组合中添加新曲目时,您必须同时提供媒体类型和曲目ID。 虽然音频和视频是最常用的媒体类型,但您也可以指定其他媒体类型,如AVMediaTypeSubtitleAVMediaTypeText

与某些视听数据相关联的每个轨道都具有称为轨迹ID的唯一标识符。 如果您指定kCMPersistentTrackID_Invalid作为首选轨道ID,则会为您自动生成唯一标识符并与轨道相关联。


Adding Audiovisual Data to a Composition

一旦您有一个或多个轨道的合成,您可以开始将您的媒体数据添加到相应的轨道上。 要将媒体数据添加到合成轨道,您需要访问媒体数据所在的AVAsset对象。 您可以使用可变组合轨道界面将同一底层介质类型的多个轨道放在同一轨道上。 以下示例说明如何按顺序将两个不同的视频资源轨道添加到相同的构图轨道中:

// You can retrieve AVAssets from a number of places, like the camera roll for example.
AVAsset *videoAsset = <#AVAsset with at least one video track#>;
AVAsset *anotherVideoAsset = <#another AVAsset with at least one video track#>;

// Get the first video track from each asset.
AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
AVAssetTrack *anotherVideoAssetTrack = [[anotherVideoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];

// Add them both to the composition.
[mutableCompositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero,videoAssetTrack.timeRange.duration) ofTrack:videoAssetTrack atTime:kCMTimeZero error:nil];
[mutableCompositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero,anotherVideoAssetTrack.timeRange.duration) ofTrack:anotherVideoAssetTrack atTime:videoAssetTrack.timeRange.duration error:nil];

1. Retrieving Compatible Composition Tracks

在可能的情况下,每个媒体类型应该只有一个合成轨迹。 兼容资产轨道的统一导致资源使用量最少。 当连续呈现媒体数据时,您应该将相同类型的任何媒体数据放在同一组合轨上。 您可以查询可变的合成,以确定是否有任何合成轨道与您所需的素材资源轨道兼容:

AVMutableCompositionTrack *compatibleCompositionTrack = [mutableComposition mutableTrackCompatibleWithTrack:<#the AVAssetTrack you want to insert#>];
if (compatibleCompositionTrack) {
    // Implementation continues.
}

将多个视频片段放置在相同的合成轨道上可能会导致视频片段之间的转换丢帧,特别是在嵌入式设备上。 为您的视频片段选择合成轨道的数量完全取决于您的app的设计及其预期平台。


Generating a Volume Ramp

单个AVMutableAudioMix对象可以对您单独组成的所有音轨执行自定义音频处理。 您可以使用audioMix类方法创建音频混合,并使用AVMutableAudioMixInputParameters类的实例将音频混合与组合中的特定轨道相关联。 可以使用音频混合来改变音轨的音量。 以下示例显示如何在特定音轨上设置音量斜坡,以在组合的持续时间内慢慢淡出音频:

AVMutableAudioMix *mutableAudioMix = [AVMutableAudioMix audioMix];

// Create the audio mix input parameters object.
AVMutableAudioMixInputParameters *mixParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:mutableCompositionAudioTrack];

// Set the volume ramp to slowly fade the audio out over the duration of the composition.
[mixParameters setVolumeRampFromStartVolume:1.f toEndVolume:0.f timeRange:CMTimeRangeMake(kCMTimeZero, mutableComposition.duration)];

// Attach the input parameters to the audio mix.
mutableAudioMix.inputParameters = @[mixParameters];

Performing Custom Video Processing

与音频混合一样,您只需要一个AVMutableVideoComposition对象来对您的组合视频轨道执行所有自定义视频处理。 使用视频构图,您可以直接为构图视频轨道设置适当的渲染大小,缩放比例和帧速率。

1. Changing the Composition’s Background Color

所有视频构图还必须包含至少包含一个视频构图指令的AVVideoCompositionInstruction对象数组。 您可以使用AVMutableVideoCompositionInstruction类创建自己的视频构图说明。 使用视频合成指令,您可以修改构图的背景颜色,指定是否需要后处理或应用图层指令。

以下示例说明如何创建一个视频合成指令,将整个构图的背景颜色更改为红色。

AVMutableVideoCompositionInstruction *mutableVideoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
mutableVideoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, mutableComposition.duration);
mutableVideoCompositionInstruction.backgroundColor = [[UIColor redColor] CGColor];

2. Applying Opacity Ramps

视频合成指令也可用于应用视频合成图层指令。 AVMutableVideoCompositionLayerInstruction对象可以对组合中的某个视频轨道应用变换,变换斜坡,不透明度和不透明度斜坡。 视频合成指令的layerInstructions数组中的图层指令的顺序决定了在构图指令的持续时间内,来自源轨道的视频帧应如何分层和组合。 以下代码片段显示如何设置不透明度斜坡以在转换到第二个视频之前缓慢淡出组合中的第一个视频:

AVAsset *firstVideoAssetTrack = <#AVAssetTrack representing the first video segment played in the composition#>;
AVAsset *secondVideoAssetTrack = <#AVAssetTrack representing the second video segment played in the composition#>;

// Create the first video composition instruction.
AVMutableVideoCompositionInstruction *firstVideoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];

// Set its time range to span the duration of the first video track.
firstVideoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, firstVideoAssetTrack.timeRange.duration);

// Create the layer instruction and associate it with the composition video track.
AVMutableVideoCompositionLayerInstruction *firstVideoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:mutableCompositionVideoTrack];

// Create the opacity ramp to fade out the first video track over its entire duration.
[firstVideoLayerInstruction setOpacityRampFromStartOpacity:1.f toEndOpacity:0.f timeRange:CMTimeRangeMake(kCMTimeZero, firstVideoAssetTrack.timeRange.duration)];

// Create the second video composition instruction so that the second video track isn't transparent.
AVMutableVideoCompositionInstruction *secondVideoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];

// Set its time range to span the duration of the second video track.
secondVideoCompositionInstruction.timeRange = CMTimeRangeMake(firstVideoAssetTrack.timeRange.duration, CMTimeAdd(firstVideoAssetTrack.timeRange.duration, secondVideoAssetTrack.timeRange.duration));

// Create the second layer instruction and associate it with the composition video track.
AVMutableVideoCompositionLayerInstruction *secondVideoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:mutableCompositionVideoTrack];

// Attach the first layer instruction to the first video composition instruction.
firstVideoCompositionInstruction.layerInstructions = @[firstVideoLayerInstruction];

// Attach the second layer instruction to the second video composition instruction.
secondVideoCompositionInstruction.layerInstructions = @[secondVideoLayerInstruction];

// Attach both of the video composition instructions to the video composition.
AVMutableVideoComposition *mutableVideoComposition = [AVMutableVideoComposition videoComposition];
mutableVideoComposition.instructions = @[firstVideoCompositionInstruction, secondVideoCompositionInstruction];

3. Incorporating Core Animation Effects

视频构图可以通过animationTool属性添加核心动画的功能。 通过这个动画工具,您可以完成水印视频和添加标题或动画叠加等任务。 核心动画可以以两种不同的方式与视频合成一起使用:您可以添加一个核心动画图层作为自己的个人作品轨迹,或者您可以将Core Animation效果(使用核心动画图层)渲染到您的作曲中的视频帧中。 以下代码通过向视频的中心添加水印来显示后一个选项:

CALayer *watermarkLayer = <#CALayer representing your desired watermark image#>;
CALayer *parentLayer = [CALayer layer];
CALayer *videoLayer = [CALayer layer];
parentLayer.frame = CGRectMake(0, 0, mutableVideoComposition.renderSize.width, mutableVideoComposition.renderSize.height);
videoLayer.frame = CGRectMake(0, 0, mutableVideoComposition.renderSize.width, mutableVideoComposition.renderSize.height);
[parentLayer addSublayer:videoLayer];
watermarkLayer.position = CGPointMake(mutableVideoComposition.renderSize.width/2, mutableVideoComposition.renderSize.height/4);
[parentLayer addSublayer:watermarkLayer];
mutableVideoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];

Putting It All Together: Combining Multiple Assets and Saving the Result to the Camera Roll

这个简短的代码示例说明了如何组合两个视频资产轨道和音频资产轨道来创建单个视频文件。 它显示如何:

  • 创建一个AVMutableComposition对象并添加多个AVMutableCompositionTrack对象
  • AVAssetTrack对象的时间范围添加到兼容的组合轨道
  • 检查视频资产轨道的preferredTransform属性以确定视频的方向
  • 使用AVMutableVideoCompositionLayerInstruction对象将变换应用到合成中的视频轨道
  • 为视频构图的renderSizeframeDuration属性设置适当的值
  • 导出到视频文件时,使用组合与视频合成
  • 将视频文件保存到相机胶卷

要关注最相关的代码,这个例子省略了一个完整的应用程序的几个方面,如内存管理和错误处理。 要使用AVFoundation,您将有足够的经验与Cocoa推断丢失的部分。

1. Creating the Composition

要从单独的资源组合轨道,您可以使用AVMutableComposition对象。 创建构图并添加一个音频和一个视频轨道。

AVMutableComposition *mutableComposition = [AVMutableComposition composition];
AVMutableCompositionTrack *videoCompositionTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *audioCompositionTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

2. Adding the Assets

一个空的组合对你来说没有用。 将两个视频资产轨道和音频资产轨道添加到组合。

AVAssetTrack *firstVideoAssetTrack = [[firstVideoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
AVAssetTrack *secondVideoAssetTrack = [[secondVideoAsset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
[videoCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, firstVideoAssetTrack.timeRange.duration) ofTrack:firstVideoAssetTrack atTime:kCMTimeZero error:nil];
[videoCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, secondVideoAssetTrack.timeRange.duration) ofTrack:secondVideoAssetTrack atTime:firstVideoAssetTrack.timeRange.duration error:nil];
[audioCompositionTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, CMTimeAdd(firstVideoAssetTrack.timeRange.duration, secondVideoAssetTrack.timeRange.duration)) ofTrack:[[audioAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:nil];

这里还要注意:这假设您有两个资产包含至少一个视频轨道,每个资产包含至少一个音轨。 可以从相机胶卷中检索视频,并且可以从音乐库或视频本身检索音轨。

3. Checking the Video Orientations

将视频和音轨添加到组合中后,您需要确保两个视频轨道的方向正确。 默认情况下,所有视频轨道都假定为横向模式。 如果您的视频轨道以纵向模式拍摄,导出时视频将无法正确定向。 同样,如果您尝试将以纵向模式拍摄的视频与横向模式下的视频拍摄相结合,导出会话将无法完成。

BOOL isFirstVideoPortrait = NO;
CGAffineTransform firstTransform = firstVideoAssetTrack.preferredTransform;
// Check the first video track's preferred transform to determine if it was recorded in portrait mode.
if (firstTransform.a == 0 && firstTransform.d == 0 && (firstTransform.b == 1.0 || firstTransform.b == -1.0) && (firstTransform.c == 1.0 || firstTransform.c == -1.0)) {
    isFirstVideoPortrait = YES;
}
BOOL isSecondVideoPortrait = NO;
CGAffineTransform secondTransform = secondVideoAssetTrack.preferredTransform;
// Check the second video track's preferred transform to determine if it was recorded in portrait mode.
if (secondTransform.a == 0 && secondTransform.d == 0 && (secondTransform.b == 1.0 || secondTransform.b == -1.0) && (secondTransform.c == 1.0 || secondTransform.c == -1.0)) {
    isSecondVideoPortrait = YES;
}
if ((isFirstVideoAssetPortrait && !isSecondVideoAssetPortrait) || (!isFirstVideoAssetPortrait && isSecondVideoAssetPortrait)) {
    UIAlertView *incompatibleVideoOrientationAlert = [[UIAlertView alloc] initWithTitle:@"Error!" message:@"Cannot combine a video shot in portrait mode with a video shot in landscape mode." delegate:self cancelButtonTitle:@"Dismiss" otherButtonTitles:nil];
    [incompatibleVideoOrientationAlert show];
    return;
}

4. Applying the Video Composition Layer Instructions

一旦您知道视频片段具有兼容的方向,您可以对每个视频片段应用必要的图层指令,并将这些图层指令添加到视频构图。

AVMutableVideoCompositionInstruction *firstVideoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];

// Set the time range of the first instruction to span the duration of the first video track.
firstVideoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, firstVideoAssetTrack.timeRange.duration);
AVMutableVideoCompositionInstruction * secondVideoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];

// Set the time range of the second instruction to span the duration of the second video track.
secondVideoCompositionInstruction.timeRange = CMTimeRangeMake(firstVideoAssetTrack.timeRange.duration, CMTimeAdd(firstVideoAssetTrack.timeRange.duration, secondVideoAssetTrack.timeRange.duration));
AVMutableVideoCompositionLayerInstruction *firstVideoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoCompositionTrack];

// Set the transform of the first layer instruction to the preferred transform of the first video track.
[firstVideoLayerInstruction setTransform:firstTransform atTime:kCMTimeZero];
AVMutableVideoCompositionLayerInstruction *secondVideoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoCompositionTrack];

// Set the transform of the second layer instruction to the preferred transform of the second video track.
[secondVideoLayerInstruction setTransform:secondTransform atTime:firstVideoAssetTrack.timeRange.duration];
firstVideoCompositionInstruction.layerInstructions = @[firstVideoLayerInstruction];
secondVideoCompositionInstruction.layerInstructions = @[secondVideoLayerInstruction];
AVMutableVideoComposition *mutableVideoComposition = [AVMutableVideoComposition videoComposition];
mutableVideoComposition.instructions = @[firstVideoCompositionInstruction, secondVideoCompositionInstruction];

所有AVAssetTrack对象都有一个preferredTransform属性,其中包含该资产轨道的方向信息。 每当资产轨迹显示在屏幕上时,就会应用此转换。 在以前的代码中,层指令的变换设置为资产轨道的变换,以便在调整渲染大小后,新构图中的视频会正确显示。

5. Setting the Render Size and Frame Duration

要完成视频方向修复,您必须相应地调整renderSize属性。 您还应为frameDuration属性选择合适的值,例如1/30秒(或每秒30帧)。 默认情况下,renderScale属性设置为1.0,适用于此组合。

CGSize naturalSizeFirst, naturalSizeSecond;
// If the first video asset was shot in portrait mode, then so was the second one if we made it here.
if (isFirstVideoAssetPortrait) {
// Invert the width and height for the video tracks to ensure that they display properly.
    naturalSizeFirst = CGSizeMake(firstVideoAssetTrack.naturalSize.height, firstVideoAssetTrack.naturalSize.width);
    naturalSizeSecond = CGSizeMake(secondVideoAssetTrack.naturalSize.height, secondVideoAssetTrack.naturalSize.width);
}
else {
// If the videos weren't shot in portrait mode, we can just use their natural sizes.
    naturalSizeFirst = firstVideoAssetTrack.naturalSize;
    naturalSizeSecond = secondVideoAssetTrack.naturalSize;
}
float renderWidth, renderHeight;
// Set the renderWidth and renderHeight to the max of the two videos widths and heights.
if (naturalSizeFirst.width > naturalSizeSecond.width) {
    renderWidth = naturalSizeFirst.width;
}
else {
    renderWidth = naturalSizeSecond.width;
}
if (naturalSizeFirst.height > naturalSizeSecond.height) {
    renderHeight = naturalSizeFirst.height;
}
else {
    renderHeight = naturalSizeSecond.height;
}
mutableVideoComposition.renderSize = CGSizeMake(renderWidth, renderHeight);
// Set the frame duration to an appropriate value (i.e. 30 frames per second for video).
mutableVideoComposition.frameDuration = CMTimeMake(1,30);

6. Exporting the Composition and Saving it to the Camera Roll

此过程的最后一步包括将整个组合导出到单个视频文件中,并将该视频保存到相机卷。 您可以使用AVAssetExportSession对象来创建新的视频文件,并将其传递给输出文件所需的URL。 然后,您可以使用ALAssetsLibrary类将生成的视频文件保存到相机胶卷。

// Create a static date formatter so we only have to initialize it once.
static NSDateFormatter *kDateFormatter;
if (!kDateFormatter) {
    kDateFormatter = [[NSDateFormatter alloc] init];
    kDateFormatter.dateStyle = NSDateFormatterMediumStyle;
    kDateFormatter.timeStyle = NSDateFormatterShortStyle;
}

// Create the export session with the composition and set the preset to the highest quality.
AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:mutableComposition presetName:AVAssetExportPresetHighestQuality];

// Set the desired output URL for the file created by the export process.
exporter.outputURL = [[[[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:@YES error:nil] URLByAppendingPathComponent:[kDateFormatter stringFromDate:[NSDate date]]] URLByAppendingPathExtension:CFBridgingRelease(UTTypeCopyPreferredTagWithClass((CFStringRef)AVFileTypeQuickTimeMovie, kUTTagClassFilenameExtension))];

// Set the output file type to be a QuickTime movie.
exporter.outputFileType = AVFileTypeQuickTimeMovie;
exporter.shouldOptimizeForNetworkUse = YES;
exporter.videoComposition = mutableVideoComposition;

// Asynchronously export the composition to a video file and save this file to the camera roll once export completes.
[exporter exportAsynchronouslyWithCompletionHandler:^{
    dispatch_async(dispatch_get_main_queue(), ^{
        if (exporter.status == AVAssetExportSessionStatusCompleted) {
            ALAssetsLibrary *assetsLibrary = [[ALAssetsLibrary alloc] init];
            if ([assetsLibrary videoAtPathIsCompatibleWithSavedPhotosAlbum:exporter.outputURL]) {
                [assetsLibrary writeVideoAtPathToSavedPhotosAlbum:exporter.outputURL completionBlock:NULL];
            }
        }
    });
}];

后记

有关视频音频合成的原理理论就这么多,后期会给大家具体工程上的演示,希望对大家有所帮助,谢谢~~~

推荐阅读更多精彩内容