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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容