9.1 組合媒体

想象一下最近如果去一趙旧金山,在那里拍摂了一些金冂公国、日本茶圓和漁人码头的短片。我們希望將这些视頻中的最好镜头组合在一起, 配上合适的音乐,做成一个可与朋友和家人一起分享的影片,如圏9-1所示。

要完成这一任务,我们首先需要將相美片段从这些源媒体中提取出来,再將它們組成一个临时排列。AV Foundation明確定于了这个功能,可以辻幵友者筒単地将多个音頻和視頻資源組合成一个新資源。圏9-2給出了框架提供的与資源組合相关的几个核心类。

AV Foundation有关资源组合的功能源于AVAsset的子类AVComposition。一个组合就是将其他几种媒体资源组合成一个自定义的临时排列,再将这个临时排列视为一个可以 呈现或处理的独立媒体项目。就比如AVAsset对象,组合相当于包含了一个或多个给定类型的媒体轨道的容器。AVComposition中的轨道都是AVAssetTrack的子类AVCompositionTrack。一个组合轨道本身由一个或多个媒体片段组成,由AVCompositionTrackSegment类定义,代表这个组合中的实际媒体区域。通常比较容易查看这些对象间的关系,因为它们与诸如iMovie或LogicPro X等编辑工具中所描绘的情况类似,如图9-3所示。

就像AVComposition扩展了AVAsset一样,所有可以使用常见资源的场景都可以使用这个对象,比如播放、图片提取或导出。不过组合被认为是更抽象的术语。不过AVAsset到特殊媒体文件具有直接一对一的映射,组合的概念更像是一组说明,描述多个资源间如何正确地呈现或处理。因此,这些一般都是轻量级的临时对象,我们在应用程序会频繁地创建和处理它们。

注意:
一个经常被问道的问题是,如何保存一个组合?简单回答就是不保存。AVComposition及其相关类没有遵循NSCoding协议,所以不能简单地将一个组合在内存的状态归档到磁盘上。如何我们正创建一个需要具有保存项目文件能力的音频或视频编辑应用程序,需要开发自定义的数据模型类来保存这个状态。

AVComposition和AVCompositionTrack都是不可变对象,提供对资源的只读操作。这些对象提供了一个合适的接口让应用程序的一部分可以进行播放或处理。不过,当创建自己的组合时,就需要使用AVMutableComposition和AVMutableCompositionTrack所提供的可变子类。这些对象提供的接口需要操作轨道和轨道分段,这样我们就可以创建所需的临时排列了。

要创建自定义组合,需指定在将要添加到组合的源媒体的时间范围,还要指定要添加片段的每个轨道的位置。在我们开始学习创建组合内容的技巧前,首先需要讨论时间和时间范围是如何在框架中呈现的。

9.2 时间的处理

第4章中曾简单讨论过有关时间的问题。要创建高效的组合内容,掌握有关时间和时间范围的相关知识非常重要,所以下面更深入地研究这一问题。

9.2.1 CMTime

通常开发者认为时间的呈现格式应该是浮点型数据。有着长期苹果平台开发经验的应用程序员一般都使用NSTimeInterval,它是一个简单的双精度类型,可以表示不同场景中的时间。实际上,AV Foundation在AVAudioPlayer和AVAudioRecorder中处理时间问题时本身也会使用这个类型。虽然很多通用的开发环境使用双精度表示时间已经足够满足要求了,不过其天然的不精确性导致双精度类型无法应用于更多的高级时基媒体的开发中。比如,一个单一舍入错误就会导致丢帧或音频丢失。相反,苹果公司使用CoreMedia框架定义的CMTime数据类型作为时间格式。C语言结构的类型定义如下所示:

typedef struct {CMTimeValue value;CMTimeScale timescale;CMTimeFlags flags;CMTimeEpoch epoch;
} CMTime;

上面结构中最相关的三个组件是value、timescale和flags。 CMTimeValue和CMTimeScale分别是64位和32位有符号整型变量,是CMTime元素的分数形式。CMTimeFlags是一个位掩码用于表示时间的指定状态,比如判断数据是否有效、不确定或是否出现舍入值等。CMTime实例可标记特定的时间点或用于表示持续时间。

有多种方法可以创建CMTime实例,不过最常见的方法是使用CMTimeMake函数,指定一个64位的value参数和一个32位的timescale参数。比如,创建一个代表3秒的CMTime表达式有下面几种不同的方式:

CMTime t1 = CMTimeMake(3, 1);
CMTime t2 = CMTimeMake (1800,600);
CMTime t3 = CMTimeMake (3000,1000) ;
CMTime t4 = CMTimeMake (132300,44100);

使用CMTimeShow函数将这些值打印到控制台将会出现如下结果:

CMTimeShow(t1); // --> {3/1 = 3.000}
CMTimeShow(t2); // --> {1800/600 = 3. 000}
CMTimeShow(t3); // --> {3000/1000 = 3.000}
CMTimeShow(t4); // --> {132300/44100 = 3. 000}

