第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
#import
@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
#import
#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,
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?