AVFoundation开发秘籍笔记:第4章 视频播放

4.1 播放功能综述

当开发一个自定义播放器时会用到大量的对象。本节从一个较高层级的介绍入手,通过探究其所扮演的角色和所含类之间的关系来学习AV Foundation的播放功能。后面还会继续深入分析具体的API,并通过实际开发一个 自定义视频播放器来实际使用这些类。图4-1 概要显示了用到的类及其关系。


AVFoundation开发秘籍笔记:第4章 视频播放_第1张图片
4.1.1 AVPlayer

AV Foundation的播放都围绕AVPlayer类展开,AVPlayer是一个用来播放基于时间的视听媒体的控制器对象。支持播放从本地、分步下载或通过HTTP Live Streaming协议得到的流媒体,并在多种播放场景中播放这些视频资源。需要说明的是,当我们说“控制器”时,是指我们通常的理解,它不是一个视图或窗口控制器,而是一个对播放和资源时间相关信息进行管理的对象。开发者通过框架提供的应用程序接口来开发控制播放基于时间的媒体的用户界面。

AVPlayer是一个不可见组件。如果播放MP3或AAC音频文件,那么没有可视化的用户界面也不会有什么问题。不过如要播放一个QuickTime电影或一个MPEG-4视频,会导致非常不好的用户体验。要将视频资源导出到用户界面的目标位置,需要使用AVPlayer类。

注意:
AVPlayer只管理一个单 独资源的播放,不过框架还提供了AVPlayer的一个子类AVQueue-Player,可以用来管理一个资源队列。当你需要在一个序列中播放多个条目或者为音频、视频资源设置播放循环时可使用该子类。

4.1.2 AVPlayerLayer

AVPlayerLayer构建于Core Animation之上,是AV Foundation中能找到的为数不多的可见组件。Core Animation是Mac和iOS平台上负责图形渲染与动画的基础框架,主要用于这些平台资源的美化和动画流畅度提升。Core Animation本身具有基于时间的属性,并且由于它基于OpenGL,所以具有很好的性能,能非常好地满足AV Foundation的各种需要。

AVPlayerLayer扩展了Core Animation的CALayer类,并通过框架在屏幕上显示视频内容。这一图层并不提供任何可视化控件或其他附件(根据开发者需求搭建的),但是它用作视频内容的渲染面。创建AVPlayerLayer需要一个指向AVPlayer实例的指针, 这就将图层和播放器紧密绑定在一起,保证了当播放器基于时间的方法出现时使二者保持同步。AVPlayerLayer与其他CALayer样, 可以设置为UIView或NSView的备用层,或者可以手动添加到一个已有的层继承关系中。

AVPlayerLayer是一个相对简单的类,使用起来也简单。在这一层中开发者可以自定义的领域只有video gravity。总共可为videoGravity属性定义三个不同的gravity值,用来确定在承载层的范围内视频可以拉伸或缩放的程度。图4-2、 图4-3和图4 4给出了一个16:9的视频置于4:3矩形范围内的情况,使我们可以看到不同gravity值。


AVFoundation开发秘籍笔记:第4章 视频播放_第2张图片
AVFoundation开发秘籍笔记:第4章 视频播放_第3张图片
AVFoundation开发秘籍笔记:第4章 视频播放_第4张图片
4.1.3 AVPlayerltem

我们最终的目的是使用AVPlayer来播放AVAsset。如果查看AVAsset文档,可以找到一些用来获取数据的方法和属性,比如创建日期、元数据和时长等信息。不过无法查到如何获取当前时间的方法,也没有在媒体中查找特定位置的方法。这是因为AVAsset模型只包含媒体资源的静态信息,这些不变的属性用来描述对象的静态状态。这就意味着仅使用AVAsset对象是无法实现播放功能的。当我们需要对一个资源及其相关曲目进行播放时,首先需要通过AVPlayerltem和AVPlayerltemTrack类构建相应的动态内容。

AVPlayerltem会建立媒体资源动态视角的数据模型并保存AVPlayer在播放资源时的呈现状态。在这个类中我们会看到诸如IseekToTime:的方法以及访问currentTime和presentationSize的属性。AVPlayerltem由一个或多 个媒体曲目组成,由AVPlayerItemTrack类建立模型。AVPlayerItemTrack实例用于表示播放器条目中的类型统一的媒体流,比如音频或视频。AVPlayerltem中的曲目直接与基础AVAsset中的AVAssetTrack实例相对应。

4.2 播放秘籍

仅掌握这些类的简单概念还不够,下面通过一小段代码来看一下如何设置播放栈来播放保存在应用程序bundle中的视频。

- (void)viewDidLoad {
    [super viewDidLoad] ;
    // 1. Define the asset URL
    NSURL *assetURL = [[NSBundle mainBundle] URLForResource:@"waves" withExtension:@"mp4"];
    
    // 2. Create an instance of AVAsset
    AVAsset *asset = [AVAsset assetWithURL:assetURL];
    
    // 3. Create an AVPlayerItem with a pointer to the asset to play
    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
    
    // 4. Create an instance of AVPlayer with a pointer to the player item
    self.player = [AVPlayer playerWithPlayerItem:playerItem];
    
    // 5. Create a player layer to direct the video content
    AVPlayerLayer *playerLayer = [AVP1ayerLayer playerLayerWithPlayer:self.player];
    
    // 6. Attach layer into layer hierarchy
    [self.view.layer addSublayer :playerLayer];
}

该示例中对播放视频文件所需的基础架构进行了设置。不过在实际播放视频内容前还需要一个额外步骤,这是因为播放器的播放控件还没有为播放动作做好准备。AVPlayerltem没有准备播放的界面,不过取而代之的是基于“主动发起请求”("don’tcall me, I'll call you")的机制。

AVPlayertem具有一个名为status的AVPlayerltemStatus类型的属性。在对象创建之初,播放条目由AVPlayertemStatusUnknown状态开始,该状态表示当前媒体还未载入并且还不在播放队列中。将AVPlayerItem与一个AVPlayer对象 进行关联就开始将媒体放入队列中,但是在具体内容可以播放前,需要等待对象的状态由AVPlayerltemStatusSUnknown变为AVPlayerftemStatusReadyToPlay。开发者可通过Key-Value Observing (KVO)机制监视status属性的值来跟踪这一变化过程。

KVO是由Foundation框架提供的Observer模式的由苹果公司给出的解决方案。可以让开发者注册一个对象作 为其他对象状态的观察者。当被观察的对象状态发生变化时,观察对象就会得到通知并采取相应的动作。在将AVPlayerItem 与AVPlayer关联之前,开发者需要将代码设置为status属性的观察者,如下面的示例所示。

static const NSString *PlayerItemStatusContext;

