AVFoundation开发秘籍笔记-09媒体的组合和编辑

一、组合媒体

AVFoundation有关资源的组合功能源于AVAsset的子类AVComposition。

一个组合就是将其他几种媒体资源组合成一个自定义的临时排列,再将这个临时排列视为一个可以呈现或处理的独立媒体项目。比如AVAsset对象,组合相当于包含了一个或多个给定类型的媒体轨道的容器。

AVCamposition中的轨道都是AVAssetTrack的子类AVCompositionTrack。

一个组合轨道本身由一个或多个媒体片段组成,有AVCompositionTrackSegment定义,代表组合中的实际媒体区域。

AVAsset到特殊媒体文件具有直接一对一的映射,组合的概念更像一组说明,描述多个资源间如何正确地呈现或处理。

AVComposition及其相关类没有遵循NSCoding协议,所以不能简单地将一个组合在内存的状态归档到磁盘上。如果需要创建具有保存项目文件能力的音频或视频编辑程序,需要自定义的数据模型类来保存这个状态。

AVComposition和AVCompositionTrack都是不可变对象,提供对资源的只读操作。

创建自己的组合,就需要使用AVMutableComposition和AVMutableCompositionTrack所提供的可变子类。

要创建自定义组合,需要指定在将要添加到组合的源媒体的时间范围,还要指定添加片段的每个轨道的位置。

二、组合的基础方法

(一)、创建资源

创建一个组合资源需要拥有一个或多个等待处理的AVAsset对象。创建资源用于组合时,需要创建AVAsset的子类AVURLAsset的实例。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
    
//AVURLAssetPreferPreciseDurationAndTimingKey值为YES确保当资源的属性使用
//AVAsynchronousKeyValueLoading协议载入时可以计算出准确的时长和时间信息。
//会对载入过程增加一些额外开销,但可以保证资源正处于合适的编辑状态。
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@YES};
    
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options:options];
NSArray *keys = @[@"tracks",@"duration",@"commonMetadata",];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
    
}];

(二)、创建组合资源

案例将从两个视频片段中第一个5秒视频拿出来,并按照组合视频轨道的顺序进行排列。还会从一个MP3文件将音频轨道整合到视频中。

  • 1、创建资源
NSURL *url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];
NSURL *url2 = [[NSBundle mainBundle] URLForResource:@"audio" withExtension:@"mp3"];
  
//AVURLAssetPreferPreciseDurationAndTimingKey值为YES确保当资源的属性使用
//AVAsynchronousKeyValueLoading协议载入时可以计算出准确的时长和时间信息。
//会对载入过程增加一些额外开销,但可以保证资源正处于合适的编辑状态。
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@YES};
    
AVAsset *videoAsset1 = [AVURLAsset URLAssetWithURL:url options:options];
AVAsset *videoAsset2 = [AVURLAsset URLAssetWithURL:url1 options:options];
AVAsset *audioAsset = [AVURLAsset URLAssetWithURL:url2 options:options];
  • 2、创建AVMutableComposition并添加两个轨道对象
//创建AVMutableComposition,并添加两个轨道对象。
AVMutableComposition *composition = [AVMutableComposition composition];
    
//创建组合轨道
AVMutableCompositionTrack *videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

//创建组合轨道时,必须指明它所能支持的媒体类型。并给出一个轨道标示符。preferredTrackID:,是一个32位整数值,这个标示符在之后返回轨道时会用到。一般来说都是赋给它一个常量。

kCMPersistentTrackID_Invalid表示需要创建一个合适轨道ID的任务委托给框架,标识会以1..n排列

创建两个轨道,包括video轨道和Audio轨道,后面可以分别向两个轨道组合添加内容

  • 3、将独立媒体片段插入到组合轨道
#pragma mark -- 将独立媒体片段插入到组合的轨道内。
    
//定义CMTime表示插入光标点,表示插入媒体片段的位置
CMTime cursorTime = kCMTimeZero;
    
//目标是捕捉每个视频前5秒的内容,创建一个CMTimeRange,从kCMTimeZero开始,持续时间5秒
CMTime videoDuration = CMTimeMake(5, 1);
CMTimeRange videoTimeRange = CMTimeRangeMake(kCMTimeZero, videoDuration);
    
//从第一个AVAsset中提取视频轨道,返回一个匹配给定媒体类型的轨道数组
//不过由于这个媒体只包含一个单独的视频轨道,只取第一个对象
AVAssetTrack *assetTrack;
assetTrack = [[videoAsset1 tracksWithMediaType:AVMediaTypeVideo] firstObject];
    
//在视频轨道上将视频片段插入到轨道中。
[videoTrack insertTimeRange:videoTimeRange ofTrack:assetTrack atTime:cursorTime error:nil];
    
//移动光标插入时间,让下一段内容在另一段内容最后插入。
cursorTime = CMTimeAdd(cursorTime, videoDuration);
    
//去资源视频轨道并将它插入组合资源的视频轨道上。
assetTrack = [[videoAsset2 tracksWithMediaType:AVMediaTypeVideo] firstObject];
[videoTrack insertTimeRange:videoTimeRange ofTrack:assetTrack atTime:cursorTime error:nil];
    
//希望音频轨道覆盖整个视频剪辑,充值cursorTime到kCMTimeZero
cursorTime = kCMTimeZero;
    
