一、组合媒体
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 composition;
@property (strong, nonatomic) AVAssetExportSession *exportSession;
@end
@implementation THCompositionExporter
- (instancetype)initWithComposition:(id )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