- (void)viewDidLoad {
    ...
    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
    [playerItem add0bserver:self
                 forKeyPath:@"status"
                    options:0
                    context:&PlayerItemStatusContext];
    self.player = [AVPlayer playerWithPlayerItem:playerItem];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context: (void *)context {
    if (context == &PlayerItemStatusContext){
        AVPlayerItem *playerItem = (AVPlayerItem *) object;
        if (playerItem.status == AVPlayerItemStatusReadyToPlay) {
            // proceed with playback
        }
    }
}

当观察到播放控件的status变为AVPlayerltemStatusReady ToPlay时,就可以开始播放了。

4.3 处理时间

AVPlayer和AVPlayerltem都是基于时间的对象,但是在我们使用它们的功能前,需要了解在AV Foundation框架中呈现时间的方式。

人们倾向于用日子、小时、分钟和秒的方式表示时间。开发人员经常将时间进一步精确到亳秒和纳秒。所以用一个双精度浮点型数据表示时间也合情合理。实际上,回顾第2章中介绍的AVAudioPlayer,可以看到时间是以NSTimeInterval表示的,其实就是简单地对double值进行了typedef定义。不过使用浮点型数据类型表示时间存在一定问题, 因为浮点型数据的运算会导致不精确的情况。当进行多时间计算累加时这些不精确的情况就会特别严重,经常导致时间的明显偏移,使得媒体的多个数据流几乎无法实现同步。此外,以浮点型数据呈现时间信息无法做到自我描述,这就导致在使用不同时间轴进行比较和运算时比较困难。AV Foundation使用一种可靠性更高的方法来展示时间信息,这就是基于CMTime数据结构。

CMTime

AV Foundation是基于Core Media的高层封装。Core Media是基于C的底层框架,提供了许多处理Mac和iOS媒体栈的关键功能。虽然这个框架通常都在后台工作,不过其中一个我们经常能够接触到的部分就是它的数据结构CMTime。CMTime 为时间的正确表示给出了一种结构,即分数值的方式。具体定义如下:

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

这个结构最关键的两个值是value和timescale。value是一个64位整数值,timescale是一个32位整数值,在时间呈现样式中分别作为分子和分母。

建立以分数的格式处理时间数据的思维方式可能开始不太习惯,不过当开发者多使用几 次这种方式之后就会慢慢习惯。下面看几个示例,了解如何使用CMTimeMake函数创建时间。

// 0.5 seconds
CMTime halfSecond = CMTimeMake(1, 2);
// 5 seconds
CMTime fiveSeconds = CMTimeMake(5, 1);

// One sample from a 44.1 kHz audio file
CMTime oneSample = CMTimeMake(1, 44100);

// Zero time value
CMTime zeroTime = kCMTimeZero;

除对CMTime进行定义外,CMTime.h头文件还定义了大量实用的函数用于简化时间的处理。与大部分苹果公司的底层C框架一样,最好的参考资料就是头文件,所以这里建议大家仔细阅读CMTime.h头文件,了解其中定义的函数的功能。

4.4 创建视频播放器

本节通过创建一个iOS 视频播放器(如图45所示)来深入学习AV Foundation播放API的细节。应用程序能播放本地和远程媒体,支持播放、暂停和拖动媒体时间轴。完成基本功能后,还需要对应用程序进一步优化以改善用户体验。 可以在Chapter 4目录中找到名为VideoPlayer_Starter的示例项目。

AVFoundation开发秘籍笔记:第4章 视频播放_第5张图片
4.4.1 创建视频视图

第一步需要创建一个用来在屏 幕上展示视频内容的视图。在示例项目中的THVideoPlayer/Views文件组下面,可以找到一个名为THPlayerView的类。这个类就是用来展示视频内容并为操作视频播放提供用户界面的类。下面看一下这个类的接口,如代码清单4-1所示。

代码清单4-1 THPlayerView 接口

#import "THTransport.h"

@class AVPlayer;

@interface THPlayerView : UIView

- (id)initWithPlayer:(AVPlayer *)player;

@property (nonatomic, readonly) id  transport;

@end

这是一个仅带有几个方法的简单类。通过调用其initWithPlayer:初始化方法,并传递当前AVPlayer实例的引用进行实例化。这样就可以将播放器输出的视频直接展示在这个视图中,只读属性transport负责管理展示在视图中的可视化控件。在讲解应用程序播放控制器类的实现时会看到它是如何工作的。下 面我们来看这个类的具体实现。

视图本身并不是视频输出的目标,相反,开发者需要将播放器输出指向一个AVPlayerLayer实例。可以手动创建层,并将它添加到视图的层继承关系中,但是在iOS平台下有一种更便捷的方法。UIView视图都受Core Animation层的支持,默认情况下,就是CALayer的通用实例,不过你可以通过在UIView中重写layerClass方法自定义支持层的类型,以便在实例化一个视图的时候返回一个要使用的自定义CALayer。在使用AVPlayerL ayer对象时上述方法更加方便,因为不需要手动创建和操作层以及层继承关系。代码清单4-2给出了THPlayerView类的实现。

代码清单4-2 THPlayerView 实现

#import "THPlayerView.h"
#import "THOverlayView.h"
#import 

@interface THPlayerView ()
@property (strong, nonatomic) THOverlayView *overlayView;                   // 1
@end

@implementation THPlayerView

+ (Class)layerClass {                                                       // 2
    return [AVPlayerLayer class];
}

- (id)initWithPlayer:(AVPlayer *)player {
    self = [super initWithFrame:CGRectZero];                                // 3
    if (self) {
        self.backgroundColor = [UIColor blackColor];
        self.autoresizingMask = UIViewAutoresizingFlexibleHeight |
                                UIViewAutoresizingFlexibleWidth;

        [(AVPlayerLayer *) [self layer] setPlayer:player];                  // 4

        [[NSBundle mainBundle] loadNibNamed:@"THOverlayView"                // 5
                                      owner:self
                                    options:nil];
        
        [self addSubview:_overlayView];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    self.overlayView.frame = self.bounds;
}

- (id )transport {
    return self.overlayView;
}

@end

(1)创建一个类扩展来定义一个私有属性用于保存指向THOverlayView视图实例的指针。这个类提供用户界面中操作视频播放的控件。
(2)重写layerClass类 方法返回一个AVPlayerLayer类。 每当创建THPlayerView实例时,就会使用AVPlayerLayer作为它的支持层。
(3)创建时没有给出默认的尺寸大小,所以开发者需要调用带有zero-sized框架的超类初始化方法。展示视图的视图控制器负责设置合适的框架。
(4)这是该类中最关键的一行代码。 我们希望获得传入初始化方法的AVPlayer实例并在AVPlayerLayer上对其进行设置。这一步将从AVPlayer输出的视频指向AVPlayerL ayer实例。
(5)在NIB中定义覆盖视图,通过调用loadNibNamed:owner:options方法创建视图实例。当视图创建完成并赋给overlayView属性后,将其作为子视图进行添加。

完成THPlayerView的实现后,下面将注意力转移到THPlayerController类。

4.4.2 创建视频控制器

在项目的THVideoPlayer/Controllers组下面,可找到THPlayerController类的具体实现代码。这个类为应用程序完成了很多功能,也是我们处理核心播放API方法的地方。代码清单4-3给出了这个类的接口。

代码清单4-3 THPlayerController 接口

@interface THPlayerController : NSObject

- (id)initWithURL:(NSURL *)assetURL;

@property (strong, nonatomic, readonly) UIView *view;

@end
创建一个THPlayerController实例时,需要调用其initWithURL:方法,并传递需要播放的媒体的NSURL。AVPlayer可用来播放本地或流媒体,所以这个URL可以是本地文件URL,也可以是远程HTTP URL。该类还为相关视图提供了一个只读属性,以便客户端UIViewController可将视图添加到视图继承关系中。返回的视图是一个THPlayerView实例,不过由于这些细节 需要对客户端隐藏,所以返回一个通用UIView即可。

转过来看类的具体实现,首先创建一个类扩展来定义控制器的内部属性(如代码清单4-4所示)。

代码清单4-4 THPlayerController 类扩展

#import "THPlayerController.h"
#import 
#import "THTransport.h"
#import "THPlayerView.h"
#import "AVAsset+THAdditions.h"
#import "UIAlertView+THAdditions.h"

// AVPlayerItem's status property
#define STATUS_KEYPATH @"status"

// Refresh interval for timed observations of AVPlayer
#define REFRESH_INTERVAL 0.5f

// Define this constant for the key-value observation context.
static const NSString *PlayerItemStatusContext;

@interface THPlayerController () 

@property (strong, nonatomic) AVAsset *asset;
@property (strong, nonatomic) AVPlayerItem *playerItem;
@property (strong, nonatomic) AVPlayer *player;
@property (strong, nonatomic) THPlayerView *playerView;

@property (weak, nonatomic) id  transport;

@property (strong, nonatomic) id timeObserver;
@property (strong, nonatomic) id itemEndObserver;
@property (assign, nonatomic) float lastPlaybackRate;

@end

在这个类的实现中,首先创建了一个类扩展来定义对象需要的存储属性。注意该扩展遵循THTransportDelegate协议并定义了一个transport属性。该类和THOverlayView之间会有很多交互操作,用来定义管理视频播放的用户界面。虽然这些类需要沟通,不过它们不必直接了解彼此。要断开这个关联,需要用到THTransport和THTransportDelegate协议(如代码清单4-5所示)。

代码清单4-5 THTransport.h

#import 
@protocol THTransportDelegate 

- (void)play;
- (void)pause;
- (void)stop;

- (void)scrubbingDidStart;
- (void)scrubbedToTime:(NSTimeInterval)time;
- (void)scrubbingDidEnd;
- (void)jumpedToTime:(NSTimeInterval)time;

@end

@protocol THTransport 

@property (weak, nonatomic) id  delegate;
- (void)setTitle:(NSString *)title;
- (void)setCurrentTime:(NSTimeInterval)time duration:(NSTimeInterval)duration;
- (void)setScrubbingTime:(NSTimeInterval)time;
- (void)playbackComplete;

@end

THOverlayView遵循THTransport协议,它可以为与覆盖视图进行通信提供正式接口。当播放栏(transport)发生变化时,比如用户改变时间轴位置或点击Play/Pause按钮,控制器对象会执行相应的委托回调。稍后将看到具体的实现过程,代码清单4-6给出了THPlayerController的实现。

代码清单4-6 THPlayerController 实现

@implementation THPlayerController
#pragma mark - Setup
- (id)initWithURL:(NSURL *)assetURL {
    self = [super init];
    if (self) {
        _asset = [AVAsset assetWithURL:assetURL];                           // 1
        [self prepareToPlay];
    }
    return self;
}

- (void)prepareToPlay {
    NSArray *keys = @[@"tracks",
                      @"duration",
                      @"commonMetadata"
                     ];
    self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset          // 2
                           automaticallyLoadedAssetKeys:keys];
    [self.playerItem addObserver:self                                      // 3
                      forKeyPath:STATUS_KEYPATH
                         options:0
                         context:&PlayerItemStatusContext];
    
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];          // 4

    self.playerView = [[THPlayerView alloc] initWithPlayer:self.player];    // 5
    self.transport = self.playerView.transport;
    self.transport.delegate = self;
}
// More methods to follow ...
@end