//获取组合资源的总duration值,并创建一个CMTimeRange,扩展为整个组合时长。
CMTime audioDuration = composition.duration;
CMTimeRange audioTimeRange = CMTimeRangeMake(kCMTimeZero, audioDuration);
    
//从音频资源中提取音频轨道并将它插入组合的音频轨道
assetTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
[audioTrack insertTimeRange:audioTimeRange ofTrack:assetTrack atTime:cursorTime error:nil];

视频轨道与音频轨道时分开的,所以时间上要单独处理。

组合的资源与其他Asset一样,可以播放、导出、处理。

三、导出组合

导出使用的是AVAssetExportSession,通过AVComposition及一个预设属性presetName初始化。

设置导出地址outputURL,以及导出文件类型outputFileType

NSString *preset = AVAssetExportPresetHighestQuality;
self.exportSession = [AVAssetExportSession exportSessionWithAsset:[self.composition copy] presetName:preset]

// 为会话动态生成一个唯一的输出URL,设置输出文件类型
self.exportSession.outputURL = [self exportURL];
self.exportSession.outputFileType = AVFileTypeMPEG4;

接下来便是导出过程

// 开始导出过程
[self.exportSession exportAsynchronouslyWithCompletionHandler:^{
    dispatch_async(dispatch_get_main_queue(), ^{
        // 导出完成回调        
    });
}];
@interface THCompositionExporter ()
@property (strong, nonatomic) id <THComposition> composition;
@property (strong, nonatomic) AVAssetExportSession *exportSession;
@end

@implementation THCompositionExporter

- (instancetype)initWithComposition:(id <THComposition>)composition {

    self = [super init];
    if (self) {
        _composition = composition;
    }
    return self;
}

- (void)beginExport {

    // Listing 9.9
    
    // 创建一个组合的可导出版本,返回一个AVAssetExportSession
    /**
     - (AVAssetExportSession *)makeExportable {
     
         // 创建新的AVAssetExportSession实例,得到一个AVMutableComposition副本。
         NSString *preset = AVAssetExportPresetHighestQuality;
         return [AVAssetExportSession exportSessionWithAsset:[self.composition copy] presetName:preset];
     }
     **/
    self.exportSession = [self.composition makeExportable];
    // 为会话动态生成一个唯一的输出URL,设置输出文件类型
    self.exportSession.outputURL = [self exportURL];
    self.exportSession.outputFileType = AVFileTypeMPEG4;
    
    // 开始导出过程
    [self.exportSession exportAsynchronouslyWithCompletionHandler:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            AVAssetExportSessionStatus status = self.exportSession.status;
            // 检查导出状态 ,成功保存到相册
            if (status == AVAssetExportSessionStatusCompleted) {
                [self writeExportedVideoToAssetsLibrary];
            } else {
                NSLog(@"Export Failed" message:@"Export failed");
            }
            
        });
    }];
    
    // 设置exporting为YES,通过监听该属性变化,呈现用户界面进展
    self.exporting = YES;
    
    // 轮询导出会话状态,确定当前进度
    [self monitorExportProgress];
    

}

// 监视导出过程
- (void)monitorExportProgress {

    // Listing 9.10
    
    double delayInsSeconds = 0.1;
    int64_t delta = (int64_t)delayInsSeconds * NSEC_PER_SEC;
    
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delta);
    
    // 短暂的延迟之后将一段执行代码放入队列,检查导出过程的状态
    dispatch_after(popTime, dispatch_get_main_queue(), ^{
        AVAssetExportSessionStatus status = self.exportSession.status;
        
        // 检查当初会话的status属性确定当前状态。
        // AVAssetExportSessionStatusExporting 用当前会话进度更新progress属性,并递归调用monitorExportProgress方法
        // 如果status属性返回其他值,这是exporting属性为NO,用户界面做出相应更新。
        if (status == AVAssetExportSessionStatusExporting) {
            self.progress = self.exportSession.progress;
            [self monitorExportProgress];
        } else {
            self.exporting = NO;
        }
        
    });
    
}

- (void)writeExportedVideoToAssetsLibrary {

    // Listing 9.11
    NSURL *exportUrl = self.exportSession.outputURL;
    // 将导出视频保存到相册
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:exportUrl]) {
        [library writeVideoAtPathToSavedPhotosAlbum:exportUrl completionBlock:^(NSURL *assetURL, NSError *error) {
            if (error) {
                NSLog(@"Write Failed" message:message);
            }
            [[NSFileManager defaultManager] removeItemAtURL:exportUrl error:nil];
        }];
    } else {
        NSLog(@"Video could not b exported to the assets library.");
    }
    
    
}

- (NSURL *)exportURL {
    NSString *filePath = nil;
    NSUInteger count = 0;
    do {
        filePath = NSTemporaryDirectory();
        NSString *numberString = count > 0 ?
            [NSString stringWithFormat:@"-%li", (unsigned long) count] : @"";
        NSString *fileNameString =
            [NSString stringWithFormat:@"Masterpiece-%@.m4v", numberString];
        filePath = [filePath stringByAppendingPathComponent:fileNameString];
        count++;
    } while ([[NSFileManager defaultManager] fileExistsAtPath:filePath]);

    return [NSURL fileURLWithPath:filePath];
}

@end

推荐阅读更多精彩内容