iOS 编辑合成视频,以及导出到相册

AVFoundation框架提供了一组功能丰富的类,以便于编辑音频视频Asset。 AVFoundation的编辑API的核心是组成。 组合物只是来自一个或多个不同媒体Asset的轨道的集合。 AVMutableComposition类提供了用于插入和删除曲目以及管理其时序排列的界面。 如果您想要做的是将多个Asset按顺序合并到一个文件中,这是您需要的详细信息。 如果要在乐曲中的曲目上执行任何自定义音频或视频处理,则需要分别合并音频混合或视频组合。
使用AVMutableAudioMix类,您可以对合成中的音轨执行自定义音频处理, 目前,您可以为音轨指定最大音量或设置音量斜坡。
您可以使用AVMutableVideoComposition类直接与合成中的视频轨道一起工作,以便进行编辑, 使用单个视频合成,您可以为输出视频指定所需的渲染大小和比例以及帧持续时间。 通过视频组合的说明(由AVMutableVideoCompositionInstruction类表示),您可以修改视频的背景颜色并应用图层说明。 这些图层指令(由AVMutableVideoCompositionLayerInstruction类表示)可用于对组合中的视频轨道应用变换,变换斜坡,不透明度和不透明度斜坡。 视频组合类还使您能够使用animationTool属性将核心动画框架的效果引入您的视频。
要将您的构图与音频混合和视频合成结合使用,您需要使用AVAssetExportSession对象,您使用构图初始化导出会话,然后分别将音频混合和视频组合分配给audioMix和videoComposition属性。

创建合成

要创建自己的合成,请使用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];

初始化合成轨道的选项

将新曲目添加到合成时,必须同时提供媒体类型和曲目ID。 虽然音频和视频是最常用的媒体类型,但您也可以指定其他媒体类型,例如AVMediaTypeSubtitle或AVMediaTypeText。
与一些视听数据相关联的每个轨道具有被称为轨道ID的唯一标识符。 如果您指定kCMPersistentTrackID_Invalid作为首选曲目ID,系统会自动为您生成并与曲目关联的唯一标识符。

向合成中添加视听数据

一旦您有一个或多个曲目的组合,您可以开始将媒体数据添加到相应的曲目。 要将媒体数据添加到合成轨道,您需要访问媒体数据所在的AVAsset对象。 您可以使用可变合成轨道接口将同一底层介质类型的多个轨道放在同一轨道上。 以下示例说明如何按顺序将两个不同的视频Asset轨道添加到同一合成轨道:
// 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];

检索兼容的合成轨道

在可能的情况下,每个媒体类型只能有一个合成轨道。 兼容的资产轨道的这种统一导致最小量的资源使用。 在连续显示媒体数据时,应将相同类型的任何媒体数据放在同一个合成轨道上。 您可以查询可变的合成,找出是否有任何合成轨道与您的所需资产轨道兼容:
AVMutableCompositionTrack *compatibleCompositionTrack = [mutableComposition mutableTrackCompatibleWithTrack:<#the AVAssetTrack you want to insert#>];
if (compatibleCompositionTrack) {
    // Implementation continues.
}
注意:在同一合成轨道上放置多个视频段可能会导致在视频段之间的过渡处丢弃帧,特别是在嵌入式设备上。 选择视频分段的合成曲目数量完全取决于您的应用及其预期平台的设计

生成卷斜坡

单个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];

执行自定义视频处理

与音频混合一样,您只需要一个AVMutableVideoComposition对象来对合成的视频轨道执行所有自定义视频处理。 使用视频组合,您可以直接为合成的视频轨道设置适当的渲染大小,比例和帧速率。 有关为这些属性设置适当值的详细示例,请参阅设置渲染大小和帧持续时间。

更改合成的背景颜色

所有视频组合还必须具有包含至少一个视频组成指令的AVVideoCompositionInstruction对象的阵列。 您可以使用AVMutableVideoCompositionInstruction类创建自己的视频组合指令。 使用视频合成指令,您可以修改合成的背景颜色,指定是否需要后处理或应用图层指令。
以下示例说明如何创建将整个合成的背景颜色更改为红色的视频合成指令。
AVMutableVideoCompositionInstruction *mutableVideoCompositionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
mutableVideoCompositionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, mutableComposition.duration);
mutableVideoCompositionInstruction.backgroundColor = [[UIColor redColor] CGColor];

应用不透明度斜坡

视频合成指令还可用于应用视频合成层指令。 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];

合并核心动画效果

视频组合可以通过animationTool属性将Core Animation的能力添加到您的合成中。 通过这个动画工具,您可以完成任务,如水印视频和添加标题或动画叠加。 核心动画可以通过两种不同的方式与视频组合一起使用:您可以将Core Animation图层添加为其自己的单独合成轨道,或者您可以直接在合成中的视频帧中渲染Core Animation效果(使用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];

一起放置:组合多个Asset并将结果保存到相机胶卷

此简要代码示例说明如何组合两个视频Asset轨道和音频Asset轨道来创建单个视频文件。 它显示如何:
  • 创建AVMutableComposition对象并添加多个AVMutableCompositionTrack对象
  • 将AVAssetTrack对象的时间范围添加到兼容的合成轨道
  • 检查视频资产轨道的preferredTransform属性,以确定视频的方向
  • 使用AVMutableVideoCompositionLayerInstruction对象将变换应用于合成内的视频轨道
  • 为视频组合的renderSize和frameDuration属性设置适当的值
  • 在导出到视频文件时,结合使用合成与视频合成
  • 将视频文件保存到相机胶卷
注意:为了集中在最相关的代码,此示例省略了完整应用程序的几个方面,例如内存管理和错误处理。 要使用AVFoundation,您应该有足够的经验与Cocoa推断缺失的部分。

创建合成

要将来自单独Asset的曲目拼凑起来,您需要使用AVMutableComposition对象。 创建合成并添加一个音频和一个视频轨道。
AVMutableComposition *mutableComposition = [AVMutableComposition composition];
AVMutableCompositionTrack *videoCompositionTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *audioCompositionTrack = [mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

添加Asset

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];
注意:假设您有两个Asset包含至少一个视频轨道,而第三个Asset至少包含一个音轨。 可以从相机胶卷检索视频,并且可以从音乐库或视频本身检索音轨。

检查视频定向

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

应用视频组成图层说明

一旦你知道视频片段具有兼容的方向,就可以对每个视频片段应用必要的图层指令,并将这些图层指令添加到视频合成。
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属性,其中包含该资产音轨的方向信息。 只要资产轨道显示在屏幕上,就会应用此转换。 在之前的代码中,图层指令的变换设置为资产音轨的变换,以便在调整渲染大小时,新合成中的视频会正确显示。

设置渲染大小和帧持续时间

要完成视频定向修复,必须相应地调整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);

导出合成并将其保存到相机胶卷

此过程的最后一步涉及将整个合成导出到单个视频文件中,并将该视频保存到相机胶卷。 您使用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];
            }
        }
    });
}];```

推荐阅读更多精彩内容