(1)首先将URL传递给初始化方法来创建一个AVAsset。资源创建完成后,调用控制器的prepareToPlay方法来设置播放该资源所需的基础结构。

(2)框架会自动载入资源的tracks属性,省去了通过AVAsynchronousKeyValue oading协议手动载入该属性的过程。不过在以前,开发者仍然需要执行loadValuesAsynchronouslyForKeys:completionHandler:方法来载入需要访问的其他资源属性。iOS 7和Mac OS 10.9在AVPlayertem的处理上有了大幅改进,通过使用新的初始化方法initWithAsset:automaticallyLoadedAssetKeys:或playerItemWithAsset:automaticallyLoadedAssetKeys:创建一个AVPlayerltem实例,将任意属性集的载入委托给该框架。两种方式都将NSArray用作第二个参数,包含了随着AVPlayerItem在初始化队列中的载入过程所需的资源键。使用这个方法自动载入tracks、duration和commonMetadata属性。

(3)添加Iself作为AVPlayerltem的status属性监听器。回顾一下创建过程,播放项开始时的status状态为AVPlayerltemStatusUnknown,播放项直到状态变为AVPlayertemStatusReadyToPlay才可以开始播放。对status属性的键值观察可以让你监听变化。

(4)为新创建的AVPlayerltem对象创建一个 AVPlayer实例。AVPlayer会立即开始媒体队列化的过程。

(5)最后,创建一个THPlayerView实例,传递给它一个指向AVPlayer实例的指针。开发者还需要为THPlayerController和ITHTransport设置关系。

4.4.3 监听状态改变

我们已经将THPlayerController设置为播放项的status属性的监听器。在对该属性监听前,需要实现observeValueForKeyPath:ofObject:change:context方法,如代码清单4-7所示。