注意:
在处理视频内容时常见的时间刻度为600,这是大部分常用视频帧率24FPS、25FPS和30FPS的公倍数。音频数据常见的时间刻度就是采样率,比如44 100(44.1kHz)或48000(48kHz)。

除创建CMTime实例的函数外,我们可以看到在CMTime.h文件中定义了许多函数,这些函数都是用于有关时间的计算的。比如,在创建组合内容时经常会用到的时间相加和时间相减,使用CMTimeAdd和CMTimeSubtract函数可以很简 单地完成这些任务。

CMTime timel = CMTimeMake(5, 1);
CMTime time2 = CMTimeMake(3,1);CMTime result;result = CMTimeAdd (time1, time2);
CMT imeShow (result); // --> {8/1 = 8.000}result = CMTimeSubtract (time1, time2);
CMTimeShow (result); // --> {2/1 = 2.000}

通常在进行时间运算时,我们会使用Core Media框架所提供的函数,因为这些函数考虑了不同时间刻度和标志的问题。不过在有些情况下,使用小学数学的知识就够了。比如,可以通过操作其他时间刻度创建一个新的时间值。

CMTime time = CMT imeMake (5000,1000) ;CMTime doubledTime = CMTimeMake(time.value,time.timescale / 2);
CMTimeShow(doubledTime); // --> (5000/500 = 10. 000}CMTime halvedTime = CMTimeMake(time.value, time.timescale * 2);

就像AVComposition扩展了AVAsset一样,所有可以使用常见资源的场景都可以使用这个对象,比如播放、图片提取或导出。不过组合被认为是更抽象的术语。不过AVAsset到特殊媒体文件具有直接一对一的映射,组合的概念更像是一组说明,描述多个资源间如何正确地呈现或处理。因此,这些一般都是轻量级的临时对象,我们在应用程序会频繁地创建和处理它们。

上例展示了一些最常用的CMTime函数,不过我们还是鼓励开发者参阅有关CMTime的文档,了解其他一些有关创建、操作和比较CMTime数值的方法。除了大量的函数外,还会发现许多常用的常量和宏。

9.2.2 CMTimeRange

Core Media框架还为时间范围提供了一个数据类型,称为CMTimeRange,它在有关资源编辑的API中扮演着重要角色。CMTimeRange由两个CMTime值组成,第一个值定义时间范围的起点,第二个值定义时间范围的持续时间。

typedef struct {CMTime start;CMTime duration;
} CMTimeRange;

创建CMTimeRange的一个方法是使用CMTimeRangeMake函数,它的第一个参数是定义时间范围起点的CMTime值,第二个参数是表示范围持续时长的CMTime值。比如,如果我们要创建一个时间范围,从时间轴的5秒钟位置开始,持续时长5秒,则创建方法如下所示:

CMTime fiveSecondsTime = CMTimeMake(5, 1);
CMTimeRange timeRange = CMTimeRangeMake(fiveSecondsTime, fiveSecondsTime) ;
CMTimeRangeShow(timeRange); // --> {{5/1 = 5.000},{5/1 = 5.000}}

另一种创建时间范围的方法是使用CMTimeRangeFromTimeToTime函数。该函数通过表示范围起点和终点的CMTime值来创建一个CMTimeRange。比如,上面的示例还可 以这样实现:

CMTime fiveSeconds = CMTimeMake(5, 1);
CMTime tenSeconds = CMTimeMake(10, 1) ;
CMTimeRange timeRange = CMTimeRangeFromTimeToTime(fiveSeconds, tenSeconds) ;
CMTimeRangeShow(timeRange); // --> {{5/1 = 5.000},{5/1 = 5.000}}

CMTimeRange.h头文件定义了大量实用函数来处理时间范围相关的运算和比较。比如图9-4所示的是两个叠加的时间范围,每个都是5秒的时长,只是开始时间不同。

如果希望创建一个两个时间交叉的时间范围,或者希望得到两个时间范围的总和,可以 尝试下面给出的实现方法。

CMTimeRange rangel = CMTimeRangeMake(kCMTimeZero, CMTimeMake(5, 1));
CMTimeRange range2 = CMTimeRangeMake(CMTimeMake(2, 1), CMTimeMake(5, 1));CMTimeRange intersectionRange = CMT imeRangeGetIntersection(range1, range2) ;
CMTimeRangeShow(intersectionRange); // --> {{2/1 = 2.000},{3/1 = 3.000}}CMTimeRange unionRange = CMTimeRangeGetUnion(range1, range2);
CMTimeRangeShow(unionRange); // --> {{0/1 = 0.000}, {7/1 = 7.000}}

用有理数来表示时间在一开始看起来可能会比较奇怪,这也是框架的编辑功能带来的一个新困惑点。不过在创建自定义组合功能时掌握这一技术至关重要。好消息是使用CMTime和CMTimeRange很快就会习惯。在下面几章的学习中,我们会大量用到这些数据类型,所以不久你就会习惯使用它们了。

