第4章中介绍了使用AVPlayer和AVPlayerltem创建一个自定义视频播放器的方法。创建一个自定义视频播放器在很多情况下是需要的,因为这样才可以对所有播放器行为和用户界面进行控制。不过我们是否希望既能利用AV Foundation强大的功能,又可以提供给用户熟悉的界面和优秀的体验呢,就如同使用iOS.上的Video应用程序或Mac OS X上的QuickTime播放器一样的感觉。要取得同样的效果和体验还需要进行大量的工作,并且还需要开发者为iOS或Mac OSX的不同版本编写多个用户界面。电视广“告会这样说:我们一定会找到更好的办法!幸运的是得益于AV Kit框架,还真有更好的办法。
AV Kit可以简化基于AV Foundation框架且满足默认操作系统视觉效果和体验的视频播放器的创建过程。AV Kit框架第一次出现是在Mac OS X Mavericks版本中,并从iOS 8开始被引入到iOS平台。本章我们集中讨论AV Kit在OS X上的应用,因为它比iOS版本的功能更多,不过下面先简单看一下AV Kit的iOS版本都能做些什么。
5.1 针对 iOs平台的AV Kit框架
iOS Media Player 框架最初就定义了MPMoviePlayerController 和MPMoviePlayerViewController两个类,它们提供一种简单的方法将完整的视频播放功能整合到应用程序中。MPMoviePlayerController定义了一些标准的播放控件,这些控件可以以子视图或全屏的方式内置于应用程序中,并支持通过AirPlay连接的音频流和视频流,同时还包含很多其他实用的功能。已经有这么多的功能,为什么我们还需要其他功能? MPMoviePlayerController的一个关键问题是它是一个极度的黑盒组件,它基于AVFoundation之上,但是不幸的是它将所有的基础功能都隐藏了。这就导致开发者无法使用AVPlayer和AVPlayerItem提供的一些更高级功能。从iOS 8版本开始,AV Kit框架的引入带来了更多强大的功能。
针对iOS平台的AV Kit是一个简单的标准框架一只 包含一个AVPlayerViewController类。它是UIViewController的子类,用于展示并控制AVPlayer实例的播放。AVPlayerViewController具有一个很小的界面,提供以下几个属性:
●player: 用来播放媒体内容的AVPlayer实例。
●showsPlaybackControls: 一个用来表示播放控件是否显示或隐藏的布尔类型的值。
●videoGravity: 对内部AVPlayerLayer实例的video gravity进行设置的一个 NSString。如果需要对AVPlayerLayer的video gravity概念进行复习,可参阅第4章。
●readyForDisplay: 通过观察这个布尔类型的值来确定视频内容是否已经准备好进行展示。
虽然界面非常朴实,不过这个类可以提供许多实用值。要证明这一点, 请查看下面给出的示例。
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchoptions{
NSURL *url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@"m4v"];
AVPlayerViewController *controller = [[AVPlayerViewController alloc] init];
controller.player = [AVPlayer playerWithURL:url];
self.window.rootViewController = controller;
return YES;
}
@end
该示例中创建了一个新的AVPlayerViewController,为它设置了一个AVPlayer实例,并将其控制器设置为窗口的rootViewController。虽然我们只写了短短几行代码,不过运行这些代码就会显示一个播放器,如图5-1所示。
如图5-1所示,AVPlayerViewController在没有编写过多代码的前提下实现了许多功能。创建了一个带有与iOS视频播放器同样用户界面和体验的功能齐全的播放器。由于这个类是UIViewController子类,所以很容易将它作为子视图控制器嵌入到其他视图中,或向其他视图控制器一样呈现。
注意:
示例中使用playerWithURL:便捷初始化方法创建了一个AVPlayer。当然这的确是一个比较简单的情况,不过在开发者需要更多控件时,可以像上一章中介绍的那样设置完整的播放栈。
与MPMoviePlayerController不同,AVPlayerViewController没 有定义controlsStyle属性用于表示所使用的播放控件。取而代之的是,它提供了一种动态播放控件,可以动态更新,为播放的内容呈现所需的用户界面。这意味着播放器总是带给用户最好的体验,无论是通过章节标签播放本地视频还是播放带有字幕的流媒体视频。图5-2给出了实际应用中的几种备用控件。
AVPlayerViewController是iOs 8给出的一个简单但功能强大的新类。它提供了一个非常强大的替代方法,即使用MPMoviePlayerController,因为其具有全部AV Foundation栈的特性,支持开发者使用更高级的功能。要了解AVPlayerViewController类的其他信息,可以参考WWDC 2014的Session 503: Mastering Modern Media Playback.
5.2 针对MacOSX平台的AVKit框架
Mac平台的AV Kit框架首先在Mac OS X Mavericks版本中引入。该框架定义了一个名为AVPlayerView的类,通过这个类可以简单地将完整的视频播放功能整合到Mac应用程序中。它所定义的界面和使用体验同QuickTimePlayer x一样,能够提供Mac用户熟悉的功能和用户界面效果。
AVPlayerView是一个NSView子类,用于展示和控制AVPlayer实例的播放。在使用AVPlayerView对象的过程中上一章我们所学到的知识同样适用,不过开发一款带 有标准化播放控件和功能的视频播放应用程序会变得更加快速和简单。它还自动支持所有标准的最新OSX功能,比如本地化、状态恢复、全屏播放、高分辨率展示和辅助功能等。
5.3 迈出第一步
本节通过创建一个基于AVKit框架的视频播放器来学习如何使用AVPlayerView。在Chapter 5目录中可以找到名为KitTimePlayer_starter的示例项目。虽然本书的大部分示例应用 程序都将AV Foundation有关的代码分解出来到放到自己的类集合中,不过由于我们现在讨论的是针对Mac系统的开发,所以我们需要在主NSDocument实例中开发这个应用程序。现在打开项目开始编码吧!
首先需要配置的是对KitTimePlayer的目标DocumentTypes进行设置。选中ProjectNavigator中的项目根节点使其处于高亮状态,并选择KifTime Player目标。选择Info选项卡并展开Document Types部分,如图5-3所示进行修改。
●将mydoc扩展从Extensions字段中移除,因为应用程序将不会创建自定义文档类型。.
●设置 Identifer字段为public.audiovisual-content.选择Uniform Type Identifer 将允许应用程序打开所有视听媒体。
●将Role设置为Viewer, 这样当启动应用程序时不会创建一个新的文档窗口。
接下来,在Project Navigator中选择THDocument.xib。NSWindow已经被设置为640X 360来适配即将播放的视频内容的宽高比,不过开发者可以根据需求自定定义该尺寸。在ObjectLibrary的搜索框中,输入AVPlayerView直到看见AV Player View组件。拖曳一个它的实例到窗口中并将它置于窗口区域的中间,如图5-4所示。
当用户改变窗口大小时需要确保AVPlayerView也进行相应的变化,开发者需要为其添加一个合适的Auto Layout约束。这种情况下最简单的方法就是选择Resolve Auto LayoutIssues按钮并选择Add Missing Constraints inWindow,如图5-5所示。
找到播放视图的属性查看器,设置Controls Style属性为Floating,如图5-6所示。
上述方法所创建的用户界面与QuickTime Player界面具有同样的视觉效果。
THDocument实例已经为播放器视图定义了一个IBOutlet,不过我们还是需要将播放器关联到IBOutlet。按住Control从File's Owner代理拖曳到AVPlayerView实例,并选择playerViewOutlet,如图5-7所示。
目前所需的Interface Builder已经配置完成了,下面介绍THDocument.m文件。代码清单5-1给出了这个类的最初实现。
代码清单5-1 Document实现
#import "THDocument.h"
#import <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
@interface THDocument ()
@property (weak) IBOutlet AVPlayerView *playerView;
@end
@implementation THDocument
#pragma mark - NSDocument Methods
- (void)windowControllerDidLoadNib:(NSWindowController *)controller {
[super windowControllerDidLoadNib:controller];
}
- (NSString *)windowNibName {
return @"THDocument";
}
- (BOOL)readFromURL:(NSURL *)url
ofType:(NSString *)typeName
error:(NSError *__autoreleasing *)outError {
return YES;
}
@end
这只是NSDocument子类的梗概,不过它具有创建视频播放器应用程序所需的全部核心元素。唯一欠缺的就是AV Foundation代码。将下面两行代码添加到windowControllerdLoadNib:方法的结尾处。
self.playerView.player = [AVPlayer playerWithPlayerItem:self.playerItem];
self.playerView.showsSharingServiceButton = YES;
将这些代码添加好就可以运行应用程序了。当应用程序启动后,选择File菜单,点击Open,然后选择Chapter 5目录下的hubblecast.m4v文件并点击应用程序的Play按钮。我们只花了几分钟时间和短短几行代码就实现了一个播放器应用程序,如图5-8所示。
AVPlayerView会自动提供标准的播放控件,一个视频搓擦条、一个音量控制器、动态的章节和字幕菜单,以及一个允许与Mac OS X提供的标准目标进行分享的分享服务菜单。显然,AVKit对于Mac平台是一个非常好的补充。虽然在之后我们会学到更多方法,不过这已经是一个令人激动的开端了。现在把视线转移到AVPlayerView提供的各种控件类型上。
5.4 控件类型
AVPlayerView定义了大量的控件类型供开发者选用。可在Interface Builder中 直接修改这些属性,我们之前就是这样做的,或以编程方式修改controlsStyle属性进行编辑。下面介绍该类定义的各个类型。
5.4.1 内嵌类型
内嵌类型(AVPlayerViewControlsStyleInline)是AVPlayerView使用的默认类型,如图5-9所示。这个类型与QTKit Framework的QTMovieView界面非常相似,并定义了标准播放栏、搓擦条和音量控制器。当媒体具有动态章节和字幕数据时,它还可以支持动态章节和字幕菜单的展示。
5.4.2 浮动类型
浮动类型(AVPlayerViewControlsStyleFloating)的效果与当前版本的QuickTimePlayer效果(如图5-10所示)一致。实际LQuickTime Player的Mavericks版本就是使用的AVPlayerView,所以它所使用的界面对于大部分Mac用户而言很快就会熟悉。与内嵌类型一样,浮动类型也定义了标准的播放栏、搓擦条和音量控件,如果视频包含章节分段和字幕,同样会自动显示章节信息和字幕菜单。
[图片上传失败...(image-e37f08-1596863857234)]
5.4.3 最小化类型
最小化类型(AVPlayerViewControlsStyleMinimal)在屏幕中间定义一个圆形浮动按钮,该按钮用来展示带有圆形进度指示图标的播放或暂停按钮,如图5-11所示。 当播放需要最小化控件的短视频时,可以选择该方案。
5.4.4 None 类型
None(AVPlayerViewControlsStyleNone)类型实际上就是没有类型。选择这一类型则不会出现播放控件,只简单展示视频的内容,如图5-12所示。 如果开发者希望使用自定义的播放控件或在一些没有特定控件需求的场景下,使用None类型很有用。
不管选择了哪个类型,AVPlayerView都 会对标准键盘集指令进行反馈。空格键可以对视频进行播放和暂停。左右箭头可以逐帧调整视频。此外,播放器视图支持J-K-L导航,即J键对应回放,L键对应快进和K键对应停止播放。
注意:
如果正在播放一个HTTP Live Streaming视频,AVPlayerView 会根据控件类型自动切换为备用控件,以满足流媒体的播放要求。
注意:
只有浮动类型和内嵌类型可以通过设置showsSharingServiceButton 属性为YES,来支持分享服务的菜单。
5.5 拓展学习
我们现在已经学习了如何通过AV Kit快速创建并运行应用程序,不过我希望进一步深入了解AV Kit框架,并在应用程序中加入一些更高级的功能。要实现这些功能,需要从媒体栈开始进行一些修改。 AVPlayer包 含了一个playerWithURL:方法,可以快速创建基础AVAsset和AVPlayerltem实例并为这些对象执行相关的准备工作。使用这个方法虽然很简单,不过通常当开发者需要直接使用这些对象时,会发现还是明确创建和准备这些对象用起来更加简单。开发者将会用到与上一章 设置AV Foundation播放栈类似的基本技巧,如代码清单5-2所示。
代码清单 5-2 设置播放栈
#import "THDocument.h"
#import <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
#define STATUS_KEY @“status"
@interface THDocument ()
@property (strong) AVAsset *asset; // 1
@property (strong) AVPlayerItem *playerItem;
@property (strong) NSArray *chapters;
@property (weak) IBOutlet AVPlayerView *playerView;
@end
@implementation THDocument
- (void)windowControllerDidLoadNib:(NSWindowController *)controller {
[super windowControllerDidLoadNib:controller];
[self setupPlaybackStackWithURL:[self fileURL]]; // 2
}
- (void)setupPlaybackStackWithURL:(NSURL *)url {
self.asset = [AVAsset assetWithURL:url];
NSArray *keys = @[@"commonMetadata", @"availableChapterLocales"]; // 3
self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset // 4
automaticallyLoadedAssetKeys:keys];
[self.playerItem addObserver:self // 5
forKeyPath:STATUS_KEY
options:0
context:NULL];
self.playerView.player = [AVPlayer playerWithPlayerItem:self.playerItem];
self.playerView.showsSharingServiceButton = YES;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:STATUS_KEY]) {
if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
NSString *title = [self titleForAsset:self.asset]; // 6
if (title) {
self.windowForSheet.title = title;
}
self.chapters = [self chaptersForAsset:self.asset]; // 7
}
[self.playerItem removeObserver:self forKeyPath:STATUS_KEY];
}
}
- (NSString *)titleForAsset:(AVAsset *)asset {
return nil;
}
- (NSArray *)chaptersForAsset:(AVAsset *)asset {
return nil;
}
(1)为AVAsset、AVPlayerltem和NSArray添加三个新属性,用于保存即将捕捉的章节数据。
(2)删除简单的AVPlayer创建代码,并将设置代码提取到一个新方法中,该方法名为setupPlaybackStackWithURL:,这个方法会在windowControllerDidI oadNib:方法中被调用。
(3)创建一个NSArray来保存commonMetadata和availableChapterLocales键。这些都是我们希望载入的AVAsset键,因为它们能使开发者对资源进行更深入的调查。
(4)使用新的playerltemWithAssetautomaticallyLoadedAssetKeys:便捷初始化方法创建一个AVPlayerltem。这是Mac OS X 10.9和iOS 7.0版本中新增的实用功能,简化了载入AVAsset键值的过程。
(5)添加self作为播放条目status属性的监听器,这样当播放条目处于准备播放状态时应用程序就会得到通知。不过前提是被请求的资源属性先载入进来并且播放通道已经准备好。
(6)如果播放条目的状态为AVPlayerltemStatusReadyToPlay,可以放心地调用请求载入的AVAsset属性。
(7)调用titleForAsset:方法获取播放 资源的标题。如果有可用的标题内容返回,将它设置为窗口的title属性。如果没有,则以资源的文件名作为标题展示。后面会给出titleForAsset:方法的实现,不过现在我们只给出一个存根实现令其返回nil。
(8)调用chaptersForAsset:方法获取当前资源中保存章节书签的对象数组。稍后会讨论如何获取章节数据,不过现在我们仅给出chaptersForAsset:方法的存根实现。
我们所做的修改并没有改变应用程序的功能,只是为下面将要介绍的功能做准备。作为完整性检查,再次运行应用程序并确保在功能方面同之前的应用程序保持一致。
在为应用程序添加更多功能之前,下面看一下如何实现titleForAsset:方法,如代码清单5-3所示。这一方法用到了前两章所介绍的AV Foundation标准元数据功能。
代码清单5-3 titleForAsset:的实现
- (NSString *)titleInMetadata:(NSArray *)metadata {
NSArray *items = // 1
[AVMetadataItem metadataItemsFromArray:metadata
withKey:AVMetadataCommonKeyTitle
keySpace:AVMetadataKeySpaceCommon];
return [[items firstObject] stringValue]; // 2
}
- (NSString *)titleForAsset:(AVAsset *)asset {
NSString *title = [self titleInMetadata:asset.commonMetadata]; // 3
if (title && ![title isEqualToString:@""]) {
return title;
}
return nil;
}
(1)添加一个新的titleInMetadata:方法,将标题信息从AVMetadataItem实例集合中解析出来。通过调用metadataltemsFromArray:withKey:keySpace:类方法在通用键空间中获取标题对应的键。
(2)获取这个数组中的firstObject并得到对应的NSString值。通用键空间中的标题值类型都是字符串,所以可以对它们使用stringValue,强制转换为NSString类型。
(3)调用titleInMetadata:方法,并将资源对象的commonMetadata作为参数。如果找到有效的标题值,则返回给调用函数,否则返回nil。
再次运行应用程序,Chapter 5目录中的视频文件包含标题元数据,所以我们可以看到窗口的标题栏中会显示资源的标题。
5.6 章的处理
如果使用浮动控件类型或内嵌控件类型的话,AVPlayerView在视频文 件具有可以展示的章数据情况下会自动显示章菜单。虽然这一做法非常方便,但是如果需要直接使用章数据实现一些额外功能,或应用程序所使用的控件类型不支持自动章菜单该怎么办呢?幸运的是,AVFoundation可以通过AVTimedMetadataGroup类直接处理章数据。
时间相关的元数据与第3章介绍的静态元数据同样重要,不过和将其作为一个整体应用于资源不同的是,时间相关元数据只用于资源时间轴内特定的时间范围。AVAsset定义了两个方法可以获取这个数据:
chapterMetadataGroupsWithTitleLocale:containingItemsWithCommonKeys:
chapterMetadataGroupsBestMatchingPreferredLanguages:
这两个方法返回AVTimedMetadataGroup对象的NSArray数组,数组中的对象为资源中包含的章元数据。从方法签名就可以推断出,章数据取决于位置。在调用这两个方法前,首先需要确保资源的availableChapterLocales键已经载入,如下面代码所示。
NSURL *url = // asset URL;
AVAsset *asset = [AVAsset assetWithURL:ur1];
NSString *key = @"availableChapterLocales";
[asset loadValuesAsynchronouslyForKeys:@[key] completionHandler:^{
AVKeyValueStatus status = [asset statusOfValueForKey:key error:nil];
if (status == AVKeyValueStatusLoaded) {
NSArray *langs = [NSLocale preferredLanguages];
NSArray * chapte rMetadata = [asset chapterMetadataGroupsBestMatchingPreferredLanguages:langs];
// Process AVTimeMetadataGroup objects
}
}];
AVTimedMetadataGroup包含两个属性: timeRange 和items。timeRange属性保存着一个CMTimeRange结构,该结构包含用于表示时间范围起点的CMTime值和定义资源时长的CMTime值。这允许确定章元数据所对应的资源时间轴上的时间范围。章的标题和作为可选项的缩略图可在items属性中找到,items属性包含了一个由来自于通用键空间的AVMetadataltem对象组成的NSArray。
代码清单5-4给出了chaptersForAsset:方法在具体实践中的实现。
代码清单5-4 chaptersForAsset:方法的实现
- (NSArray *)chaptersForAsset:(AVAsset *)asset {
NSArray *languages = [NSLocale preferredLanguages]; // 1
NSArray *metadataGroups = // 2
[asset chapterMetadataGroupsBestMatchingPreferredLanguages:languages];
NSMutableArray *chapters = [NSMutableArray array];
for (NSUInteger i = 0; i < metadataGroups.count; i++) {
AVTimedMetadataGroup *group = metadataGroups[I];
CMTime time = group.timeRange.start;
NSUInteger number = i + 1;
NSString *title = [self titleInMetadata:group.items];
THChapter *chapter = // 3
[THChapter chapterWithTime:time number:number title:title];
[chapters addObject:chapter];
}
return chapters;
}
(1])首先查询NSLocale的preferredL anguages数组,这将返回根据用户语言首选项排序的语言编码。我们的示例中,第一个 元素就是对应英文语言的en。
(2)获取资源中与用户首选语言最匹配的章元数据组。
(3)遍历每个AVTimedMetadataGroup对象并提取对应的相关数据。具体来说我们需要获取开始时间timeRange并通过我们之前定义的titleInMetadata:方法获取标题信息。同时还要基于当前循环索引为每章创建章号。将这个数据保存在一个 自定义对象THChapter中,之后使用该数据时就会很方便。
我们已经成功获取章节元数据并将它妥善封装到THChapter对象集合中,那么如何使用这些信息呢?通过利用AVPlayerView给出的另一个自定义点来实际使用这些数据。AVPlayerView 定义了一个actionPopUpButtonMenu方法让开发者向播放器控件添加自定义NSMenu。我们创建一个菜单用来实现跳转到下一章和前一章的功能。如代码清单5-5所示,首先构建NSMenu。
代码清单5-5创建一个Action Menu
- (void)setupActionMenu {
NSMenu *menu = [[NSMenu alloc] init]; // 1
[menu addItem:[[NSMenuItem alloc] initWithTitle:@"Previous Chapter"
action:@selector(previousChapter:)
keyEquivalent:@""]];
[menu addItem:[[NSMenuItem alloc] initWithTitle:@"Next Chapter"
action:@selector(nextChapter:)
keyEquivalent:@""]];
self.playerView.actionPopUpButtonMenu = menu; // 2
}
- (void)previousChapter:(id)sender {
}
- (void)nextChapter:(id)sender {
}
(1)创建一个新的NSMenu实例,添加"Previous Chapter"(上一章)和"Next Chapter"(下一章)菜单项,分别取名previousChapter:和nextChapter:选择器。现在暂时提供这两个方法的存根实现。
(2)将这个菜单设置到播放视图的actionPopUpButtonMenu属性。当使用浮动类型控件或内嵌类型控件时就会显示该菜单。
在添加菜单前,首先需要调用setupActionMenu方法。修改observeValueForKeyPath:方法,如代码清单5-6所示。
代码清单5-6修改observeValueForKeyPath:方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
NSString *title = [self titleForAsset:self.asset];
if (title) {
self.windowForSheet.title = title;
}
self.chapters = [self chaptersForAsset:self.asset];
// Create action menu if chapters are available
if ([self.chapters count] > 0) {
[self setupActionMenu];
}
}
[self.playerItem removeObserver:self forKeyPath:STATUS_KEY];
}
我们希望在资源中存在章节数据时才显示这个菜单,如果chapters数组非空,就调用setupActionMenu方法。
再次运行应用程序并确认菜单是否显示。点击菜单项,不过由于我们还没有实现这些方法,所以什么也不会发生。在为这些方法添加具体实现前,首先需要添加一些额外的辅助代码来找到具体的上一章和下一章(如代码清单5-7所示)。下面的代码就开始变得相对复杂一些,也是我们第一次看到Core Media类型和函数的高级用法,所以我们仔细研究一下这些方法。
代码清单5-7查找章节
- (THChapter *)findPreviousChapter {
CMTime playerTime = self.playerItem.currentTime;
CMTime currentTime = CMTimeSubtract(playerTime, CMTimeMake(3, 1)); // 1
CMTime pastTime = kCMTimeNegativeInfinity;
CMTimeRange timeRange = CMTimeRangeMake(pastTime, currentTime); // 2
return [self findChapterInTimeRange:timeRange reverse:YES]; // 3
}
- (THChapter *)findNextChapter {
CMTime currentTime = self.playerItem.currentTime; // 4
CMTime futureTime = kCMTimePositiveInfinity;
CMTimeRange timeRange = CMTimeRangeMake(currentTime, futureTime); // 5
return [self findChapterInTimeRange:timeRange reverse:NO]; // 6
}
- (THChapter *)findChapterInTimeRange:(CMTimeRange)timeRange
reverse:(BOOL)reverse {
__block THChapter *matchingChapter = nil;
NSEnumerationOptions options = reverse ? NSEnumerationReverse : 0;
[self.chapters enumerateObjectsWithOptions:options // 7
usingBlock:^(id obj,
NSUInteger idx,
BOOL *stop) {
if ([(THChapter *)obj isInTimeRange:timeRange]) { // 8
matchingChapter = obj;
*stop = YES;
}
}];
return matchingChapter; // 9
}
(1)要查找前一章,首先定义两个CMTime值。第一个值为播放控件当前时间减掉3秒,第二个值是使用kCMTimeNegativeInfinity常量定义一个当前时间之前的无限大时间。如果视频正处于播放状态,则时间会不断向前增加,所以当用户选择菜单项时应该留一些时间余地。如果不这么做,用户就会陷入再次回到章起始时间的循环中。当计算currentTime时我们对它使用CMTimeSubtract函数减掉3秒,并以此结果值作为currentTime。
(2)使用CMTimeRangeMake函数创建CMTimeRange,将pastTime作为时间范围的start值,将currentTime作为时间范围的duration值。这个时间范围用于在THChapter对象集合中查找。
(3)调用findChapterInTimeRange:reverse:方法 为reverse:参数传递YES值。表示我们希望在chapters数组中后向进行搜索。
(4)与findPreviousChapter方法类似,获取播放控件的当前时间。不需要进行任何特别的计算,因为当时间前进时不需要考虑时间相关的问题。我们还使用kCMTimePositiveInfinity常量创建一个当前时间之后的无限大时间,用来标记时间范围的,上限。
(5)创建一个CMTimeRange,使用当前时间作为start,使用预计时间作为duration。
(6)调用findChapterInTimeRange:reverse:方法, 这次将reverse:参数设 为NO,因为我们希望向前查找chapters数组。
(7)枚举chapters数组中的对象,按照reverse: 参数指定的顺序遍历集合。
(8)调用章的isInTimeRange:方法判断章的起点时间是否在时间范围之内。如果在,则可以找到匹配并停止对元素的处理。
(9)最后,返回匹配的THChapter。如果没有发现匹配则返回nil,比如在时间轴开始时向后导航和在时间轴结尾处向前导航这两种情况。
我们不对完整的THChapter类实现展开讲解,因为它是一个简单保存数据的对象,不过我们一定要注意isInTimeRange:方法,因为它使用了Core Media框架中的宏。
- (BOOL)isInTimeRange:(CMTimeRange)timeRange {
return CMTIME_COMPARE_INLINE(_time, >, timeRange.start) &&
CMTIME_COMPARE_INLINE(_time, <, timeRange.duration);
}
CoreMedia定义了大量有用的函数和宏。其中一个最常用的宏就是CMTIME_COMPAREINLINE。这个宏包含两个CMTime值和一个比较运算符,并返回一个布尔值用来表示比较结果,在isInTimeRange:方法中,我们会判断章的时间是否处于start和duration时间范围内。
现在最难的部分已经完成了,我们准备实现这个功能。代码清单5-8给出了剩下几个方法的实现。
代码清单5-8 previousChapter:和nextChapter:方法的实现。
- (void)previousChapter:(id)sender {
[self skipToChapter:[self findPreviousChapter]]; // 1
}
- (void)nextChapter:(id)sender {
[self skipToChapter:[self findNextChapter]]; // 2
}
- (void)skipToChapter:(THChapter *)chapter { // 3
[self.playerItem seekToTime:chapter.time completionHandler:^(BOOL done) {
[self.playerView flashChapterNumber:chapter.number
chapterTitle:chapter.title];
}];
}
(1)当用户选择Previous Chapter菜单,就会调用findPreviousChapter方法,将结果传递给skipToChapter:方法。
(2)同样,当用户选择Next Chapter菜单,就会调用findNextChapter方法,将结果传递给skipToChapter:方法。
(3)最后,在skipToChapter:方法中,调用播放条目的seekToTime:completionHandler:方法,将章时间作为第一个参 数。在compltionHandler:回调中, 调用播放器视图的fashChapterNumber:chapterTitle:方法,将章节编号和标题显示在播放器视图中。
再次运行应用程序,打开视频,尝试跳过各场景。
5.7 启用修剪
除了AVPlayerView定义的即用型播放体验外,还有一种实用方式。如果你用过目前版本的QuickTime Player,可能会注意到Edit菜单中的Trim选项,选择该选项可以对资源进行修剪,如图5-13所示。
AVPlayerView定义了相同的功能和界面,最主要的是它非常容易添加到应用程序中。示例应用程序已经定义了一个Trim选项控件和startTrimming:存根方法,不过我们需要为这个方法提供具体的实现,如代码清单5-9所示。
代码清单5-9启用修剪
- (IBAction)startTrimming:(id)sender {
[self.playerView beginTrimmingWithCompletionHandler:NULL];
}
读者可能会想代码这么简单是不是我们没有写全啊,那你就亲自试试吧。运行应用程序并打开示例视频,进入Edit菜单并选择Trim菜单项。我们所看到的修剪界面与QuickTime Player的一样,Trim和Cancel按钮的功能都和预期的一样。
在修剪前,首先需要查询播放器视图的canBeginTrimming属性。有一些情况下该属性会返回NO。第一种情况是如果修剪界面已经处于展示中,因为这时打开修剪界面是没有意义的。第二种情况是资源已明确不能被修剪。比如不能对从iTunes Store上购买的电影或电视节目进行修剪。查询该属性最常见的位置是在validateUserInterfaceItem:方法中,以便启用或禁用相应的菜单项(如代码清单5-10所示)。
代码清单5-10实现方式
- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)item {
SEL action = [item action];
if (action == @selector(startTrimming:)) {
return self.playerView.canBeginTrimming;
}
return YES;
}
现在运行应用程序,当修剪界面打开时,Trim菜单项处于禁用状态。
读者朋友可能会好奇资源的修剪是如何完成的。开发者不需要添加任何修剪资源的代码,如果回顾之前的内容,就应该知道AVAsset是不可变对象,也就是说它是不能被修改的。那点击Trim按钮会发生什么呢?是的,会产生错觉。当我们点击Trim按钮,播放器视图会修改当前AVPlayerltem的两个属性。左侧的修剪控件会设置播放条目的reversePlaybackEndTime属性,右边的控件设置forwardPlaybackEndTime。这些属性定义了资源的有效时间轴。如果修剪视频之后调用Trim菜单项,会发现所有内容保持不变并且修剪界面处于已修剪的部分。这并不是AVPlayerView的限制,而仅是为了保持AV Foundation的不可变设计理念,确实在QuickTimePlayer中也是这样。所以如果资源不能被修改,那如何才能保存这些变更呢?这就带来了下面有关资源导出的主题。
5.8 导出
要保存修剪操作的结果,需要使用AVAssetExportSession类将当前资源导出为一个新的资源。我们已经见过这个类了,它是AV Foundation框架最常用的类之一。
应用程序已经在File菜单中加入了Export菜单项,并使它同对应的startExporting:方法建立了关联,现在需要的就是具体的实现代码了。首先为文档对象的类扩展添加一组新的属性,如代码清单5-11所示。
代码清单5-11添加导出属性
#import "THExportWindowController.h"
@interface THDocument () <THExportWindowControllerDelegate>
@property (strong) AVAsset *asset;
@property (strong) AVPlayerItem *playerItem;
@property (strong) NSArray *chapters;
@property (strong) AVAssetExportSession *exportSession; // 添加导出属性
@property (strong) THExportWindowController *exportController; // 添加导出属性
@property (weak) IBOutlet AVPlayerView *playerView;
@end
配置好之后,继续看startExporting:方法写实现,如代码清单5-12所示。
代码清单5-12 startExporting:方法的实现
- (IBAction)startExporting:(id)sender {
[self.playerView.player pause]; // 1
NSSavePanel *savePanel = [NSSavePanel savePanel];
[savePanel beginSheetModalForWindow:self.windowForSheet
completionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
// Order out save panel as the export window will be shown
[savePanel orderOut:nil];
NSString *preset = AVAssetExportPresetAppleM4V720pHD;
self.exportSession = // 2
[[AVAssetExportSession alloc] initWithAsset:self.asset
presetName:preset];
NSLog(@"%@", [self.exportSession.supportedFileTypes firstObject]);
CMTime startTime = self.playerItem.reversePlaybackEndTime;
CMTime endTime = self.playerItem.forwardPlaybackEndTime;
CMTimeRange timeRange = CMTimeRangeMake(startTime, endTime); // 3
// Configure the export session // 4
self.exportSession.timeRange = timeRange;
self.exportSession.outputFileType =
[self.exportSession.supportedFileTypes firstObject];
self.exportSession.outputURL = savePanel.URL;
self.exportController = [[THExportWindowController alloc] init];
self.exportController.exportSession = self.exportSession;
self.exportController.delegate = self;
[self.windowForSheet beginSheet:self.exportController.window // 5
completionHandler:nil];
[self.exportSession exportAsynchronouslyWithCompletionHandler:^{
// Tear down // 6
[self.windowForSheet endSheet:self.exportController.window];
self.exportController = nil;
self.exportSession = nil;
}];
}
}];
}
- (void)exportDidCancel {
[self.exportSession cancelExport]; // 7
}
(1)如果视频处于播放状态则需要先暂停,因为当视频导出时用户不能与播放栏控件互动。
(2)创建一个新的AVAssetExportSession实例,传递资源对象和导出配置。这里使用AVAssetExportPresetAppleM4V720pHD参数,它将创建一个720p MPEG-4视频文件,不过也可以任意选择其他预设配置。比如,如果正在导出一个修剪操作,可以考虑使用AVAssetExportPresetPassthrough,不需要对媒体进行转码。
(3)基于反向和正向播放的结束时间创建一个时间范围。 如果没有执行修剪操作,反向和前向结束时间就等于kCMTimeInvalid,这样会得到一个等于kCMTimeRangeInvalid的时间范围。在导出会话上设置kCMTimeRangeInvalid范围值,就会导出全部视频。
(4)为导出会话配置时间范围、一个与会话兼容的输出文件类型以及从NSSavePanel获取的输出URL。
(5)示例应用程序提供了一个THExportWindowController类,它是一个简单的NSWindowController子类,用于展示导出进度窗口。创建一个新的实例并设置self作为委托,在新的页面展示该窗口。
(6)开始导出资源,当回调函数被调用时就可以关掉导出页面和控制器实例。
(7)如果用户在导出进度窗口中点击Cancel按钮,就会取消导出会话。
是时候体验所有功能了,运行应用程序并打开示例视频,修剪其中感兴趣的部分并导出一个视频副本到硬盘。要验证导出的结果是否可用,应该使用什么?当然是KitTimePlayer!
5.9 传统资源的兼容
本章最后一个要解决的问题并不是AV Foundation范畴的,不过它肯定与我们的主题相关,对于基于QuickTime的应用程序尤其重要。
QuickTime支持很多编解码方式和媒体类型,这也是QuickTime被认为是一个功能强大且灵活的平台的原因之一。不过大部分它所支持的编解码都过时了,并且它支持的曲目类型也被最新的技术所超过。AV Foundation不支持这些传统的特性,而是将注意力放在苹果公司所认为的未来最流行的编解码和曲目类型处理上。
对于当今的媒体环境,这一重点的编解码集合和媒体类型完全有理由更进一步;不过Macintosh多媒体环境不是今天开始的。用户可能已经使用这些不被支持的编码格式许多年了,所以放弃这些格式也不是用户所期望的。那么我们应该怎么办呢?
少有的是,苹果公司在Mac OS X 10.9中为这些过时的框架给出了一个新的类。确切地讲,为QTKit框架加入了一个新类QTMovieModernizer。该类可将过时的媒体内容转换为AVFoundation支持的格式。实际上,如果你使用Mavericks系统下的QuickTime Player播放器打开一个早期QuickTime文件,就会发现其实已经实现了这个转变功能。当用户打开文件时,展 示内容前会看到一条消息显示“转换中...”。QuickTime Player使用这个类转换媒体,开发者也可在自己的应用程序中使用它。
运行该应用程序,选择File,再选择Open..., 找到Chapter 5目录。在里面可以找到一个子目录Legacy,其中包含一些使用传 统编解码的QuickTime文件。打开其中一个文件并点击Play按钮。可以听到有声音传出,不过没有视频影像展示,这是因为AV Foundation无法对该视频曲目解码。下面看一下如何使用QTMovieModernizer类解决这一问题。首先对之前的代码进行一些修改,如代码清单5-13所示。
代码清单5-13 Movie Modernization准备
#import "THDocument.h"
#import <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
#import "THChapter.h"
#import <QTKit/QTMovieModernizer.h>
#import "NSFileManager+THAdditions.h"
#import "THWindow.h"
#import "THExportWindowController.h"
#define STATUS_KEY @"status"
@interface THDocument () <THExportWindowControllerDelegate>
@property (strong) AVAsset *asset;
@property (strong) AVPlayerItem *playerItem;
@property (strong) NSArray *chapters;
@property (strong) AVAssetExportSession *exportSession;
@property (strong) THExportWindowController *exportController;
@property BOOL modernizing;
@property (weak) IBOutlet AVPlayerView *playerView;
@end
@implementation THDocument
#pragma mark - NSDocument Methods
- (NSString *)windowNibName {
return @"THDocument";
}
- (void)windowControllerDidLoadNib:(NSWindowController *)controller {
[super windowControllerDidLoadNib:controller];
if (!self.modernizing) {
[self setupPlaybackStackWithURL:[self fileURL]];
} else {
[(id)controller.window showConvertingView];
}
}
修改windowControllerDidL oadNib:方法,根据moderizing属性的状态酌情执行相应的设置,modernizing属性用于表示QTMovieModermizer当前是否正在运行一个 会话。如果modernizing为false,则运行正常设置,如果modernizing为true,在窗口对象中调用showConvertingView,展示一个进度条。
完成代码清单5-13的修改后,下面看一下实际的现代化过程。按照下面的应用程序对readFromURL:ofTyperror:方法进行修改,如代码清单5-14所示。
代码清单5-14运行现代化
- (BOOL)readFromURL:(NSURL *)url
ofType:(NSString *)typeName
error:(NSError *__autoreleasing *)outError {
NSError *error = nil;
if ([QTMovieModernizer requiresModernization:url error:&error]) { // 1
self.modernizing = YES;
NSURL *destURL = [self tempURLForURL:url]; // 2
if (!destURL) {
self.modernizing = NO;
NSLog(@"Error creating destination URL, skipping modernization.");
return NO;
}
QTMovieModernizer *modernizer = // 3
[[QTMovieModernizer alloc] initWithSourceURL:url
destinationURL:destURL];
modernizer.outputFormat = QTMovieModernizerOutputFormat_H264; // 4
[modernizer modernizeWithCompletionHandler:^{
if (modernizer.status == // 5
QTMovieModernizerStatusCompletedWithSuccess) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setupPlaybackStackWithURL:destURL]; // 6
[(id)self.windowForSheet hideConvertingView];
});
}
}];
}
return YES;
}
- (NSURL *)tempURLForURL:(NSURL *)url {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *dirPath = // 7
[fileManager temporaryDirectoryWithTemplateString:@"kittime.XXXXXX"];
if (dirPath) { // 8
NSString *filePath =
[dirPath stringByAppendingPathComponent:[url lastPathComponent]];
return [NSURL fileURLWithPath:filePath];
}
return nil;
}
(1)首先在QTMovieModernizer上调用requiresModermization:error:类方法来判断媒体是否需要转换。这个方法会确定是否需要一个转换过程。 要知道这是一个同步调用,所以在实际应用程序中通常是在后台线程中执行该查找,这样就不会阻塞用户界面。如果该方法返回YES,则继续执行会话。有时返回值为YES,错误指针会包含一些在转换过程中可能出现的错误信息描述,所以在实际应用程序中对这一点要格外注意。
(2)在NSTemporaryDirectoryO中获取一个写入转换文件的NSURL地址。如果没有找到有效地址,会终止该方法,弹出一个默认提示框,告诉用户无法读取文件。
(3) temporaryDirectoryWithTemplateString: 是我们为NSFileManager添加的一个分类方法。这个方法基于传递给方法的模板字符串并利用mkdtemp函数在NSTemporaryDirectory()之下创建一个唯一的目录。
(4)如果临时目录创建好了,再基于源URL的文件名创建一个新的NSURL并将其返回给调用方法。
(5)创建一个新的QTMovieModermizer实例,传递源地址和目的地址URL。
(6)为转换媒体资源指定一个输 出格式,这里我们使用h.264编码,不过你也可以使用QTMovieModernizerOutputFormat_ AppleProRes422常量或QTMovieModermizerOutputFormat_AppleProRes4444常量分别指定使用Apple ProRes422或4444编码格式。
(7)通过调用modernizeWithCompletionHandler:方法开始现代化过程。当调用completionhandler块时,需要查看它的状态来判断转换过程是否成功。如果成功,则调度回主线程继续处理。
(8)调用setupPlaybackStackWithURL:方法,并传递转换资源的URL。最后,将之前的转换视图删除。
再次运行应用程序,重新打开一个示例传统格式的文件。这次会看到一个带有转换效果的spinner视图。当该视图消失后,就可以点击Play按钮观看视频内容了,多亏了QTMovieModernizer!
5.10 小结
本章我们对AV Kit框架的使用有了比较深入的了解。iOS版本的AV Kit定义了AVPlayer-ViewController类,为使用AV Foundation创建iOS最新视频播放应用程序提供了一种简单方法。Mac OS X版本的AV Kit定义了AVPlayerView类,为Mac平 台创建视频播放应用程序提供了一种简单且功能强大的方法,并填补了无法播放QTMovieView传统格式的空白。使用AV Kit可让开发者在两个平台上创建具有相同功能、特色和用户界面的视频播放器。我们还学习了使,用AVTimeMetadataGroup类来处理存储在AVAsset中的基于时间的章元数据。虽然本章使用Mac平台讨论这些问题,不过这些功能和方法在iOS平台也是适用的。最后,我们学习了QTMovieModernizer类。虽然这个类是QTKit框架的一部分,不过它可以使Mac平台的AVFoundation应用程序拥有更完善的功能。
网友评论