代码清单4-7监听 status属性

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if (context == &PlayerItemStatusContext) {
        
        dispatch_async(dispatch_get_main_queue(), ^{                        // 1
            
            [self.playerItem removeObserver:self forKeyPath:STATUS_KEYPATH];
            
            if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
                
                // Set up time observers.                                   // 2
                [self addPlayerItemTimeObserver];
                [self addItemEndObserverForPlayerItem];
                
                CMTime duration = self.playerItem.duration;
                
                // Synchronize the time display                             // 3
                [self.transport setCurrentTime:CMTimeGetSeconds(kCMTimeZero)
                                      duration:CMTimeGetSeconds(duration)];
                
                // Set the video title.
                [self.transport setTitle:self.asset.title];                 // 4
                
                [self.player play];                                         // 5
                
                [self loadMediaOptions];
                [self generateThumbnails];
                
            } else {
                [UIAlertView showAlertWithTitle:@"Error"
                                        message:@"Failed to load video"];
            }
        });
    }
}

(1) AV Foundation没有指定在哪个线程执行status改变通知,所以在采取下一步动作前,需要通过dispatch_async确保应用程序返回到主线程,向其传递一个主队列的引用。

(2)通过调用私有方法addPlayerltemTimeObserver和addItemEndObserverForPlayerItem设置播放器的时间监听器。下面的小节会讨论这些方法和时间监听器。

(3)在ransport对象上设置当前时间和总长。将用户界面上展示的时间与播放的媒体进行同步。transport对象无法识别CMTime,只能处理以秒为单位的NSTimelInterval类型的时间。我们使用CMTimeGetSeconds函数将CMTime值转换为秒。Core Media定义了常量kCMTimeZero,开发者可以将它作为开头的currentTime参数,使用播放条目的duration属性值作为第二个参数。

(4)向播放栏传递一个标题字符串,来展示资源的标题(如果资源的元数据中存在标题信

息)。AVAsset没有title属性,这是我们加入AVAsset中的一个分类方法,目的是增加代码的可读性。这个分类方法用到了,上一章介绍的元数据API,具体地讲,从资源的commonMetadata得到AVMetadataCommonKeyTitle值。具体细节参考AVAsset+THAdditions。

(5)现在就准备调用AVPlayer的play方法进行播放了。最后,在完成对status 关键路径的监听后,我们希望将作为监听器的self移除。

现在可以启动应用程序并开始播放其中一个视频。虽然视频已经播放,不过用户界面上的控件还没有提供任何功能,并且随着时间的推移用户界面也没有相应的反馈信息。这就又回到了addPlayerItemTimeObserver方法上,我们需要在该方法上实现相关的功能,不过在此之前我们需要先学习如何得知AVPlayer的时间变化。

4.5 时间监听

我们已经讨论过并了解到如何使用KVO来观察播放条目的status属性。KVO对于常见的状态监控表现得很出色,并且可以监听AVPlayerltem和AVPlayer的许多属性。不过KVO也有不能胜任的场景,比如需要监听AVPlayer的时间变化。这些监听类型都是自身具有明显的动态特性并需要非常高的精确度,这一点要比标准的键值监听要求高。为满足这一需求,AVPlayer提供了两种基于时间的监听方法,让应用程序可以对时间变化进行精准的监听。下面分别看一下这两个方法。

4.5.1 定期监听

通常情况下,我们希望以一定的时间间隔获得通知。如果需要随着时间的变化移动播放头位置或更新时间显示,这非常重要。利用AVPlayer的addPeriodic TimeObserverForInterval:queue:usingBlock:方法可以很容易地监听到此类变化。这个方法需要传递如下参数:

●interv: 一个用于指定通知周期间隔的CMTime值。
●queue: 通知发送的顺序调度队列。大多数时候,我们希望这些通知消息发生在主队列,在如果没有明确指定的情况下则默认为主队列。需要重点注意的是不可以使用并行调度队列,因为API没有处理并行队列的方法,否则会导致一些不可 知的问题。
●block:一个在指定的时间间隔中将会在队列上调用的回调块。这个块传递一个CMTime值用于指示播放器的当前时间。

4.5.2 边界时间监听

AVPlayer还提供了一种更有针对性的方法来监听时间,应用程序可以得到播放器时间轴中多个边界点的遍历结果。这一方法 主要用于同步用户界面变更或随着视频播放记录一些非可视化数据。比如,可以定义25%、50%和75%边界的标记,以此判断用户播放进度。要使用这个功能,需要用到addBoundaryTimeObserverForTimes:queue:usingBlock:方法,并提供如下参数:

●times: CMTime 值组成的一个NSArray数组定义了需要通知的边界点。
●queue: 与定期监听类似,为方法提供一个用来发送通知的顺序调度队列。指定NULL等同于明确设置主队列。
●block: 每当正常播放中跨越一个边界点时就会在队列中调用这个回调块。有趣的是,该块不提供遍历的CMTime值,所以开发者需要为此执行一些额外计算进行确定。

本示例应用程序没有用到边界时间监听,不过定期监听对应用程序的功能非常重要。下面通过addPlayerltemTimeObserver方法的实现看一下如何在实际中使用定期监听法,如代码清单4-8所示。

代码清单4-8定期监听法

- (void)addPlayerItemTimeObserver {
    
    // Create 0.5 second refresh interval - REFRESH_INTERVAL == 0.5
    CMTime interval =
        CMTimeMakeWithSeconds(REFRESH_INTERVAL, NSEC_PER_SEC);              // 1
    
    // Main dispatch queue
    dispatch_queue_t queue = dispatch_get_main_queue();                     // 2
    
    // Create callback block for time observer
    __weak THPlayerController *weakSelf = self;                             // 3
    void (^callback)(CMTime time) = ^(CMTime time) {
        NSTimeInterval currentTime = CMTimeGetSeconds(time);
        NSTimeInterval duration = CMTimeGetSeconds(weakSelf.playerItem.duration);
        [weakSelf.transport setCurrentTime:currentTime duration:duration];  // 4
    };
    
    // Add observer and store pointer for future use
    self.timeObserver =                                                     // 5
        [self.player addPeriodicTimeObserverForInterval:interval
                                                  queue:queue
                                             usingBlock:callback];
}

注意:
AV Foundation使用较长的类名和方法名。与块连在一起,一行应用程序就会显得非常多。这个方法还可以写得更简洁,不过除非出版社想要这本书达到14英尺宽,否则我还是按上面格式撰写应用程序吧。不过这里建议大家在实际项目代码中采用更简洁的代码风格。

(1)首先创建一个用于定义通知时间间隔的CMTime值。这里将间隔定义为0.5秒,这个时间粒度足以更新播放器的时间显示。

(2)定义发送回调通知的调度队列。大多数情况下,由于我们所要更新的用户界面处于主线程,所以一般使用主队列。

(3)定义一个回调块,在前面定义的时间周期内会调用该代码块。非常重要的一点是代码块要获取self的弱引用。不这样做会出现难以诊断的内存泄漏。