9.3 基础方法

了解到CMTime和CMTimeRange的用法后,下面继续讨论实现一个与图9-1类似的组合资源。

创建一个组合资源需要我们拥有一个或多个等待处理的源AVAsset对象。在开始介绍组合资源示例前,了解一下有关资源创建和准备的知识非常重要。本书中大部分的示例都使用assetWithURL:类方法来创建AVAsset实例。这里创建它的一个子类AVURLAsset的实例。当创建资源用于组合时,应该直接使用URLAssetWithURL:options:方法实例化一个AVURLAsset。options:参数允许通过传递一个带有一个或多 个初始化选项的NSDictionary来自定义资源初始化的方式。我们看一一个载入应用程序bundle中的MP4视频的示例。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"mp4"];NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey : @YES};
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options:options];// Asset "keys" to load
NSArray *keys = @[@"tracks", @"duration", @"commonMetadata"];[asset loadValuesAsynchronouslyForKeys:keys complet ionHandler:^{// Validate loaded status of keys
}];

框架定义了一个名为AVURLAssetPreferPreciseDurationAndTimingKey的选项。选项带有@YES值可以确保当资源的属性使用AVAsynchronousKeyValueLoading协议载入时可以计算出准确的时长和时间信息。虽然使用这个option时还会对载入过程增添一些额外开销,不过这可以保证资源正处于合适的编辑状态。

现在就下面继续并开始创建组合资源吧。这一组合 操作会将两个视频片段中第一个5秒视频拿出来,并按照组合视频轨道的顺序进行排列。组合资源还会从一个MP3文件中将音频轨道整合到视频中。

AVAsset *goldenGateAsset = // prepared golden gate asset
AVAsset *teaGardenAsset = // prepared tea garden asset
AVAsset *soundtrackAsset = // prepared sound track asset
AVMutableComposition *composition = [AVMutableComposition composition];// Video Track
AVMutableCompositionTrack *videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideopreferredTrackID:kCMPersistentTrackID_Invalid];// Audio Track
AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudiopreferredTrackID:kCMPersistentTrackIDInvalid];

上面的示例创建了一个AVMutableComposition并用它的addMutableTrackWithMediaType:preferredTrackID:方法添加了两个轨道对象。当创建组合轨道时,开发者必须指明它所能支持的媒体类型,并给出一个轨道标识符。设置preferredTrackID:参数为CMPersistentTrackID,这是一个32位的整数值。虽然我们可以传递任意标识符作为参数,这个标识符在我们之后需要返回轨道时会用到,不过一般来说都是赋给它一个kCMPersistentTrackID_ Invalid常量。 这个有着奇怪名字的常量的意思是我们需要将创建一个合适轨道ID的任务委托给框架,标识符会以..n排列。

现在我们已经实现了一个类似图9-5所示的组合资源了。组合的过程还是比较高效的,不过方法为我们提供了必要的轨道编排来将媒体片段添加到单独轨道中。

下一步就是将独立的媒体片段插入到组合的轨道内,如下面的代码示例所示。这段代码有点复杂,所以我们在相关的步骤周围加入了一些标注。

// The "insertion cursor" time
CMTime cursorTime = kCMTimeZero;                                                // 1CMTime videoDuration = CMTimeMake(5, 1) ;                                       // 2
CMTimeRange videoTimeRange = CMTimeRangeMake(kCMTimeZero, videoDuration);AVAssetTrack *assetTrack;// Extract and insert Golden Gate Segment
assetTrack =                                                                    // 3
[[goldenGateAsset tracksWithMediaType:AVMediaTypeVideo] firstobject];
[videoTrack insertTimeRange:videoTimeRangeofTrack:assetTrackatTime:cursorTimeerror:nil];// Increment cursor time
cursorTime = CMTimeAdd(cursorTime, videoDuration) ;                             // 4// Extract and insert Tea Garden segment
assetTrack = // 5
[[teaGardenAsset tracksWithMediaType:AVMediaTypeVideo] firstobject];
[videoTrack insertTimeRange:videoTimeRangeofTrack:assetTrackatTime:cursorTimeerror:nil];// Reset cursor time
cursorTime = kCMTimeZero;                                                       // 6
CMTime audioDuration = composition.duration;
CMTimeRange audioTimeRange = CMTimeRangeMake(kCMTimeZero, audioDuration) ;// Extract and insert Tea Garden segment
assetTrack =                                                                    // 7
[[soundtrackAsset tracksWithMediaType:AVMediaTypeAudio] firstobject];
[audioTrack insertTimeRange:audioTimeRangeofTrack:assetTrackatTime:cursorTimeerror:nil];