(4)在回调块内部,我们希望通过CMTimeGetSeconds函数将代码块的CMTime值转换成一个NSTimeInterval。同样,还需要将播放条目的duration进行转换。传递这个duration信息看起来是多余的,因为我们已经在KVO回调中传递duration到transport中,不过transport会随着媒体的载入而改变,所以要保持用户界面的同步,最好还是传递最新的值。

(5)最后调用addPeriodicTimeObserverForInterval:queue:usingBlock:方法并传递定义好的参数。调用这个方法会返回一个隐含id类型指针。对这些回调必须保持一个强引用。这个指针还会用于移除监听器。

4.5.3 条目结束监听

另一常见的需要监听的事件就是条目播放完毕的时间,虽然这不同于上面介绍的基于时间的监听,不过我们倾向于认为二者有着类似的原理。当播放完成时,AVPlayerItem会发送一个AVPlayerItemDidPlayToEndTimeNotification通知。THPlayerController实例应 该注册为该通知的监听器,这样就可以采取相应的动作。代码清单4_9给出了addItemEndObserverForPlayerItem方法的实现。

代码清单4-9条目结束监听

- (void)addItemEndObserverForPlayerItem {
    NSString *name = AVPlayerItemDidPlayToEndTimeNotification;
    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    __weak THPlayerController *weakSelf = self;                             // 1
    void (^callback)(NSNotification *note) = ^(NSNotification *notification) {
        [weakSelf.player seekToTime:kCMTimeZero                            // 2
                  completionHandler:^(BOOL finished) {
            [weakSelf.transport playbackComplete];                          // 3
        }];
    };
    self.itemEndObserver =                                                  // 4
        [[NSNotificationCenter defaultCenter] addObserverForName:name
                                                          object:self.playerItem
                                                           queue:queue
                                                      usingBlock:callback];
}

- (void)dealloc {
    if (self.itemEndObserver) {                                             // 5
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        [nc removeObserver:self.itemEndObserver
                      name:AVPlayerItemDidPlayToEndTimeNotification
                    object:self.player.currentItem];
        self.itemEndObserver = nil;
    }
}

(1)在定义代码块之前,首先需要定义一个到self的弱引用。与定期监听使用的回调块类似,如果没有建立对self的弱引用将会导致内存泄漏。这些基于块的计数循环诊断起来非常难。

(2)当播放完毕时,需要通过调用播放器实例的seekToTime:kCMTimeZero方法重新定位播放头光标回到0位置。

(3)当#2的搜索调用完成时,通知播放栏播放已经完成了,这样就可以重新设置展示时间和搓擦条。

(4)通过注册NSNotificationCenter来添 加itemEndObserver作为通知的监听器,并将定义好的参数传递给它。

(5)最后重写dealloc方法,当控制器被释放时移除作为监听器的itemEndObserver。

运行应用程序。可以看到在视频播放过程中,随着时间的变动,当前时间和剩余时间标签中的值不断更新,并且可以看到时间搓擦条相应地更新播放头的位置。

下面继续实现其他委托回调方法,使播放栏控件正常工作。
4.5.4播放栏委托回调

我们先来看一下THTransportDelegate协 议提供的简单播放栏回调的实现。代码清单4-10给出了这些方法的实现。

代码清单4-10播放栏委托回调

- (void)play {
    [self.player play];
}

- (void)pause {
    self.lastPlaybackRate = self.player.rate;
    [self.player pause];
}

- (void)stop {
    [self.player setRate:0.0f];
    [self.transport playbackComplete];
}

- (void)jumpedToTime:(NSTimeInterval)time {
    [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC)];
}

play的实现不需要过多解释,因为它委托给播放器的同名方法。同样,pause方法委托播放器的pause方法,不过为了条理清晰,仍获取lastPlaybackRate。 stop方法调用setRate:并传递参数0,相当于调用了pause,只是采用不同方法实现同一效果。还对播放栏调用了playbackComplete来更新搓擦条的位置。jumpedToTime:方法 利用播放器的seekToTime:方法跳转到时间轴上的任意位置。这个方法的使用会在本章后面看到。

接下来,看一下如何实现搓擦条相关的方法。一共有三个方法需要实现,分别对应着当用户与UISlider控件交互时产生的三个事件,如代码清单4-11所示。

代码清单4-11 Scrubbing 方法

- (void)scrubbingDidStart {                                                 // 1
    self.lastPlaybackRate = self.player.rate;
    [self.player pause];
    [self.player removeTimeObserver:self.timeObserver];
}
- (void)scrubbedToTime:(NSTimeInterval)time {                               // 2
    [self.playerItem cancelPendingSeeks];
    [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
- (void)scrubbingDidEnd {                                                   // 3
    [self addPlayerItemTimeObserver];
    if (self.lastPlaybackRate > 0.0f) {
        [self.player play];
    }
}

(1)触控事件(UIControlEventTouchDown)会调用scrubbingDidStart方法。在这个方法中开发者将获取当前播放率并暂停播放器。获取当前播放率是为了在搓擦进度结束时恢复播放。此外,还需要移除当前定期监听器,因为我们不希望在用户直接控制媒体进度时触发此类事件。

(2)当UISlider实例的值发生变化时(UIControlEventValueChanged)会调用scrubbedToTime方法。由于这个方法在用户移动滑动条位置时会迅速触发,所以首先应该在播放条目上调用cancelPendingeeks。这是经过性能优化的,如果前一个搜索请求没有完成,则避免出现搜索操作堆积情况的出现。开发者可调用seekToTime:发起一个新的搜索, 并将NSTimeInterval值转换为CMTime。

(3)区域内触控事件(UIControlEventTouchUpInside)会调用scrubbingDidEnd方法,用来表示用户已经完成了搓擦操作。在这个方法中,需要调用addPlayerltemTimeObserver重新添加定期监听器。之后查看lastPlaybackRate值,如果该值大于0,则表示视频已经播放过了,需要重新播放该视频。

通过上述过程,最主要的视频播放功能都已经完成了!运行应用程序,现在可以播放、暂停和调整视频播放进度。完成了这些核心的播放功能,下 面就需要对播放中涉及的各功能进行优化,通过添加一些功能提高视频播放的用户体验。

4.6 创建可视搓擦条

你可能已经注意到了播放器右上角有一个带 有Show标签的按钮。如果点击这个按钮,会发现在主导航栏下面出现了一个黑色的栏。目前它还没有实际的功能,不过下面看一下能否把这个地方有效利用起来。

可在AV Foundation中找到一个名为AVAssetImageGenerator的工具类。这个类可用来从一个AVAsset视频曲目中提取图片。这样可以生成-一个或多 个缩略图,用来提升应用程序用户界面的效果。

AVAssetImageGenerator定义了两个方法实现从视频资源中检索图片,分别为:

●copyCGImageAtTime:actualTime:error:允许 在指定时间点捕捉图片。如果开发者希望捕捉一张图片那么这个方法是最适合的,可能用于在视频列表中展示视频缩略图。
●generateCGlmagesAsynchronouslyForTimes:completionHandler: 允许按照第一个参数所指定的时间段生成一个图片序列。该方法具有很高的性能,只需要调用这一个方法就可以生成一组图片。

注意:
AVAssetlmageGenerator既可以生成本地图片,也可以生成持续下载的资源。不过它不能从HTTP Live Stream生成图片。

由此实现的一个优秀功能就是创建可视搓擦条。不同于在工具栏底部展示的标准搓擦条,这里创建一个可视化的搓擦条,这样用户可以更简单地在时间轴中指定位置并立即跳转到指定位置。下面看一下如何实现这个功能(如代码清单4- 12所示)。

代码清单4-12生成图片

#import “THPlayerController.h"
#import 
#import “THTransport.h"
#import “THPlayerView.h"
#import "AVAsset+THAdditions.h”
#import "UIAlertView+THAdditions.h"
#import "THThumbnail.h"

...

@interface THPlayerController () 
@property (strong, nonatomic) AVAsset *asset;
@property (strong, nonatomic) AVPlayerItem *playerItem;
@property (strong, nonatomic) AVPlayer *player;
@property (strong, nonatomic) THPlayerView *playerView;

@property (weak, nonatomic) id  transport;

@property (strong, nonatomic) id timeObserver;
@property (strong, nonatomic) id itemEndObserver;
@property (assign, nonatomic) float lastPlaybackRate;

@property (strong, nonatomic) AVAssetImageGenerator *imageGenerator;

@end

将导入THThumbnail.h头文件。THThumbnail类是项目中的-一个简单模型对象,用来保存我们捕捉到的图片及其相关的时间。还需要添加一一个 AVAssetImageGenerator类型的新属性。

接下来添加一个新方法generateThumbnails并在status监听器回调方法中调用这个方法,如代码清单4-13所示。

代码清单4-13调用generate Thumbnails

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if (context == &PlayerItemStatusContext) {
        
        dispatch_async(dispatch_get_main_queue(), ^{                        
            
            [self.playerItem removeObserver:self forKeyPath:STATUS_KEYPATH];
            
            if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
                
                // Set up time observers.                                   
                [self addPlayerItemTimeObserver];
                [self addItemEndObserverForPlayerItem];
                
                CMTime duration = self.playerItem.duration;
                
                // Synchronize the time display                             
                [self.transport setCurrentTime:CMTimeGetSeconds(kCMTimeZero)
                                      duration:CMTimeGetSeconds(duration)];
                
                // Set the video title.
                [self.transport setTitle:self.asset.title];                 
                
                [self.player play];                                         
                
                [self loadMediaOptions];
                [self generateThumbnails]; // 调用generateThumbnails
                
            } else {
                [UIAlertView showAlertWithTitle:@"Error"
                                        message:@"Failed to load video"];
            }
        });
    }
}

- (void) generateThumbnails {
}

构建基础结构后,下面具体实现方法。代码清单4-14给出了generateThumbnails方法的实现。

代码清单4-11 generateThumbnails的实现

- (void)generateThumbnails {
    
    self.imageGenerator =                                                   // 1
        [AVAssetImageGenerator assetImageGeneratorWithAsset:self.asset];
    
    // Generate the @2x equivalent
    self.imageGenerator.maximumSize = CGSizeMake(200.0f, 0.0f);             // 2

    CMTime duration = self.asset.duration;

    NSMutableArray *times = [NSMutableArray array];                         // 3
    CMTimeValue increment = duration.value / 20;
    CMTimeValue currentValue = 2.0 * duration.timescale;
    while (currentValue <= duration.value) {
        CMTime time = CMTimeMake(currentValue, duration.timescale);
        [times addObject:[NSValue valueWithCMTime:time]];
        currentValue += increment;
    }

    __block NSUInteger imageCount = times.count;                            // 4
    __block NSMutableArray *images = [NSMutableArray array];

    AVAssetImageGeneratorCompletionHandler handler;                         // 5
    
    handler = ^(CMTime requestedTime,
                CGImageRef imageRef,
                CMTime actualTime,
                AVAssetImageGeneratorResult result,
                NSError *error) {

        if (result == AVAssetImageGeneratorSucceeded) {                     // 6
            UIImage *image = [UIImage imageWithCGImage:imageRef];
            id thumbnail =
                [THThumbnail thumbnailWithImage:image time:actualTime];
            [images addObject:thumbnail];
        } else {
            NSLog(@"Error: %@", [error localizedDescription]);
        }

        // If the decremented image count is at 0, we're all done.
        if (--imageCount == 0) {                                            // 7
            dispatch_async(dispatch_get_main_queue(), ^{
                NSString *name = THThumbnailsGeneratedNotification;
                NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
                [nc postNotificationName:name object:images];
            });
        }
    };

    [self.imageGenerator generateCGImagesAsynchronouslyForTimes:times       // 8
                                              completionHandler:handler];
}

(1)首先创建一个新的AVAssetlmageGenerator实例,为其传递一个对控制器asset属性的引用。保持对该对象的强引用非常关键。如果没有注意到这一点将遇到麻烦,因为会导致无法调用回调。

(2) AVAssetImageGenerator为配置图片生成定义了一些属性。 虽然为大部分属性提供了合理的默认值,不过有一个属性在每次使用时都需要明确配置,就是maximumSize属性。 默认情况下,捕捉的图片都保持原始维度。如果处理720p或1080p视频的话,则创建的图片会非常大。设置maximumSize属性会自动对图片的尺寸进行缩放并显著提高性能。指定一个width值为200、height值 为0的CGSize。这样可以确保生成的图片都遵循一定宽度, 并且会根据视频的宽高比自动设置高度值。

(3)下面需要做的是执行一些计算来生成CMTime值的集合,这些值用来指定视频中的捕捉位置。代码中将视频时间轴平均分成20个CMTime值。循环遍历视频的duration,使用CMTimeMake函数创建了一个新的时间,之后将结果CMTime封装成一个NSValue保存 在times数组中。

(4)基于times数组中元素的个数,定义一个名为imageCount的_block变量。 这用于确定所有图片处理完成的时间。还定义一个block变量,类型为NSMutableArray,名为images。用于保存生成图片的集合。 _block修饰词用来确保回调block操作直接发生在这些指针上而非副本上。

(5)接下来定义了一个AVAssetlmageGeneratorCompletionHandler类型的回调块。这是其中一个较长的代码块定义,下面看一下它的参数:

●requestedTime: 请求的最初时间。它对应于生成图像的调用中指定的times数组中的值。
●imageRef: 生成的CGImageRef,如果在给定的时间点没有生成图片则赋值NULL.
●actualTime: 图片实际生成的时间。基于实际效率,这个值可能与请求时间不同。可以在生成图片前通过在AVAssetImageGenerator实例设置requestedTime ToleranceBefore和requestedTimeToleranceAfter 值来调整requestedTime和actualTime的接近程度。
●result: AVAssetImageGeneratorResult 用来表示图片是成功生成、失败还是取消。
●error:一个NSError指针,如果收到AVAssetlmageGeneratorFailed的AVAssetlmageGeneratorResult,可以通过这个NSError指针诊断问题。

(6)如果result值为AVAssetlmageGeneratorSucceeded,则表示图片已经成功生成了,基于返回的CGImageRef创建一个新的Ullmage。接下来创建一个新的THThumbnail实例将图片和时间信息打包,并添加到数组中。

(7)在回调块的每次调用中,使imageCount属性减1并判断其是否等于0,如果等于0则表明所有图片都处理完成了。之后发送一个新的名为THThumbnails- GeneratedNotification的应用程序专用通知消息,将图片集合作为object参数传递。视图层会接收该通知并用它生成可视化搓擦条。

再次运行该应用程序。现在当我们点击Show按钮时会看到黑色的条被一串缩略图所替代,缩略图对应于视频文件中的不同时间点。点击一张图片会调用我们之前实现的委托的jumpedToTime:方法。注意AVAssetlmageGenerator可为本地资源和远程资源生成图片,不过可以预料的是,当为远程资源生成图片会消耗比较长的时间。这种情况下可以使用效率更好的方法以提升用户体验,比如为每个返回的图片创建可视化布局,或将其写入图片缓存并让视图定期轮询缓存。

注意:
大部分播放用例都可以在iOs模拟器中进行测试。不过在实际设备.上测试时性能表现更好。

4.7显示字幕

使应用程序被尽可能多的用户接受是一件非常重要的事,这就意味着我们需要让用户可以使用本国母语访问我们的应用程序,同时还要考虑存在听觉障碍或有其他辅助功能需求的用户。视频播放器在这一点上提高用户体验常用的方法就是随时提供字幕。AV Foundation在展示字幕或隐藏式字幕方面提供了可靠方法。AVPlayerLayer会自动渲染这些元素,并且可以让开发者告诉应用程序哪些元素需要渲染。完成这些操作要用到AVMediaSelectionGroup和AVMediaSelectionOption两个类。

AVMediaSelectionOption表示AVAsset中的备用媒体呈现方式。一个资源可能包含备用媒体呈现方式,比如备用音频、视频或文本轨道。这些轨道可能是指定语言的音频轨道、备用相机角度或此刻我们所感兴趣的指定语言的字幕。确定存在哪些备用轨道要用到一个名为availableMediaCharacteristicsWithMediaSelectionOptions的AVAsset属性(我之前就说过AVFoundation团队喜欢这种长名字)。这个属性会返回一个包含字符串的数组,这些字符串用于表示保存在资源中可用选项的媒体特征。具体来说,返回数组所包含的字符串值为AVMediaCharacteristicVisual(视频)、AVMediaCharacteristicAudible(音频)、AVMediaCharacteristicLegible (字幕或隐藏式字幕)。

请求可用媒体特性数据后,调用AVAsset的mediaSelectionGroupForMediaCharaceristic:方法,为其传递要检索的选项的特定媒体特性。这个方法会返回一个AVMediaSelectionGroup,它作为一个或多个互斥的AVMediaSelectionOption实例的容器。下面看一个简 单示例:

NSArray *mediaCharacteristics = self.asset.availableMediaCharacteristicsWithMediaSelectionOptions;
for (NSString *characteristic in mediaCharacteristics) {
    AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:characteristic];
    NSLog (@"[&@]", characteristic);
    for (AVMediaSelectionOption *option in group.options) {
        NSLog (@"Option: 8@",option.displayName);
    }
}

为包含一个或多个字幕的资源运行这段代码所生成的输出内容如下所示:

[AVMediaCharacteristicLegible]
Option: English
Option: Italian
option: Portuguese
Option: Russian
[AVMedi aCharacteristicAudible]
Option: English

在示例中可以看到多个字幕轨道以及一个English音频轨道。

当我们载入正确的AVMediaSelectionGroup并定义好需要的AVMediaSelectionOption之后,下一步就是付诸实际行动了。通过在激活的AVPlayerltem上调用selectMediaOption:inMediaSelectionGroup:来实现这一功能。 比如,如果需要显示俄文字幕,如下编码:

AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:characteristic];
NSLocale *russianLocale = [ [NSLocale alloc] initWithLocaleIdentifier:@"ru_RU"];
NSArray *options = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:group.options
                                                              withLocale:russianLocale];
AVMediaSelectionOption *option = [options firstobject];
[self.playerItem selectMediaOption:option inMediaselectionGroup:group];

下面在Video Player应用程序中具体实施,在THPlayerController类中添加几个新的方法。首先如代码清单4-15所示进行一些修改。

代码清单4-15 loadMediaOptions 设置

- (void)prepareToPlay {
    NSArray *keys = @[
        @"tracks",
        @"duration",
        @"commonMetadata",
        @“availableMediaCharacteristicsWithMediaSelectionOptions”//loadMediaOptions 设置
    ];
    ...
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if (context == &PlayerItemStatusContext) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
                ...
                [self loadMediaOptions];
            } else {
                [UIAlertView showAlertWithTitle:@"Error"
                                        message:@"Failed to load video"];
            }
        });
    }
}

- (void) loadMediaoptions {
}

在prepareToPlay方法中,我们希望在它的自动载入属性列表中加入availableMediaCharacteristicsWithMediaSelectionOptions属性。在调用任何媒体选择API前载入该属性很有必要,这样会避免主线程拥堵。当播放器条目准备播放就绪时调用loadMediaOptions方法。

loadMediaOptions方法的实现如代码清单4-16所示。

代码清单4-16 loadMediaOptions 实现

- (void)loadMediaOptions {
    NSString *mc = AVMediaCharacteristicLegible;                            // 1
    AVMediaSelectionGroup *group =
        [self.asset mediaSelectionGroupForMediaCharacteristic:mc];          // 2
    if (group) {
        NSMutableArray *subtitles = [NSMutableArray array];                 // 3
        for (AVMediaSelectionOption *option in group.options) {
            [subtitles addObject:option.displayName];
        }
        [self.transport setSubtitles:subtitles];                            // 4
    } else {
        [self.transport setSubtitles:nil];
    }
}