(1)定义一个CMTime来表示我们所指的插入光标点。轨道的这个时间点就是我们插入媒体片段的位置。
(2)我们的目标是捕捉每个视频前5秒的内容,所以创建一个CMTimeRange,令其从kCMTimeZero开始,持续时间为5秒。
(3)使用tracksWithMediaType:方法从第一个 AVAsset中提取视频轨道。这个方法会返回一个匹配给定媒体类型的轨道数组,不过由于这个媒体只包含一个单独的视频轨道,所以我们只取第一个对象。 之后在视频轨道上调用insertTimeRange:ofTrack:atTimerror方法将视频片段插入到轨道中。
(4)调用CMTimeAdd函数来移动光标的插入时间,将videoDuration和当前cursorTime值相加。这会向前移动光标,这样下一段内容就可以在另一段内容最后插入了。
(5)与第3步一样,提取资源视频轨道并将它插入组合资源的视频轨道上。
(6)我们希望使音频轨道覆盖整个视频剪辑,所以首先重新设置cursorTime回到kCMTimeZero。之后获取组合资源的总duration值并创建一个CMTimeRange,令它扩展为整个组合时长。
(7)从音频资源中提取音频轨道并将它插入组合的音频轨道。现在我们的组合资源如图9-6所示。

这个组合的资源现在与其他任何AVAsset一样,也就是说可以播放、导出或处理。利用多种媒体资源创建一个多轨道的组合是AV Foundation的一个强大且实用的功能。

9.4 15 Seconds示例应用程序

接下来的几章将创建一个视频编辑应用程序,名为15 Seconds,如图9-7所示。在Chapter9目录中可以找到一个名为FifteenSeconds_Starter的启动项目。

从名字大概可以推测出,这个应用程序会创建一个 15秒的媒体组合。应用程序会将目前为止我们所学到的知识进行整合,包括视频播放、读取元数据信息、图片提取,并为我们学习AVFoundation编辑功能打下坚实基础。下面先来讨论这个应用程序的一些关键功能来了解它的具体功能。

视图控制器的组合

应用程序包含3个不同的部分,分别是: 一个用于选择和预览音频及视频剪辑的媒体捕捉器,一个视频播放器和一个 允许排列组合中媒体剪辑的时间轴区域。图9-8给出了这个应用程序视图控制器对象的示意图。

THMainViewController是一个容器视图控制器,负责管理它的子类视图控制器并对应用程序中的请求处理进行集中协调。比如,当从THVideoPickerViewController或THVideoPickerViewController接收到一个预览媒体对象的请求时,选中的媒体对象会传递到主视图控制器,创建相应的AVPlayerItem,并将它传递给THPlayerViewController进行播放。同样,当用户点击播放器上的Play按钮时,主视图控制器会接收到一个请求, 获取THTimelineViewController的当前状态,并创建一个可播放格式的时间轴传递回播放视图控制器。我们不需要关心每个视图控制器的责任范围,因为这些视图控制器要么执行前几章中介绍的任务,要么执行现在不需要了解的任务。不过还是建议开发者详细了解应用程序的源代码,对这些代码段如何组织在一起有更深的认识。

对于这个应用程序,我们需要创建的部分是模型,这需要一些有 关应用程序数据模型的知识。图9-9给出了一个15 Seconds应用程序用到的组成数据模型的核心对象示意图。

THTimeline对象支持应用程序的时间轴区域。这个对象包含一个或多个THTimelineItem实例,用来表示我们在用户界面上看到的彩色方块。THTimelineItem 与AVFoundation没有依赖关系,只是定义了内容的时长和它在时间轴上的位置。当处理音频和视频媒体时,会用到时间轴项的子类THMedialtem。这个类是AVAsset实例的封装并负责为组合内容合理地载入和 准备资源。THMedialtem进一步子类化,分别针对视频和音频的处理给出THVideoItem和THAudioItem两个类。本章中,音频项和视频项的区别并不重要,不过在后续章中就要特殊区别对待它们了。

当请求播放当前时间轴状态时,THMainViewController会协同多个对象开始处理该请求。首先从时间轴视图控制器中获取当前的THTimeline实例,从相关的工场对象中获取THCompositionBuilder实例。THCompositionBuilder协议定 义了创建THComposition实例的接口。一个具体的组合创建器负责创建实际的AVComposition和相关的轨道和片段,并将它们封装到一个THComposition实例中。这是封装AVComposition实例的又一个协议类型,它提供了一些方法将组合对象转换为可播放或可导出的格式。核心的AV Foundation处理发生在THCompositionBuilder和THComposition对象上,所以本章和后续章节为这些类创建了具体的实现代码。

现在我们已经了解到应用程序和其相关的数据模型对象是如何组合在一起的,可以开始实现应用程序的具体功能了。

9.5 创建一个组合

第一项任务就是创建一个遵循THComposition协议的对象,如代码清单9-1所示。

代码清单9-1 THComposition 协议

#import <AVFoundation/AVFoundation.h>@protocol THComposition <NSObject>- (AVPlayerItem *)makePlayable;
- (AVAssetExportSession *)makeExportable;@end