(1)我们只对查找资源中的字幕选项感兴趣,所以只定义了一个媒体特性字符串,并令它的值为AVMediaCharacteristicLegible。

(2)请求与已定义媒体特性对应的AVMediaSelectionGroup。

(3)假设找到一组数据(应用程序本地资源包含字幕、远程资源不包含),创建一个包含要传递给视图层的用户可呈现字符串的数组,做法是请求每个选项的displayName属性。

(4)最后,设置播放栏上的字幕字符串集合,使其可在字幕选择界面中呈现。在else条件中,传递nil,表示没有可呈现的界面。

当用户选择一个字幕时,需要一个方法来处理该选择,并在当前播放器条目上激活相应的AVMediaSelectionOption。代码清单4-17给出了这个方法的实现。

代码清单4-17处理字幕选择

- (void)subtitleSelected:(NSString *)subtitle {
    NSString *mc = AVMediaCharacteristicLegible;
    AVMediaSelectionGroup *group =
        [self.asset mediaSelectionGroupForMediaCharacteristic:mc];          // 1
    BOOL selected = NO;
    for (AVMediaSelectionOption *option in group.options) {
        if ([option.displayName isEqualToString:subtitle]) {
            [self.playerItem selectMediaOption:option                       // 2
                         inMediaSelectionGroup:group];
            selected = YES;
        }
    }
    if (!selected) {
        [self.playerItem selectMediaOption:nil                              // 3
                     inMediaSelectionGroup:group];
    }
}

(1])为资源中包含的有效选项检索AVMediaSelectionGroup。
(2)循环遍历所有组中的选项,并找到与传递给subtitleSelected:方法的字幕字符串匹配的AVMediaSelectionOption。找到正确选项后,在播放器条目上调用selectMediaOption:inMediaSelectionGroup:方法激活它。这样选中的字暮就会立即出现在AVPlayerLayer上。
(3)如果用户在字幕选项列表中选择None,则为选中媒体选项设置nil,以便移除展示中的字幕。

最后需要做的一件事是打开VideoPlayer-Prefix.pch文件并将ENABLE SUBTITLES的定义由0改为1。如果当前媒体中有可用字幕,则播放视图会展示合适的字幕选择界面。

再次运行应用程序,在播放栏右下角会看到一个新的按钮。选中按钮查看可用的字幕,选择一个选项,瞧!神奇的一幕出现了。

4.8 Airplay

最后一个需要讨论的优化问题是在Video Player应用程序中整合AirPlay功能。AirPlay是苹果公司推出的一项技术,旨在用无线方式将流媒体音频和视频内容在Apple TV上播放,或者将纯音频内容在多种第三方音频系统中播放。如果用户拥有Apple TV或其他音频系统中的一个,就会知道这个功能简直太神奇了。好消息是将这个功能整合到应用程序非常容易实现。

AVPlayer有一个属性allowsExternalPlayback,允许启用或禁用AirPlay播放功能。该属性的默认值为YES,即在不做任何额外编码的情况下,播放器应用程序也会自动支持AirPlay功能。虽然通常AirPlay功能 是需要的,不过如果由于某些强制的原因要禁用该功能,可以通过设置allowsExtermalPlayback属性为NO来实现。

线路选择功能

iOS为选择AirPlay线路提供了一个整体的界面。具体的用户界面和手势动作取决于iOS的版本。在iOS 6及早期版本中,用户双击Home键启动dock,向右滑动找到选择AirPlay线路的界面,如图4-6所示。


AVFoundation开发秘籍笔记:第4章 视频播放_第6张图片

iOs 7及之后的版本提供了一种更方便的方法访问该界面。在屏幕底端向上滑动打开控制中心(Control Center),选择AirPlay按钮即可, 如图4-7所示。


AVFoundation开发秘籍笔记:第4章 视频播放_第7张图片

虽然使用整体线路选择方法可以帮助用户实现期望的功能,不过其在用户体验方面做的还不够理想。尤其是iOS 6版本中需要用户跳出应用程序,会中断应用程序的工作流。另一个重要的需要注意的是很多用户并不知道这个整体界面,很可能完全忽略这个强大实用的功能。所以开发者应该以比较明显的方式在应用程序内部提供AirPlay线路选择界面。有趣的是,iOS并没有AirPlay框架或API供开发者使用,取而代之的是我们使用MediaPlayer框架中的MPVolumeView类来实现这个功能。

使用这个组件时,需要关联和导入MediaPlayer框架()并创建一个MPVolumeView实例,如下面代码所示:

CGRect rect = // desired frame
MPVolumeView *volumeView = [[MPVolumeView alloc] initwithFrame:rect];
[self.view addSubview:volumeView];

默认的MPVolumeView实例提供两个用户界面元素。顾名思义,其中一个元素是控制系统音量的滑动条。它所提供的功能等同于iOS设备侧面的硬音量调节按钮(硬件)。如果用户网络中存在可用的AirPlay设备,则会额外显示一个 AirPlay路线选择按钮。点击按钮会显示所有可用AirPlay线路的列表。

如果只需要展示线路选择按钮,可以对应用程序做如下修改。

MPVolumeView *volumeView = [[MPVolumeView alloc] init];
volumeView.showsVolumeSlider = NO;
[volumeView sizeToFit];
[transportView addsubview:volumeView];

有一点需要明确,只有当用户具有可用AirPlay目标而且WiFi网络启用时才会显示线路选择按钮。这两个条件只要有一个不满足,MPVolumeView就 会自动隐藏按钮。

注意:
MPVolumeView只有在iOS设备上才可以显示,iOS 模拟器是不可以显示的。

我们不准备对实现过程进行详细讲解,因为与同前面的示例一样简单。如果可能的话应用程序已经创建好了线路选择按钮,不过我们还是需要打开VideoPlayer Prefix.pch,将

NABLE AIRPLAY定义由0修改为1。如果你有一个Apple TV或一个支持AirPlay的音频系统,当运行应用程序时就会看到AirPlay线路选择按钮了。

要了解更多关于AirPlay及其用法的高级技术,可到Apple Developer Center中 查找AirPlay Overview文档。

4.9 小结

本章深入探讨了AV Foundation的视频播放功能。现在我们知道了如何通过AVPlayer播放AVPlayerItem实例,并直接将视频输出为AVPlayerLayer实例。我们还第一次接触了AVAsetlmageGenerator,用它来创建播放器的可视化搓擦条。开发者会发现这是在AVFoundation中的不同场景都有用的类。最后我们通过整合AirPlay功能提高视频播放的体验,并使用AVMediaSelectionGroup和AVMediaSelectionOption来展示字幕。本章构建的示例应用程序对开发者编写任何视频播放解决方案都是一个好起点。

你可能感兴趣的:(AVFoundation开发秘籍笔记:第4章 视频播放)