这里为后面几章创建的应用程序定义一个通用的接口。这个接口用来创建一个组合的可播放版本和可导出版本,它将用于THMainViewController类。在这个协议中首先实现的类是THBasicComposition,如代码清单9-2所示。

代码清单9-2 THBasicComposition 接口

#import <AVFoundation/AVFoundation.h>
#import "THComposition.h"@interface THBasicComposition : NSObject <THComposition>@property (strong, readonly, nonatomic) AVComposition *composition;+ (instancetype)compositionWithComposition:(AVComposition *)composition;
- (instancetype)initWithComposition:(AVComposition *)composition;@end

这是一个简单接口,包括一个带有两个可变初始化方法和一个指向基础AVComposition对象的只读指针。下面为这个类添加具体实现代码, 如代码清单9-3所示。

代码清单9-3 THBasicComposition实现

#import "THBasicComposition.h"@interface THBasicComposition ()
@property (strong, nonatomic) AVComposition *composition;
@end@implementation THBasicComposition+ (id)compositionWithComposition:(AVComposition *)composition {return [[self alloc] initWithComposition:composition];
}- (id)initWithComposition:(AVComposition *)composition {self = [super init];if (self) {_composition = composition;}return self;
}- (AVPlayerItem *)makePlayable {                                            // 1return [AVPlayerItem playerItemWithAsset:[self.composition copy]];
}- (AVAssetExportSession *)makeExportable {                                  // 2NSString *preset = AVAssetExportPresetHighestQuality;return [AVAssetExportSession exportSessionWithAsset:[self.composition copy]presetName:preset];
}@end

(1) makePlayable方法会从基础AVComposition实例创建一个AVPlayerltem。AVComposition遵循NSCopying协议,所以正确的做法是通过调用copy方法得到这个对象的一个不可变副本。这样可以防止在呈现时对象状态发生改变。
(2)在makeExportable 方法内创建一个新的AVAssetExportSession实例,再次得到一个AVMutableComposition的副本,传递给初始化方法exportSessionWithAsset:presetName:.

这样我们就完成了THComposition协议实现。现在需要实现一个构建器来创建对象的实例。

创建一个组合构建器

对于每个THComposition实现,应用程序内都会有一个相应的THCompositionBuilder负责构建实例。这个协议如代码清单9-4所示。

代码清单9-4 THCompositionBuilder 协议

#import <AVFoundation/AVFoundation.h>
#import "THComposition.h"@protocol THCompositionBuilder <NSObject>- (id <THComposition>)buildComposition;@end

这个协议的具体实例负责提供buildComposition方法的实现,它会创建AVComposition以及相关的轨道和轨道片段。我们需要创建一个THBasicCompositionBuilder对 象实现该协议,代码清单9-5给出了这个类的接口。

代码清单9-5 THBasicCompositionBuilder 接口

#import "THCompositionBuilder.h"
#import "THTimeline.h"@interface THBasicCompositionBuilder : NSObject <THCompositionBuilder>- (id)initWithTimeline:(THTimeline *)timeline;@end

这个类遵循THCompositionBuilder协议并提供一个initWithTimeline:方法,参数为THTimeline的实例。时间轴对象保存所有关于THTimelineItem实例和其基础AVAsset实例的引用,创建一个AVComposition需要的所有资源都在这里。代码清单9-6给出了这个类的具体实现。

代码清单9-6 THBasicCompositionBuilder 实现

#import "THBasicCompositionBuilder.h"
#import "THBasicComposition.h"
#import "THFunctions.h"@interface THBasicCompositionBuilder ()
@property (strong, nonatomic) THTimeline *timeline;
@property (strong, nonatomic) AVMutableComposition *composition;
@end@implementation THBasicCompositionBuilder- (id)initWithTimeline:(THTimeline *)timeline {self = [super init];if (self) {_timeline = timeline;}return self;
}- (id <THComposition>)buildComposition {self.composition = [AVMutableComposition composition];                  // 1[self addCompositionTrackOfType:AVMediaTypeVideowithMediaItems:self.timeline.videos];[self addCompositionTrackOfType:AVMediaTypeAudiowithMediaItems:self.timeline.voiceOvers];[self addCompositionTrackOfType:AVMediaTypeAudiowithMediaItems:self.timeline.musicItems];// Create and return the basic composition                              // 2return [THBasicComposition compositionWithComposition:self.composition];
}- (void)addCompositionTrackOfType:(NSString *)mediaTypewithMediaItems:(NSArray *)mediaItems {// To be implemented
}@end

(1)在buildComposition方法中首先创建了一个新的AVMutableComposition实例。为时间轴中的每个元素调用私有方法addCompositionTrackOfType:withMedialtems。这个方法的具体实现稍后会介绍。
(2)最后,当组合对象和所有轨道都创建完毕后,还需要创建一个我们之前介绍过的THBasicComposition对象的实例,将最新创建的AVMutableComposition对象的引用传递给它。

下面就着手实现私有方法addCompositionTrackOfType:withMediaTtems,它在这个类中起到关键作用,如代码清单9-7所示。

代码清单9-7创建轨道内容

@implementation THBasicCompositionBuilder...- (void)addCompositionTrackOfType:(NSString *)mediaTypewithMediaItems:(NSArray *)mediaItems {if (!THIsEmpty(mediaItems)) {                                           // 1CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;AVMutableCompositionTrack *compositionTrack =                       // 2[self.composition addMutableTrackWithMediaType:mediaTypepreferredTrackID:trackID];// Set insert cursor to 0CMTime cursorTime = kCMTimeZero;                                    // 3for (THMediaItem *item in mediaItems) {if (CMTIME_COMPARE_INLINE(item.startTimeInTimeline,             // 4!=,kCMTimeInvalid)) {cursorTime = item.startTimeInTimeline;}AVAssetTrack *assetTrack =                                      // 5[[item.asset tracksWithMediaType:mediaType] firstObject];[compositionTrack insertTimeRange:item.timeRange                // 6ofTrack:assetTrackatTime:cursorTimeerror:nil];// Move cursor to next item timecursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);    // 7}}
}@end

(1)使用THIsEmpty函数,首先验证传进方法中的mediaItems数组是否为空。它是一个泛型函数,用于验证Foundation框架的各个类型是否包含有效内容。
(2)每次调用这个方法就会创建一个带 有指定媒体类型的AVMutableCompositionTrack实例。使用常量kCMPersistentTrackID_Invalid来 表示AV Foundation应该自动生成一个正确的轨道ID。这样轨道ID的序号就由1到n进行分配。
(3)创建一个新的带有kCMTimeZero常量的CMTime对象,它作为插入光标时间。
(4)执行一个快速检查来确定时间轴对象的startTimeInTimeline是否返回一个有效的CMTime值。时间轴视频和音乐轨道中包含的片段将为startTimeInTimeline返回kCMTimeInvalid,因为它们都是按序排列的,这就意味着应用程序不允许剪辑有间隙。不过配音轨道允许在时间轴的任意点上滑动剪辑。本例希望修改cursorTime来在时间轴上显示这个剪辑的用户定义的位置。
(5)从条目的基础AVAsset实例中提取与请求的mediaType相应的轨道,可能是AVMediaTypeVideo或AVMediaTypeAudio。这个方法返回一个NSArray数组,其中包含了与给定媒体类型匹配的轨道,不过所有示例项目中的媒体都只包含一个单独的给定类型的轨道,所以我们 只取数组中的第一个对象即可。
(6)使用条目指定的timeRange,在计算的cursor Time值处将媒体剪辑插入到组合轨道中。
(7)最后,将当前剪辑的时长数据和cursorTime相加,计算得到一个新的cursorTime值,这样就为下一个循环迭代设置了正确的时间点。

终于到了最后的关键时刻,下面运行应用程序并将3个视频剪辑从视频选取器中加入到时间轴,添加一个单独的音乐轨道和一个单独的配音轨道,点击Play按钮来欣赏我们的大作。可以拖拽配音剪辑到时间轴上的任意位置,改变视频剪辑的顺序并再次点击Play按钮。我们实现了一个真正的视频编辑器!

9.6 导出组合

可以创建一个组合一定会很有趣,不过如果不能把这些有创造力的结果分享到全世界岂不是毫无价值了。所以我们还要学习如何将组合导出。如果我们点击屏幕右上角的齿轮图标,可以看到有一个选项为Export Composition(导 出组合)。目前点击这个选项不会有任何动作,那下面就下面实现这个功能,这一过程会用到THCompositionExporter对象,如代码清单9-8所示。

代码清单9-8 THCompositionExporter 接口

#import "THTimeline.h"
#import "THComposition.h"@interface THCompositionExporter : NSObject@property (nonatomic) BOOL exporting;
@property (nonatomic) CGFloat progress;- (instancetype)initWithComposition:(id <THComposition>)composition;- (void)beginExport;@end

这个对象定义的一个核心方法称为beginExport,这个方法负责实际的导出过程。在导出过程中,exporting和progress属 性的状态通过THMainViewController监听,进而更新用户界面上的展示状态。代码清单9-9给出了这个类的具体实现。

代码清单9-9导 出组合

#import "THCompositionExporter.h"
#import "UIAlertView+THAdditions.h"
#import <AssetsLibrary/AssetsLibrary.h>@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 {self.exportSession = [self.composition makeExportable];                 // 1self.exportSession.outputURL = [self exportURL];self.exportSession.outputFileType = AVFileTypeMPEG4;[self.exportSession exportAsynchronouslyWithCompletionHandler:^{        // 2// To be implemented}];self.exporting = YES;                                                   // 3[self monitorExportProgress];                                           // 4
}- (void)monitorExportProgress {// To be implemented
}- (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

(1)首先创建一个组合的可导出版本。这会返回一个AVAssetExportSession实例,这个实例有基础AVComposition实例,且具有相应的会话预设值。我们为这个会话动态生成一个唯一的输出URL并设置输出文件类型。
(2)调用exportAsynchronouslyWithCompletionHandler:方法开始导出过程。我们暂时提供一个空的handler block作为方法的参数。
(3)设置exporting属性为YES。THMasterViewController会 监听该属性的变化,并呈现用 户界面的进展。
(4)最后,调用monitorExportProgress方法, 轮询导出会话的状态来确定当前进度。

继续看一下monitorExportProgress方法的实现,如代码清单9- 10所示。

代码清单9-10监视导出过程

@implementation THCompositionExporter...- (void)monitorExportProgress {double delayInSeconds = 0.1;int64_t delta = (int64_t)delayInSeconds * NSEC_PER_SEC;dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delta);dispatch_after(popTime, dispatch_get_main_queue(), ^{                   // 1AVAssetExportSessionStatus status = self.exportSession.status;if (status == AVAssetExportSessionStatusExporting) {                // 2self.progress = self.exportSession.progress;[self monitorExportProgress];                                   // 3} else {self.exporting = NO;}});
}...@end

(1)首先在一个短暂的延迟之后将一段执行代码放入队列,用来检查导出过程的状态。
(2)检查导出会话的status属性来确定其当前状态。如果状态返回AVAssetExport-SessionStatusExporting,则用当前导出会话的进度值更新progress属性,并递归调用monitorExportProgress方法。如果导出会话的status属性返回任何其他值,则设置exporting属性为NO,这样用户界面就可以相应地更新了。

最后一点需要实现的功能就是,处理导出语句并将导出的文件写入Assets Library,如代码清单9-11所示。

代码清单9-11处理导出语句

@implementation THCompositionExporter...- (void)beginExport {self.exportSession = [self.composition makeExportable];self.exportSession.outputURL = [self exportURL];self.exportSession.outputFileType = AVFileTypeMPEG4;[self.exportSession exportAsynchronouslyWithCompletionHandler:^{dispatch_async(dispatch_get_main_queue(), ^{                        // 1AVAssetExportSessionStatus status = self.exportSession.status;if (status == AVAssetExportSessionStatusCompleted) {[self writeExportedVideoToAssetsLibrary];} else {[UIAlertView showAlertWithTitle:@"Export Failed"message:@"The request export failed."];}});}];self.exporting = YES;[self monitorExportProgress];
}- (void)writeExportedVideoToAssetsLibrary {NSURL *exportURL = self.exportSession.outputURL;ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:exportURL]) {  // 2[library writeVideoAtPathToSavedPhotosAlbum:exportURL               // 3completionBlock:^(NSURL *assetURL,NSError *error) {if (error) {                                                    // 4NSString *message = @"Unable to write to Photos library.";[UIAlertView showAlertWithTitle:@"Write Failed"message:message];}[[NSFileManager defaultManager] removeItemAtURL:exportURL       error:nil];}];} else {NSLog(@"Video could not be exported to assets library.");}self.exportSession = nil;
}...@end

(1)在handler block中,首先调度回主队列并检查导出会话状态。如果导出成功完成,则将导出的视频文件写入Assets Library。如果导出失败,则弹出一个错误提示框给用户。
(2)创建一个新的ALAssetsLibrary实例。 在尝试写入库之前,最好验证一下即将写入的内容是否可以被写入,通过调用videoAtPathIsCompatibleWithSavedPhotosAlbum:方法进行这一验证。
(3)调用writeVideoAtPathToSavedPhotosAlbum:completionBlock:方法开始写入操作。 第一次尝试时,会接收到一个安全提示,询问是否同意向Photos Library写入内容,选择同意。
(4)最后,在completion handler中判断是否有错误出现。如果有,展示一个相应的错误提示框给用户。不管写入操作的结果如何,都需要将用户临时目录中的导出文件删除。

运行该应用程序,可手动创建一个新的组合或从菜单选择Load DefaultComposition选项。点击屏幕角落处的齿轮图标并选择Export Composition选项。将会看到视频播放器上出现一个表示导出进展的进度条。当导出完成后,可以切换到相机应用程序欣赏我们的作品。

9.7 小结

本章你第一次接触 到AV Foundation的媒体编辑功能。你使用AVComposition创建了一个多轨道的项目,将一些视频和音频资源进行组合创建出一个新的唯一的媒体片段。你还学习了如何像其他任何AVAsset那样,为播放和导出等任务使用组合。虽然我们的讨论集中在创建一个传统的视频编辑应用程序,不过会发现AVComposition其实适用于很多场景。

你已经开了一个好头,不过有趣的部分才刚刚开始!下面3章的内容将讨论如何实现剪切编辑的应用程序并为应用程序进一步添加诸多高级功能。

AVFoundation开发秘籍笔记:第9章 媒体的組合和編緝相关推荐

  1. AVFoundation开发秘籍笔记:第5章 AV Kit用法

    第4章中介绍了使用AVPlayer和AVPlayerltem创建一个自定义视频播放器的方法.创建一个自定义视频播放器在很多情况下是需要的,因为这样才可以对所有播放器行为和用户界面进行控制.不过我们是否 ...

  2. AVFoundation开发秘籍笔记:第12章 动图层内容

    12.1 Core Animation应用 Core Animation是OS X和iOS平台提供的用于合成和制作动画的框架,苹果平台上面漂亮.流畅的动画效果都是通过这个框架实现的.它提供- -种简 ...

  3. AVFoundation开发秘籍笔记-06捕捉媒体

    一.捕捉功能 1.捕捉会话 AVCaptureSession AVFoundation捕捉栈的核心类是AVCaptureSession.一个捕捉会话相当于一个虚拟的"插线板",用于 ...

  4. AVFoundation开发秘籍笔记-03资源和元数据

    一.资源AVAsset AVAsset是一个抽象类和不可变类,定义媒体资源混合呈现的方式,将媒体资源的静态属性模块化成为一个整体,比如标题.时长和元数据等. AVAsset不需要考虑媒体资源所具有的两 ...

  5. AVFoundation开发秘籍笔记-02播放和录制音频

    一.音频会话 AVAudioSession 音频会话在应用程序和操作系统之间扮演着中间人的角色,提供一种简单实用的方法是OS得知应用程序应该如何与iOS音频环境进行交互. AVAudioSession ...

  6. TMS320C6748开发视频教程笔记 第6章 GEL 文件

    第6章 GEL 文件 6-1 回调函数 6-2 关键字及内建函数 6-1 回调函数 视频地址:回调函数   GEL(General Extension Language)通用扩展语言.   熟悉:使用 ...

  7. TMS320C6748开发视频教程笔记 第12章 通用输入输出口 GPIO

    第12章 通用输入输出口 GPIO General-purpose input/output 12-1 相关例程演示 12-2 管脚复用配置 12-3 输入输出模式 12-4 中断 12-5 EDMA ...

  8. TMS320C6748开发视频教程笔记 第7章 启动与烧写

    第7章 启动与烧写 7-1 启动流程及方式 7-2 AIS文件结构 7-3 通过串口加载运行程序 7-4 配置并生成AIS文件 7-5 程序烧写 7-4-1 CCS烧写 7-4-2 串口烧写 7-4- ...

  9. TMS320C6748开发视频教程笔记 第9章 中断与异常

    第9章 中断与异常 9 中断与异常 9-1 中断 9-1-1 中断类型 9-1-2 中断结构 9-1-3 中断事件选择 9-1-4 中断错误事件 9-1-5 中断向量表 9-1-6 中断嵌套 9-2 ...

最新文章

  1. 独家 | 微软与哈佛大学定量社会科学研究所合作开发开放数据差异隐私平台,开启研究新征程(附链接)...
  2. vuepdf转换html,Vue网页html转换PDF(最低兼容ie10)的思路详解
  3. android sharesdk分享功能,Android ShareSDK快速实现分享功能
  4. 【视频】视频文件格式和视频编码
  5. 解码(一):AVCodecContext解码上下文初始化讲解
  6. 记一次微信数据库解密过程
  7. A - Cube Stacking(带权并查集)
  8. Ubuntu打印机控制命令笔记
  9. 二元函数求最小值 c语言,最小二乘法及C语言实现
  10. Android apk签名详解——AS签名、获取签名信息、系统签名、命令行签名
  11. WebBrowser怎么指定ie内核
  12. Alpha版本冲刺(八)
  13. 《Effective C++》读书笔记 条款40:明智而审慎地使用多重继承
  14. 转 js控制excel打印完美解决方案
  15. WinSCP 连接 AWS 服务器
  16. 踩坑记录丨记Jekyll + Github Pages搭建个人博客时遇到的各种问题
  17. 电脑加载,电脑加载慢又卡怎么办
  18. RS485方向切换的几种方案及优劣势分析
  19. 联想电脑怎么进bios设置U盘为第一启动项
  20. 下一代5G网络:比4G快1000倍 普及至少需8年

热门文章

  1. 一阶逻辑等值式及前束范式
  2. 学习笔记 - 小常识
  3. C#下生成CSR证书签名请求
  4. 华为命令手册_网络故障排查之tracert命令详解
  5. BUUCTF-MD5强弱比较-MD5()的万能密码-tornado框架注入-中文电码
  6. 外贸助手 - 网页版
  7. android开发教程(一)——目录
  8. python获取期货实时行情_量化投资者是如何获取实时行情数据的呢?
  9. Hackme-1靶机
  10. 2021年登高架设考试技巧及登高架设复审模拟考试