4.1 播放功能綜述
當(dāng)開(kāi)發(fā)一個(gè)自定義播放器時(shí)會(huì)用到大量的對(duì)象。本節(jié)從一個(gè)較高層級(jí)的介紹入手,通過(guò)探究其所扮演的角色和所含類之間的關(guān)系來(lái)學(xué)習(xí)AV Foundation的播放功能。后面還會(huì)繼續(xù)深入分析具體的API,并通過(guò)實(shí)際開(kāi)發(fā)一個(gè) 自定義視頻播放器來(lái)實(shí)際使用這些類。圖4-1 概要顯示了用到的類及其關(guān)系。

4.1.1 AVPlayer
AV Foundation的播放都圍繞AVPlayer類展開(kāi),AVPlayer是一個(gè)用來(lái)播放基于時(shí)間的視聽(tīng)媒體的控制器對(duì)象。支持播放從本地、分步下載或通過(guò)HTTP Live Streaming協(xié)議得到的流媒體,并在多種播放場(chǎng)景中播放這些視頻資源。需要說(shuō)明的是,當(dāng)我們說(shuō)“控制器”時(shí),是指我們通常的理解,它不是一個(gè)視圖或窗口控制器,而是一個(gè)對(duì)播放和資源時(shí)間相關(guān)信息進(jìn)行管理的對(duì)象。開(kāi)發(fā)者通過(guò)框架提供的應(yīng)用程序接口來(lái)開(kāi)發(fā)控制播放基于時(shí)間的媒體的用戶界面。
AVPlayer是一個(gè)不可見(jiàn)組件。如果播放MP3或AAC音頻文件,那么沒(méi)有可視化的用戶界面也不會(huì)有什么問(wèn)題。不過(guò)如要播放一個(gè)QuickTime電影或一個(gè)MPEG-4視頻,會(huì)導(dǎo)致非常不好的用戶體驗(yàn)。要將視頻資源導(dǎo)出到用戶界面的目標(biāo)位置,需要使用AVPlayer類。
注意:
AVPlayer只管理一個(gè)單 獨(dú)資源的播放,不過(guò)框架還提供了AVPlayer的一個(gè)子類AVQueue-Player,可以用來(lái)管理一個(gè)資源隊(duì)列。當(dāng)你需要在一個(gè)序列中播放多個(gè)條目或者為音頻、視頻資源設(shè)置播放循環(huán)時(shí)可使用該子類。
4.1.2 AVPlayerLayer
AVPlayerLayer構(gòu)建于Core Animation之上,是AV Foundation中能找到的為數(shù)不多的可見(jiàn)組件。Core Animation是Mac和iOS平臺(tái)上負(fù)責(zé)圖形渲染與動(dòng)畫(huà)的基礎(chǔ)框架,主要用于這些平臺(tái)資源的美化和動(dòng)畫(huà)流暢度提升。Core Animation本身具有基于時(shí)間的屬性,并且由于它基于OpenGL,所以具有很好的性能,能非常好地滿足AV Foundation的各種需要。
AVPlayerLayer擴(kuò)展了Core Animation的CALayer類,并通過(guò)框架在屏幕上顯示視頻內(nèi)容。這一圖層并不提供任何可視化控件或其他附件(根據(jù)開(kāi)發(fā)者需求搭建的),但是它用作視頻內(nèi)容的渲染面。創(chuàng)建AVPlayerLayer需要一個(gè)指向AVPlayer實(shí)例的指針, 這就將圖層和播放器緊密綁定在一起,保證了當(dāng)播放器基于時(shí)間的方法出現(xiàn)時(shí)使二者保持同步。AVPlayerLayer與其他CALayer樣, 可以設(shè)置為UIView或NSView的備用層,或者可以手動(dòng)添加到一個(gè)已有的層繼承關(guān)系中。
AVPlayerLayer是一個(gè)相對(duì)簡(jiǎn)單的類,使用起來(lái)也簡(jiǎn)單。在這一層中開(kāi)發(fā)者可以自定義的領(lǐng)域只有video gravity??偣部蔀関ideoGravity屬性定義三個(gè)不同的gravity值,用來(lái)確定在承載層的范圍內(nèi)視頻可以拉伸或縮放的程度。圖4-2、 圖4-3和圖4 4給出了一個(gè)16:9的視頻置于4:3矩形范圍內(nèi)的情況,使我們可以看到不同gravity值。



4.1.3 AVPlayerltem
我們最終的目的是使用AVPlayer來(lái)播放AVAsset。如果查看AVAsset文檔,可以找到一些用來(lái)獲取數(shù)據(jù)的方法和屬性,比如創(chuàng)建日期、元數(shù)據(jù)和時(shí)長(zhǎng)等信息。不過(guò)無(wú)法查到如何獲取當(dāng)前時(shí)間的方法,也沒(méi)有在媒體中查找特定位置的方法。這是因?yàn)锳VAsset模型只包含媒體資源的靜態(tài)信息,這些不變的屬性用來(lái)描述對(duì)象的靜態(tài)狀態(tài)。這就意味著僅使用AVAsset對(duì)象是無(wú)法實(shí)現(xiàn)播放功能的。當(dāng)我們需要對(duì)一個(gè)資源及其相關(guān)曲目進(jìn)行播放時(shí),首先需要通過(guò)AVPlayerltem和AVPlayerltemTrack類構(gòu)建相應(yīng)的動(dòng)態(tài)內(nèi)容。
AVPlayerltem會(huì)建立媒體資源動(dòng)態(tài)視角的數(shù)據(jù)模型并保存AVPlayer在播放資源時(shí)的呈現(xiàn)狀態(tài)。在這個(gè)類中我們會(huì)看到諸如IseekToTime:的方法以及訪問(wèn)currentTime和presentationSize的屬性。AVPlayerltem由一個(gè)或多 個(gè)媒體曲目組成,由AVPlayerItemTrack類建立模型。AVPlayerItemTrack實(shí)例用于表示播放器條目中的類型統(tǒng)一的媒體流,比如音頻或視頻。AVPlayerltem中的曲目直接與基礎(chǔ)AVAsset中的AVAssetTrack實(shí)例相對(duì)應(yīng)。
4.2 播放秘籍
僅掌握這些類的簡(jiǎn)單概念還不夠,下面通過(guò)一小段代碼來(lái)看一下如何設(shè)置播放棧來(lái)播放保存在應(yīng)用程序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];
}
該示例中對(duì)播放視頻文件所需的基礎(chǔ)架構(gòu)進(jìn)行了設(shè)置。不過(guò)在實(shí)際播放視頻內(nèi)容前還需要一個(gè)額外步驟,這是因?yàn)椴シ牌鞯牟シ趴丶€沒(méi)有為播放動(dòng)作做好準(zhǔn)備。AVPlayerltem沒(méi)有準(zhǔn)備播放的界面,不過(guò)取而代之的是基于“主動(dòng)發(fā)起請(qǐng)求”("don’tcall me, I'll call you")的機(jī)制。
AVPlayertem具有一個(gè)名為status的AVPlayerltemStatus類型的屬性。在對(duì)象創(chuàng)建之初,播放條目由AVPlayertemStatusUnknown狀態(tài)開(kāi)始,該狀態(tài)表示當(dāng)前媒體還未載入并且還不在播放隊(duì)列中。將AVPlayerItem與一個(gè)AVPlayer對(duì)象 進(jìn)行關(guān)聯(lián)就開(kāi)始將媒體放入隊(duì)列中,但是在具體內(nèi)容可以播放前,需要等待對(duì)象的狀態(tài)由AVPlayerltemStatusSUnknown變?yōu)锳VPlayerftemStatusReadyToPlay。開(kāi)發(fā)者可通過(guò)Key-Value Observing (KVO)機(jī)制監(jiān)視status屬性的值來(lái)跟蹤這一變化過(guò)程。
KVO是由Foundation框架提供的Observer模式的由蘋(píng)果公司給出的解決方案??梢宰岄_(kāi)發(fā)者注冊(cè)一個(gè)對(duì)象作 為其他對(duì)象狀態(tài)的觀察者。當(dāng)被觀察的對(duì)象狀態(tài)發(fā)生變化時(shí),觀察對(duì)象就會(huì)得到通知并采取相應(yīng)的動(dòng)作。在將AVPlayerItem 與AVPlayer關(guān)聯(lián)之前,開(kāi)發(fā)者需要將代碼設(shè)置為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
}
}
}
當(dāng)觀察到播放控件的status變?yōu)锳VPlayerltemStatusReady ToPlay時(shí),就可以開(kāi)始播放了。
4.3 處理時(shí)間
AVPlayer和AVPlayerltem都是基于時(shí)間的對(duì)象,但是在我們使用它們的功能前,需要了解在AV Foundation框架中呈現(xiàn)時(shí)間的方式。
人們傾向于用日子、小時(shí)、分鐘和秒的方式表示時(shí)間。開(kāi)發(fā)人員經(jīng)常將時(shí)間進(jìn)一步精確到亳秒和納秒。所以用一個(gè)雙精度浮點(diǎn)型數(shù)據(jù)表示時(shí)間也合情合理。實(shí)際上,回顧第2章中介紹的AVAudioPlayer,可以看到時(shí)間是以NSTimeInterval表示的,其實(shí)就是簡(jiǎn)單地對(duì)double值進(jìn)行了typedef定義。不過(guò)使用浮點(diǎn)型數(shù)據(jù)類型表示時(shí)間存在一定問(wèn)題, 因?yàn)楦↑c(diǎn)型數(shù)據(jù)的運(yùn)算會(huì)導(dǎo)致不精確的情況。當(dāng)進(jìn)行多時(shí)間計(jì)算累加時(shí)這些不精確的情況就會(huì)特別嚴(yán)重,經(jīng)常導(dǎo)致時(shí)間的明顯偏移,使得媒體的多個(gè)數(shù)據(jù)流幾乎無(wú)法實(shí)現(xiàn)同步。此外,以浮點(diǎn)型數(shù)據(jù)呈現(xiàn)時(shí)間信息無(wú)法做到自我描述,這就導(dǎo)致在使用不同時(shí)間軸進(jìn)行比較和運(yùn)算時(shí)比較困難。AV Foundation使用一種可靠性更高的方法來(lái)展示時(shí)間信息,這就是基于CMTime數(shù)據(jù)結(jié)構(gòu)。
CMTime
AV Foundation是基于Core Media的高層封裝。Core Media是基于C的底層框架,提供了許多處理Mac和iOS媒體棧的關(guān)鍵功能。雖然這個(gè)框架通常都在后臺(tái)工作,不過(guò)其中一個(gè)我們經(jīng)常能夠接觸到的部分就是它的數(shù)據(jù)結(jié)構(gòu)CMTime。CMTime 為時(shí)間的正確表示給出了一種結(jié)構(gòu),即分?jǐn)?shù)值的方式。具體定義如下:
typedef struct {
CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;
這個(gè)結(jié)構(gòu)最關(guān)鍵的兩個(gè)值是value和timescale。value是一個(gè)64位整數(shù)值,timescale是一個(gè)32位整數(shù)值,在時(shí)間呈現(xiàn)樣式中分別作為分子和分母。
建立以分?jǐn)?shù)的格式處理時(shí)間數(shù)據(jù)的思維方式可能開(kāi)始不太習(xí)慣,不過(guò)當(dāng)開(kāi)發(fā)者多使用幾 次這種方式之后就會(huì)慢慢習(xí)慣。下面看幾個(gè)示例,了解如何使用CMTimeMake函數(shù)創(chuàng)建時(shí)間。
// 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;
除對(duì)CMTime進(jìn)行定義外,CMTime.h頭文件還定義了大量實(shí)用的函數(shù)用于簡(jiǎn)化時(shí)間的處理。與大部分蘋(píng)果公司的底層C框架一樣,最好的參考資料就是頭文件,所以這里建議大家仔細(xì)閱讀CMTime.h頭文件,了解其中定義的函數(shù)的功能。
4.4 創(chuàng)建視頻播放器
本節(jié)通過(guò)創(chuàng)建一個(gè)iOS 視頻播放器(如圖45所示)來(lái)深入學(xué)習(xí)AV Foundation播放API的細(xì)節(jié)。應(yīng)用程序能播放本地和遠(yuǎn)程媒體,支持播放、暫停和拖動(dòng)媒體時(shí)間軸。完成基本功能后,還需要對(duì)應(yīng)用程序進(jìn)一步優(yōu)化以改善用戶體驗(yàn)。 可以在Chapter 4目錄中找到名為VideoPlayer_Starter的示例項(xiàng)目。

4.4.1 創(chuàng)建視頻視圖
第一步需要?jiǎng)?chuàng)建一個(gè)用來(lái)在屏 幕上展示視頻內(nèi)容的視圖。在示例項(xiàng)目中的THVideoPlayer/Views文件組下面,可以找到一個(gè)名為T(mén)HPlayerView的類。這個(gè)類就是用來(lái)展示視頻內(nèi)容并為操作視頻播放提供用戶界面的類。下面看一下這個(gè)類的接口,如代碼清單4-1所示。
代碼清單4-1 THPlayerView 接口
#import "THTransport.h"
@class AVPlayer;
@interface THPlayerView : UIView
- (id)initWithPlayer:(AVPlayer *)player;
@property (nonatomic, readonly) id <THTransport> transport;
@end
這是一個(gè)僅帶有幾個(gè)方法的簡(jiǎn)單類。通過(guò)調(diào)用其initWithPlayer:初始化方法,并傳遞當(dāng)前AVPlayer實(shí)例的引用進(jìn)行實(shí)例化。這樣就可以將播放器輸出的視頻直接展示在這個(gè)視圖中,只讀屬性transport負(fù)責(zé)管理展示在視圖中的可視化控件。在講解應(yīng)用程序播放控制器類的實(shí)現(xiàn)時(shí)會(huì)看到它是如何工作的。下 面我們來(lái)看這個(gè)類的具體實(shí)現(xiàn)。
視圖本身并不是視頻輸出的目標(biāo),相反,開(kāi)發(fā)者需要將播放器輸出指向一個(gè)AVPlayerLayer實(shí)例??梢允謩?dòng)創(chuàng)建層,并將它添加到視圖的層繼承關(guān)系中,但是在iOS平臺(tái)下有一種更便捷的方法。UIView視圖都受Core Animation層的支持,默認(rèn)情況下,就是CALayer的通用實(shí)例,不過(guò)你可以通過(guò)在UIView中重寫(xiě)layerClass方法自定義支持層的類型,以便在實(shí)例化一個(gè)視圖的時(shí)候返回一個(gè)要使用的自定義CALayer。在使用AVPlayerL ayer對(duì)象時(shí)上述方法更加方便,因?yàn)椴恍枰謩?dòng)創(chuàng)建和操作層以及層繼承關(guān)系。代碼清單4-2給出了THPlayerView類的實(shí)現(xiàn)。
代碼清單4-2 THPlayerView 實(shí)現(xiàn)
#import "THPlayerView.h"
#import "THOverlayView.h"
#import <AVFoundation/AVFoundation.h>
@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 <THTransport>)transport {
return self.overlayView;
}
@end
(1)創(chuàng)建一個(gè)類擴(kuò)展來(lái)定義一個(gè)私有屬性用于保存指向THOverlayView視圖實(shí)例的指針。這個(gè)類提供用戶界面中操作視頻播放的控件。
(2)重寫(xiě)layerClass類 方法返回一個(gè)AVPlayerLayer類。 每當(dāng)創(chuàng)建THPlayerView實(shí)例時(shí),就會(huì)使用AVPlayerLayer作為它的支持層。
(3)創(chuàng)建時(shí)沒(méi)有給出默認(rèn)的尺寸大小,所以開(kāi)發(fā)者需要調(diào)用帶有zero-sized框架的超類初始化方法。展示視圖的視圖控制器負(fù)責(zé)設(shè)置合適的框架。
(4)這是該類中最關(guān)鍵的一行代碼。 我們希望獲得傳入初始化方法的AVPlayer實(shí)例并在AVPlayerLayer上對(duì)其進(jìn)行設(shè)置。這一步將從AVPlayer輸出的視頻指向AVPlayerL ayer實(shí)例。
(5)在NIB中定義覆蓋視圖,通過(guò)調(diào)用loadNibNamed:owner:options方法創(chuàng)建視圖實(shí)例。當(dāng)視圖創(chuàng)建完成并賦給overlayView屬性后,將其作為子視圖進(jìn)行添加。
完成THPlayerView的實(shí)現(xiàn)后,下面將注意力轉(zhuǎn)移到THPlayerController類。
4.4.2 創(chuàng)建視頻控制器
在項(xiàng)目的THVideoPlayer/Controllers組下面,可找到THPlayerController類的具體實(shí)現(xiàn)代碼。這個(gè)類為應(yīng)用程序完成了很多功能,也是我們處理核心播放API方法的地方。代碼清單4-3給出了這個(gè)類的接口。
代碼清單4-3 THPlayerController 接口
@interface THPlayerController : NSObject
- (id)initWithURL:(NSURL *)assetURL;
@property (strong, nonatomic, readonly) UIView *view;
@end
創(chuàng)建一個(gè)THPlayerController實(shí)例時(shí),需要調(diào)用其initWithURL:方法,并傳遞需要播放的媒體的NSURL。AVPlayer可用來(lái)播放本地或流媒體,所以這個(gè)URL可以是本地文件URL,也可以是遠(yuǎn)程HTTP URL。該類還為相關(guān)視圖提供了一個(gè)只讀屬性,以便客戶端UIViewController可將視圖添加到視圖繼承關(guān)系中。返回的視圖是一個(gè)THPlayerView實(shí)例,不過(guò)由于這些細(xì)節(jié) 需要對(duì)客戶端隱藏,所以返回一個(gè)通用UIView即可。
轉(zhuǎn)過(guò)來(lái)看類的具體實(shí)現(xiàn),首先創(chuàng)建一個(gè)類擴(kuò)展來(lái)定義控制器的內(nèi)部屬性(如代碼清單4-4所示)。
代碼清單4-4 THPlayerController 類擴(kuò)展
#import "THPlayerController.h"
#import <AVFoundation/AVFoundation.h>
#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 () <THTransportDelegate>
@property (strong, nonatomic) AVAsset *asset;
@property (strong, nonatomic) AVPlayerItem *playerItem;
@property (strong, nonatomic) AVPlayer *player;
@property (strong, nonatomic) THPlayerView *playerView;
@property (weak, nonatomic) id <THTransport> transport;
@property (strong, nonatomic) id timeObserver;
@property (strong, nonatomic) id itemEndObserver;
@property (assign, nonatomic) float lastPlaybackRate;
@end
在這個(gè)類的實(shí)現(xiàn)中,首先創(chuàng)建了一個(gè)類擴(kuò)展來(lái)定義對(duì)象需要的存儲(chǔ)屬性。注意該擴(kuò)展遵循THTransportDelegate協(xié)議并定義了一個(gè)transport屬性。該類和THOverlayView之間會(huì)有很多交互操作,用來(lái)定義管理視頻播放的用戶界面。雖然這些類需要溝通,不過(guò)它們不必直接了解彼此。要斷開(kāi)這個(gè)關(guān)聯(lián),需要用到THTransport和THTransportDelegate協(xié)議(如代碼清單4-5所示)。
代碼清單4-5 THTransport.h
#import <AVFoundation/AVFoundation.h>
@protocol THTransportDelegate <NSObject>
- (void)play;
- (void)pause;
- (void)stop;
- (void)scrubbingDidStart;
- (void)scrubbedToTime:(NSTimeInterval)time;
- (void)scrubbingDidEnd;
- (void)jumpedToTime:(NSTimeInterval)time;
@end
@protocol THTransport <NSObject>
@property (weak, nonatomic) id <THTransportDelegate> delegate;
- (void)setTitle:(NSString *)title;
- (void)setCurrentTime:(NSTimeInterval)time duration:(NSTimeInterval)duration;
- (void)setScrubbingTime:(NSTimeInterval)time;
- (void)playbackComplete;
@end
THOverlayView遵循THTransport協(xié)議,它可以為與覆蓋視圖進(jìn)行通信提供正式接口。當(dāng)播放欄(transport)發(fā)生變化時(shí),比如用戶改變時(shí)間軸位置或點(diǎn)擊Play/Pause按鈕,控制器對(duì)象會(huì)執(zhí)行相應(yīng)的委托回調(diào)。稍后將看到具體的實(shí)現(xiàn)過(guò)程,代碼清單4-6給出了THPlayerController的實(shí)現(xiàn)。
代碼清單4-6 THPlayerController 實(shí)現(xiàn)
@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傳遞給初始化方法來(lái)創(chuàng)建一個(gè)AVAsset。資源創(chuàng)建完成后,調(diào)用控制器的prepareToPlay方法來(lái)設(shè)置播放該資源所需的基礎(chǔ)結(jié)構(gòu)。
(2)框架會(huì)自動(dòng)載入資源的tracks屬性,省去了通過(guò)AVAsynchronousKeyValue oading協(xié)議手動(dòng)載入該屬性的過(guò)程。不過(guò)在以前,開(kāi)發(fā)者仍然需要執(zhí)行l(wèi)oadValuesAsynchronouslyForKeys:completionHandler:方法來(lái)載入需要訪問(wèn)的其他資源屬性。iOS 7和Mac OS 10.9在AVPlayertem的處理上有了大幅改進(jìn),通過(guò)使用新的初始化方法initWithAsset:automaticallyLoadedAssetKeys:或playerItemWithAsset:automaticallyLoadedAssetKeys:創(chuàng)建一個(gè)AVPlayerltem實(shí)例,將任意屬性集的載入委托給該框架。兩種方式都將NSArray用作第二個(gè)參數(shù),包含了隨著AVPlayerItem在初始化隊(duì)列中的載入過(guò)程所需的資源鍵。使用這個(gè)方法自動(dòng)載入tracks、duration和commonMetadata屬性。
(3)添加Iself作為AVPlayerltem的status屬性監(jiān)聽(tīng)器。回顧一下創(chuàng)建過(guò)程,播放項(xiàng)開(kāi)始時(shí)的status狀態(tài)為AVPlayerltemStatusUnknown,播放項(xiàng)直到狀態(tài)變?yōu)锳VPlayertemStatusReadyToPlay才可以開(kāi)始播放。對(duì)status屬性的鍵值觀察可以讓你監(jiān)聽(tīng)變化。
(4)為新創(chuàng)建的AVPlayerltem對(duì)象創(chuàng)建一個(gè) AVPlayer實(shí)例。AVPlayer會(huì)立即開(kāi)始媒體隊(duì)列化的過(guò)程。
(5)最后,創(chuàng)建一個(gè)THPlayerView實(shí)例,傳遞給它一個(gè)指向AVPlayer實(shí)例的指針。開(kāi)發(fā)者還需要為T(mén)HPlayerController和ITHTransport設(shè)置關(guān)系。
4.4.3 監(jiān)聽(tīng)狀態(tài)改變
我們已經(jīng)將THPlayerController設(shè)置為播放項(xiàng)的status屬性的監(jiān)聽(tīng)器。在對(duì)該屬性監(jiān)聽(tīng)前,需要實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context方法,如代碼清單4-7所示。
代碼清單4-7監(jiān)聽(tīng) 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沒(méi)有指定在哪個(gè)線程執(zhí)行status改變通知,所以在采取下一步動(dòng)作前,需要通過(guò)dispatch_async確保應(yīng)用程序返回到主線程,向其傳遞一個(gè)主隊(duì)列的引用。
(2)通過(guò)調(diào)用私有方法addPlayerltemTimeObserver和addItemEndObserverForPlayerItem設(shè)置播放器的時(shí)間監(jiān)聽(tīng)器。下面的小節(jié)會(huì)討論這些方法和時(shí)間監(jiān)聽(tīng)器。
(3)在ransport對(duì)象上設(shè)置當(dāng)前時(shí)間和總長(zhǎng)。將用戶界面上展示的時(shí)間與播放的媒體進(jìn)行同步。transport對(duì)象無(wú)法識(shí)別CMTime,只能處理以秒為單位的NSTimelInterval類型的時(shí)間。我們使用CMTimeGetSeconds函數(shù)將CMTime值轉(zhuǎn)換為秒。Core Media定義了常量kCMTimeZero,開(kāi)發(fā)者可以將它作為開(kāi)頭的currentTime參數(shù),使用播放條目的duration屬性值作為第二個(gè)參數(shù)。
(4)向播放欄傳遞一個(gè)標(biāo)題字符串,來(lái)展示資源的標(biāo)題(如果資源的元數(shù)據(jù)中存在標(biāo)題信
息)。AVAsset沒(méi)有title屬性,這是我們加入AVAsset中的一個(gè)分類方法,目的是增加代碼的可讀性。這個(gè)分類方法用到了,上一章介紹的元數(shù)據(jù)API,具體地講,從資源的commonMetadata得到AVMetadataCommonKeyTitle值。具體細(xì)節(jié)參考AVAsset+THAdditions。
(5)現(xiàn)在就準(zhǔn)備調(diào)用AVPlayer的play方法進(jìn)行播放了。最后,在完成對(duì)status 關(guān)鍵路徑的監(jiān)聽(tīng)后,我們希望將作為監(jiān)聽(tīng)器的self移除。
現(xiàn)在可以啟動(dòng)應(yīng)用程序并開(kāi)始播放其中一個(gè)視頻。雖然視頻已經(jīng)播放,不過(guò)用戶界面上的控件還沒(méi)有提供任何功能,并且隨著時(shí)間的推移用戶界面也沒(méi)有相應(yīng)的反饋信息。這就又回到了addPlayerItemTimeObserver方法上,我們需要在該方法上實(shí)現(xiàn)相關(guān)的功能,不過(guò)在此之前我們需要先學(xué)習(xí)如何得知AVPlayer的時(shí)間變化。
4.5 時(shí)間監(jiān)聽(tīng)
我們已經(jīng)討論過(guò)并了解到如何使用KVO來(lái)觀察播放條目的status屬性。KVO對(duì)于常見(jiàn)的狀態(tài)監(jiān)控表現(xiàn)得很出色,并且可以監(jiān)聽(tīng)AVPlayerltem和AVPlayer的許多屬性。不過(guò)KVO也有不能勝任的場(chǎng)景,比如需要監(jiān)聽(tīng)AVPlayer的時(shí)間變化。這些監(jiān)聽(tīng)類型都是自身具有明顯的動(dòng)態(tài)特性并需要非常高的精確度,這一點(diǎn)要比標(biāo)準(zhǔn)的鍵值監(jiān)聽(tīng)要求高。為滿足這一需求,AVPlayer提供了兩種基于時(shí)間的監(jiān)聽(tīng)方法,讓?xiě)?yīng)用程序可以對(duì)時(shí)間變化進(jìn)行精準(zhǔn)的監(jiān)聽(tīng)。下面分別看一下這兩個(gè)方法。
4.5.1 定期監(jiān)聽(tīng)
通常情況下,我們希望以一定的時(shí)間間隔獲得通知。如果需要隨著時(shí)間的變化移動(dòng)播放頭位置或更新時(shí)間顯示,這非常重要。利用AVPlayer的addPeriodic TimeObserverForInterval:queue:usingBlock:方法可以很容易地監(jiān)聽(tīng)到此類變化。這個(gè)方法需要傳遞如下參數(shù):
●interv: 一個(gè)用于指定通知周期間隔的CMTime值。
●queue: 通知發(fā)送的順序調(diào)度隊(duì)列。大多數(shù)時(shí)候,我們希望這些通知消息發(fā)生在主隊(duì)列,在如果沒(méi)有明確指定的情況下則默認(rèn)為主隊(duì)列。需要重點(diǎn)注意的是不可以使用并行調(diào)度隊(duì)列,因?yàn)锳PI沒(méi)有處理并行隊(duì)列的方法,否則會(huì)導(dǎo)致一些不可 知的問(wèn)題。
●block:一個(gè)在指定的時(shí)間間隔中將會(huì)在隊(duì)列上調(diào)用的回調(diào)塊。這個(gè)塊傳遞一個(gè)CMTime值用于指示播放器的當(dāng)前時(shí)間。
4.5.2 邊界時(shí)間監(jiān)聽(tīng)
AVPlayer還提供了一種更有針對(duì)性的方法來(lái)監(jiān)聽(tīng)時(shí)間,應(yīng)用程序可以得到播放器時(shí)間軸中多個(gè)邊界點(diǎn)的遍歷結(jié)果。這一方法 主要用于同步用戶界面變更或隨著視頻播放記錄一些非可視化數(shù)據(jù)。比如,可以定義25%、50%和75%邊界的標(biāo)記,以此判斷用戶播放進(jìn)度。要使用這個(gè)功能,需要用到addBoundaryTimeObserverForTimes:queue:usingBlock:方法,并提供如下參數(shù):
●times: CMTime 值組成的一個(gè)NSArray數(shù)組定義了需要通知的邊界點(diǎn)。
●queue: 與定期監(jiān)聽(tīng)類似,為方法提供一個(gè)用來(lái)發(fā)送通知的順序調(diào)度隊(duì)列。指定NULL等同于明確設(shè)置主隊(duì)列。
●block: 每當(dāng)正常播放中跨越一個(gè)邊界點(diǎn)時(shí)就會(huì)在隊(duì)列中調(diào)用這個(gè)回調(diào)塊。有趣的是,該塊不提供遍歷的CMTime值,所以開(kāi)發(fā)者需要為此執(zhí)行一些額外計(jì)算進(jìn)行確定。
本示例應(yīng)用程序沒(méi)有用到邊界時(shí)間監(jiān)聽(tīng),不過(guò)定期監(jiān)聽(tīng)對(duì)應(yīng)用程序的功能非常重要。下面通過(guò)addPlayerltemTimeObserver方法的實(shí)現(xiàn)看一下如何在實(shí)際中使用定期監(jiān)聽(tīng)法,如代碼清單4-8所示。
代碼清單4-8定期監(jiān)聽(tīng)法
- (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使用較長(zhǎng)的類名和方法名。與塊連在一起,一行應(yīng)用程序就會(huì)顯得非常多。這個(gè)方法還可以寫(xiě)得更簡(jiǎn)潔,不過(guò)除非出版社想要這本書(shū)達(dá)到14英尺寬,否則我還是按上面格式撰寫(xiě)應(yīng)用程序吧。不過(guò)這里建議大家在實(shí)際項(xiàng)目代碼中采用更簡(jiǎn)潔的代碼風(fēng)格。
(1)首先創(chuàng)建一個(gè)用于定義通知時(shí)間間隔的CMTime值。這里將間隔定義為0.5秒,這個(gè)時(shí)間粒度足以更新播放器的時(shí)間顯示。
(2)定義發(fā)送回調(diào)通知的調(diào)度隊(duì)列。大多數(shù)情況下,由于我們所要更新的用戶界面處于主線程,所以一般使用主隊(duì)列。
(3)定義一個(gè)回調(diào)塊,在前面定義的時(shí)間周期內(nèi)會(huì)調(diào)用該代碼塊。非常重要的一點(diǎn)是代碼塊要獲取self的弱引用。不這樣做會(huì)出現(xiàn)難以診斷的內(nèi)存泄漏。
(4)在回調(diào)塊內(nèi)部,我們希望通過(guò)CMTimeGetSeconds函數(shù)將代碼塊的CMTime值轉(zhuǎn)換成一個(gè)NSTimeInterval。同樣,還需要將播放條目的duration進(jìn)行轉(zhuǎn)換。傳遞這個(gè)duration信息看起來(lái)是多余的,因?yàn)槲覀円呀?jīng)在KVO回調(diào)中傳遞duration到transport中,不過(guò)transport會(huì)隨著媒體的載入而改變,所以要保持用戶界面的同步,最好還是傳遞最新的值。
(5)最后調(diào)用addPeriodicTimeObserverForInterval:queue:usingBlock:方法并傳遞定義好的參數(shù)。調(diào)用這個(gè)方法會(huì)返回一個(gè)隱含id類型指針。對(duì)這些回調(diào)必須保持一個(gè)強(qiáng)引用。這個(gè)指針還會(huì)用于移除監(jiān)聽(tīng)器。
4.5.3 條目結(jié)束監(jiān)聽(tīng)
另一常見(jiàn)的需要監(jiān)聽(tīng)的事件就是條目播放完畢的時(shí)間,雖然這不同于上面介紹的基于時(shí)間的監(jiān)聽(tīng),不過(guò)我們傾向于認(rèn)為二者有著類似的原理。當(dāng)播放完成時(shí),AVPlayerItem會(huì)發(fā)送一個(gè)AVPlayerItemDidPlayToEndTimeNotification通知。THPlayerController實(shí)例應(yīng) 該注冊(cè)為該通知的監(jiān)聽(tīng)器,這樣就可以采取相應(yīng)的動(dòng)作。代碼清單4_9給出了addItemEndObserverForPlayerItem方法的實(shí)現(xiàn)。
代碼清單4-9條目結(jié)束監(jiān)聽(tīng)
- (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)在定義代碼塊之前,首先需要定義一個(gè)到self的弱引用。與定期監(jiān)聽(tīng)使用的回調(diào)塊類似,如果沒(méi)有建立對(duì)self的弱引用將會(huì)導(dǎo)致內(nèi)存泄漏。這些基于塊的計(jì)數(shù)循環(huán)診斷起來(lái)非常難。
(2)當(dāng)播放完畢時(shí),需要通過(guò)調(diào)用播放器實(shí)例的seekToTime:kCMTimeZero方法重新定位播放頭光標(biāo)回到0位置。
(3)當(dāng)#2的搜索調(diào)用完成時(shí),通知播放欄播放已經(jīng)完成了,這樣就可以重新設(shè)置展示時(shí)間和搓擦條。
(4)通過(guò)注冊(cè)NSNotificationCenter來(lái)添 加itemEndObserver作為通知的監(jiān)聽(tīng)器,并將定義好的參數(shù)傳遞給它。
(5)最后重寫(xiě)dealloc方法,當(dāng)控制器被釋放時(shí)移除作為監(jiān)聽(tīng)器的itemEndObserver。
運(yùn)行應(yīng)用程序??梢钥吹皆谝曨l播放過(guò)程中,隨著時(shí)間的變動(dòng),當(dāng)前時(shí)間和剩余時(shí)間標(biāo)簽中的值不斷更新,并且可以看到時(shí)間搓擦條相應(yīng)地更新播放頭的位置。
下面繼續(xù)實(shí)現(xiàn)其他委托回調(diào)方法,使播放欄控件正常工作。
4.5.4播放欄委托回調(diào)
我們先來(lái)看一下THTransportDelegate協(xié) 議提供的簡(jiǎn)單播放欄回調(diào)的實(shí)現(xiàn)。代碼清單4-10給出了這些方法的實(shí)現(xiàn)。
代碼清單4-10播放欄委托回調(diào)
- (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的實(shí)現(xiàn)不需要過(guò)多解釋,因?yàn)樗薪o播放器的同名方法。同樣,pause方法委托播放器的pause方法,不過(guò)為了條理清晰,仍獲取lastPlaybackRate。 stop方法調(diào)用setRate:并傳遞參數(shù)0,相當(dāng)于調(diào)用了pause,只是采用不同方法實(shí)現(xiàn)同一效果。還對(duì)播放欄調(diào)用了playbackComplete來(lái)更新搓擦條的位置。jumpedToTime:方法 利用播放器的seekToTime:方法跳轉(zhuǎn)到時(shí)間軸上的任意位置。這個(gè)方法的使用會(huì)在本章后面看到。
接下來(lái),看一下如何實(shí)現(xiàn)搓擦條相關(guān)的方法。一共有三個(gè)方法需要實(shí)現(xiàn),分別對(duì)應(yīng)著當(dāng)用戶與UISlider控件交互時(shí)產(chǎn)生的三個(gè)事件,如代碼清單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)會(huì)調(diào)用scrubbingDidStart方法。在這個(gè)方法中開(kāi)發(fā)者將獲取當(dāng)前播放率并暫停播放器。獲取當(dāng)前播放率是為了在搓擦進(jìn)度結(jié)束時(shí)恢復(fù)播放。此外,還需要移除當(dāng)前定期監(jiān)聽(tīng)器,因?yàn)槲覀儾幌M谟脩糁苯涌刂泼襟w進(jìn)度時(shí)觸發(fā)此類事件。
(2)當(dāng)UISlider實(shí)例的值發(fā)生變化時(shí)(UIControlEventValueChanged)會(huì)調(diào)用scrubbedToTime方法。由于這個(gè)方法在用戶移動(dòng)滑動(dòng)條位置時(shí)會(huì)迅速觸發(fā),所以首先應(yīng)該在播放條目上調(diào)用cancelPendingeeks。這是經(jīng)過(guò)性能優(yōu)化的,如果前一個(gè)搜索請(qǐng)求沒(méi)有完成,則避免出現(xiàn)搜索操作堆積情況的出現(xiàn)。開(kāi)發(fā)者可調(diào)用seekToTime:發(fā)起一個(gè)新的搜索, 并將NSTimeInterval值轉(zhuǎn)換為CMTime。
(3)區(qū)域內(nèi)觸控事件(UIControlEventTouchUpInside)會(huì)調(diào)用scrubbingDidEnd方法,用來(lái)表示用戶已經(jīng)完成了搓擦操作。在這個(gè)方法中,需要調(diào)用addPlayerltemTimeObserver重新添加定期監(jiān)聽(tīng)器。之后查看lastPlaybackRate值,如果該值大于0,則表示視頻已經(jīng)播放過(guò)了,需要重新播放該視頻。
通過(guò)上述過(guò)程,最主要的視頻播放功能都已經(jīng)完成了!運(yùn)行應(yīng)用程序,現(xiàn)在可以播放、暫停和調(diào)整視頻播放進(jìn)度。完成了這些核心的播放功能,下 面就需要對(duì)播放中涉及的各功能進(jìn)行優(yōu)化,通過(guò)添加一些功能提高視頻播放的用戶體驗(yàn)。
4.6 創(chuàng)建可視搓擦條
你可能已經(jīng)注意到了播放器右上角有一個(gè)帶 有Show標(biāo)簽的按鈕。如果點(diǎn)擊這個(gè)按鈕,會(huì)發(fā)現(xiàn)在主導(dǎo)航欄下面出現(xiàn)了一個(gè)黑色的欄。目前它還沒(méi)有實(shí)際的功能,不過(guò)下面看一下能否把這個(gè)地方有效利用起來(lái)。
可在AV Foundation中找到一個(gè)名為AVAssetImageGenerator的工具類。這個(gè)類可用來(lái)從一個(gè)AVAsset視頻曲目中提取圖片。這樣可以生成-一個(gè)或多 個(gè)縮略圖,用來(lái)提升應(yīng)用程序用戶界面的效果。
AVAssetImageGenerator定義了兩個(gè)方法實(shí)現(xiàn)從視頻資源中檢索圖片,分別為:
●copyCGImageAtTime:actualTime:error:允許 在指定時(shí)間點(diǎn)捕捉圖片。如果開(kāi)發(fā)者希望捕捉一張圖片那么這個(gè)方法是最適合的,可能用于在視頻列表中展示視頻縮略圖。
●generateCGlmagesAsynchronouslyForTimes:completionHandler: 允許按照第一個(gè)參數(shù)所指定的時(shí)間段生成一個(gè)圖片序列。該方法具有很高的性能,只需要調(diào)用這一個(gè)方法就可以生成一組圖片。
注意:
AVAssetlmageGenerator既可以生成本地圖片,也可以生成持續(xù)下載的資源。不過(guò)它不能從HTTP Live Stream生成圖片。
由此實(shí)現(xiàn)的一個(gè)優(yōu)秀功能就是創(chuàng)建可視搓擦條。不同于在工具欄底部展示的標(biāo)準(zhǔn)搓擦條,這里創(chuàng)建一個(gè)可視化的搓擦條,這樣用戶可以更簡(jiǎn)單地在時(shí)間軸中指定位置并立即跳轉(zhuǎn)到指定位置。下面看一下如何實(shí)現(xiàn)這個(gè)功能(如代碼清單4- 12所示)。
代碼清單4-12生成圖片
#import “THPlayerController.h"
#import <AVFoundation/AVFoundation.h>
#import “THTransport.h"
#import “THPlayerView.h"
#import "AVAsset+THAdditions.h”
#import "UIAlertView+THAdditions.h"
#import "THThumbnail.h"
...
@interface THPlayerController () <THTransportDelegate>
@property (strong, nonatomic) AVAsset *asset;
@property (strong, nonatomic) AVPlayerItem *playerItem;
@property (strong, nonatomic) AVPlayer *player;
@property (strong, nonatomic) THPlayerView *playerView;
@property (weak, nonatomic) id <THTransport> transport;
@property (strong, nonatomic) id timeObserver;
@property (strong, nonatomic) id itemEndObserver;
@property (assign, nonatomic) float lastPlaybackRate;
@property (strong, nonatomic) AVAssetImageGenerator *imageGenerator;
@end
將導(dǎo)入THThumbnail.h頭文件。THThumbnail類是項(xiàng)目中的-一個(gè)簡(jiǎn)單模型對(duì)象,用來(lái)保存我們捕捉到的圖片及其相關(guān)的時(shí)間。還需要添加一一個(gè) AVAssetImageGenerator類型的新屬性。
接下來(lái)添加一個(gè)新方法generateThumbnails并在status監(jiān)聽(tīng)器回調(diào)方法中調(diào)用這個(gè)方法,如代碼清單4-13所示。
代碼清單4-13調(diào)用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]; // 調(diào)用generateThumbnails
} else {
[UIAlertView showAlertWithTitle:@"Error"
message:@"Failed to load video"];
}
});
}
}
- (void) generateThumbnails {
}
構(gòu)建基礎(chǔ)結(jié)構(gòu)后,下面具體實(shí)現(xiàn)方法。代碼清單4-14給出了generateThumbnails方法的實(shí)現(xiàn)。
代碼清單4-11 generateThumbnails的實(shí)現(xiàn)
- (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)首先創(chuàng)建一個(gè)新的AVAssetlmageGenerator實(shí)例,為其傳遞一個(gè)對(duì)控制器asset屬性的引用。保持對(duì)該對(duì)象的強(qiáng)引用非常關(guān)鍵。如果沒(méi)有注意到這一點(diǎn)將遇到麻煩,因?yàn)闀?huì)導(dǎo)致無(wú)法調(diào)用回調(diào)。
(2) AVAssetImageGenerator為配置圖片生成定義了一些屬性。 雖然為大部分屬性提供了合理的默認(rèn)值,不過(guò)有一個(gè)屬性在每次使用時(shí)都需要明確配置,就是maximumSize屬性。 默認(rèn)情況下,捕捉的圖片都保持原始維度。如果處理720p或1080p視頻的話,則創(chuàng)建的圖片會(huì)非常大。設(shè)置maximumSize屬性會(huì)自動(dòng)對(duì)圖片的尺寸進(jìn)行縮放并顯著提高性能。指定一個(gè)width值為200、height值 為0的CGSize。這樣可以確保生成的圖片都遵循一定寬度, 并且會(huì)根據(jù)視頻的寬高比自動(dòng)設(shè)置高度值。
(3)下面需要做的是執(zhí)行一些計(jì)算來(lái)生成CMTime值的集合,這些值用來(lái)指定視頻中的捕捉位置。代碼中將視頻時(shí)間軸平均分成20個(gè)CMTime值。循環(huán)遍歷視頻的duration,使用CMTimeMake函數(shù)創(chuàng)建了一個(gè)新的時(shí)間,之后將結(jié)果CMTime封裝成一個(gè)NSValue保存 在times數(shù)組中。
(4)基于times數(shù)組中元素的個(gè)數(shù),定義一個(gè)名為imageCount的_block變量。 這用于確定所有圖片處理完成的時(shí)間。還定義一個(gè)block變量,類型為NSMutableArray,名為images。用于保存生成圖片的集合。 _block修飾詞用來(lái)確?;卣{(diào)block操作直接發(fā)生在這些指針上而非副本上。
(5)接下來(lái)定義了一個(gè)AVAssetlmageGeneratorCompletionHandler類型的回調(diào)塊。這是其中一個(gè)較長(zhǎng)的代碼塊定義,下面看一下它的參數(shù):
●requestedTime: 請(qǐng)求的最初時(shí)間。它對(duì)應(yīng)于生成圖像的調(diào)用中指定的times數(shù)組中的值。
●imageRef: 生成的CGImageRef,如果在給定的時(shí)間點(diǎn)沒(méi)有生成圖片則賦值NULL.
●actualTime: 圖片實(shí)際生成的時(shí)間。基于實(shí)際效率,這個(gè)值可能與請(qǐng)求時(shí)間不同??梢栽谏蓤D片前通過(guò)在AVAssetImageGenerator實(shí)例設(shè)置requestedTime ToleranceBefore和requestedTimeToleranceAfter 值來(lái)調(diào)整requestedTime和actualTime的接近程度。
●result: AVAssetImageGeneratorResult 用來(lái)表示圖片是成功生成、失敗還是取消。
●error:一個(gè)NSError指針,如果收到AVAssetlmageGeneratorFailed的AVAssetlmageGeneratorResult,可以通過(guò)這個(gè)NSError指針診斷問(wèn)題。
(6)如果result值為AVAssetlmageGeneratorSucceeded,則表示圖片已經(jīng)成功生成了,基于返回的CGImageRef創(chuàng)建一個(gè)新的Ullmage。接下來(lái)創(chuàng)建一個(gè)新的THThumbnail實(shí)例將圖片和時(shí)間信息打包,并添加到數(shù)組中。
(7)在回調(diào)塊的每次調(diào)用中,使imageCount屬性減1并判斷其是否等于0,如果等于0則表明所有圖片都處理完成了。之后發(fā)送一個(gè)新的名為T(mén)HThumbnails- GeneratedNotification的應(yīng)用程序?qū)S猛ㄖ?,將圖片集合作為object參數(shù)傳遞。視圖層會(huì)接收該通知并用它生成可視化搓擦條。
再次運(yùn)行該應(yīng)用程序。現(xiàn)在當(dāng)我們點(diǎn)擊Show按鈕時(shí)會(huì)看到黑色的條被一串縮略圖所替代,縮略圖對(duì)應(yīng)于視頻文件中的不同時(shí)間點(diǎn)。點(diǎn)擊一張圖片會(huì)調(diào)用我們之前實(shí)現(xiàn)的委托的jumpedToTime:方法。注意AVAssetlmageGenerator可為本地資源和遠(yuǎn)程資源生成圖片,不過(guò)可以預(yù)料的是,當(dāng)為遠(yuǎn)程資源生成圖片會(huì)消耗比較長(zhǎng)的時(shí)間。這種情況下可以使用效率更好的方法以提升用戶體驗(yàn),比如為每個(gè)返回的圖片創(chuàng)建可視化布局,或?qū)⑵鋵?xiě)入圖片緩存并讓視圖定期輪詢緩存。
注意:
大部分播放用例都可以在iOs模擬器中進(jìn)行測(cè)試。不過(guò)在實(shí)際設(shè)備.上測(cè)試時(shí)性能表現(xiàn)更好。
4.7顯示字幕
使應(yīng)用程序被盡可能多的用戶接受是一件非常重要的事,這就意味著我們需要讓用戶可以使用本國(guó)母語(yǔ)訪問(wèn)我們的應(yīng)用程序,同時(shí)還要考慮存在聽(tīng)覺(jué)障礙或有其他輔助功能需求的用戶。視頻播放器在這一點(diǎn)上提高用戶體驗(yàn)常用的方法就是隨時(shí)提供字幕。AV Foundation在展示字幕或隱藏式字幕方面提供了可靠方法。AVPlayerLayer會(huì)自動(dòng)渲染這些元素,并且可以讓開(kāi)發(fā)者告訴應(yīng)用程序哪些元素需要渲染。完成這些操作要用到AVMediaSelectionGroup和AVMediaSelectionOption兩個(gè)類。
AVMediaSelectionOption表示AVAsset中的備用媒體呈現(xiàn)方式。一個(gè)資源可能包含備用媒體呈現(xiàn)方式,比如備用音頻、視頻或文本軌道。這些軌道可能是指定語(yǔ)言的音頻軌道、備用相機(jī)角度或此刻我們所感興趣的指定語(yǔ)言的字幕。確定存在哪些備用軌道要用到一個(gè)名為availableMediaCharacteristicsWithMediaSelectionOptions的AVAsset屬性(我之前就說(shuō)過(guò)AVFoundation團(tuán)隊(duì)喜歡這種長(zhǎng)名字)。這個(gè)屬性會(huì)返回一個(gè)包含字符串的數(shù)組,這些字符串用于表示保存在資源中可用選項(xiàng)的媒體特征。具體來(lái)說(shuō),返回?cái)?shù)組所包含的字符串值為AVMediaCharacteristicVisual(視頻)、AVMediaCharacteristicAudible(音頻)、AVMediaCharacteristicLegible (字幕或隱藏式字幕)。
請(qǐng)求可用媒體特性數(shù)據(jù)后,調(diào)用AVAsset的mediaSelectionGroupForMediaCharaceristic:方法,為其傳遞要檢索的選項(xiàng)的特定媒體特性。這個(gè)方法會(huì)返回一個(gè)AVMediaSelectionGroup,它作為一個(gè)或多個(gè)互斥的AVMediaSelectionOption實(shí)例的容器。下面看一個(gè)簡(jiǎn) 單示例:
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);
}
}
為包含一個(gè)或多個(gè)字幕的資源運(yùn)行這段代碼所生成的輸出內(nèi)容如下所示:
[AVMediaCharacteristicLegible]
Option: English
Option: Italian
option: Portuguese
Option: Russian
[AVMedi aCharacteristicAudible]
Option: English
在示例中可以看到多個(gè)字幕軌道以及一個(gè)English音頻軌道。
當(dāng)我們載入正確的AVMediaSelectionGroup并定義好需要的AVMediaSelectionOption之后,下一步就是付諸實(shí)際行動(dòng)了。通過(guò)在激活的AVPlayerltem上調(diào)用selectMediaOption:inMediaSelectionGroup:來(lái)實(shí)現(xiàn)這一功能。 比如,如果需要顯示俄文字幕,如下編碼:
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應(yīng)用程序中具體實(shí)施,在THPlayerController類中添加幾個(gè)新的方法。首先如代碼清單4-15所示進(jìn)行一些修改。
代碼清單4-15 loadMediaOptions 設(shè)置
- (void)prepareToPlay {
NSArray *keys = @[
@"tracks",
@"duration",
@"commonMetadata",
@“availableMediaCharacteristicsWithMediaSelectionOptions”//loadMediaOptions 設(shè)置
];
...
}
- (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方法中,我們希望在它的自動(dòng)載入屬性列表中加入availableMediaCharacteristicsWithMediaSelectionOptions屬性。在調(diào)用任何媒體選擇API前載入該屬性很有必要,這樣會(huì)避免主線程擁堵。當(dāng)播放器條目準(zhǔn)備播放就緒時(shí)調(diào)用loadMediaOptions方法。
loadMediaOptions方法的實(shí)現(xiàn)如代碼清單4-16所示。
代碼清單4-16 loadMediaOptions 實(shí)現(xiàn)
- (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)我們只對(duì)查找資源中的字幕選項(xiàng)感興趣,所以只定義了一個(gè)媒體特性字符串,并令它的值為AVMediaCharacteristicLegible。
(2)請(qǐng)求與已定義媒體特性對(duì)應(yīng)的AVMediaSelectionGroup。
(3)假設(shè)找到一組數(shù)據(jù)(應(yīng)用程序本地資源包含字幕、遠(yuǎn)程資源不包含),創(chuàng)建一個(gè)包含要傳遞給視圖層的用戶可呈現(xiàn)字符串的數(shù)組,做法是請(qǐng)求每個(gè)選項(xiàng)的displayName屬性。
(4)最后,設(shè)置播放欄上的字幕字符串集合,使其可在字幕選擇界面中呈現(xiàn)。在else條件中,傳遞nil,表示沒(méi)有可呈現(xiàn)的界面。
當(dāng)用戶選擇一個(gè)字幕時(shí),需要一個(gè)方法來(lái)處理該選擇,并在當(dāng)前播放器條目上激活相應(yīng)的AVMediaSelectionOption。代碼清單4-17給出了這個(gè)方法的實(shí)現(xiàn)。
代碼清單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])為資源中包含的有效選項(xiàng)檢索AVMediaSelectionGroup。
(2)循環(huán)遍歷所有組中的選項(xiàng),并找到與傳遞給subtitleSelected:方法的字幕字符串匹配的AVMediaSelectionOption。找到正確選項(xiàng)后,在播放器條目上調(diào)用selectMediaOption:inMediaSelectionGroup:方法激活它。這樣選中的字暮就會(huì)立即出現(xiàn)在AVPlayerLayer上。
(3)如果用戶在字幕選項(xiàng)列表中選擇None,則為選中媒體選項(xiàng)設(shè)置nil,以便移除展示中的字幕。
最后需要做的一件事是打開(kāi)VideoPlayer-Prefix.pch文件并將ENABLE SUBTITLES的定義由0改為1。如果當(dāng)前媒體中有可用字幕,則播放視圖會(huì)展示合適的字幕選擇界面。
再次運(yùn)行應(yīng)用程序,在播放欄右下角會(huì)看到一個(gè)新的按鈕。選中按鈕查看可用的字幕,選擇一個(gè)選項(xiàng),瞧!神奇的一幕出現(xiàn)了。
4.8 Airplay
最后一個(gè)需要討論的優(yōu)化問(wèn)題是在Video Player應(yīng)用程序中整合AirPlay功能。AirPlay是蘋(píng)果公司推出的一項(xiàng)技術(shù),旨在用無(wú)線方式將流媒體音頻和視頻內(nèi)容在Apple TV上播放,或者將純音頻內(nèi)容在多種第三方音頻系統(tǒng)中播放。如果用戶擁有Apple TV或其他音頻系統(tǒng)中的一個(gè),就會(huì)知道這個(gè)功能簡(jiǎn)直太神奇了。好消息是將這個(gè)功能整合到應(yīng)用程序非常容易實(shí)現(xiàn)。
AVPlayer有一個(gè)屬性allowsExternalPlayback,允許啟用或禁用AirPlay播放功能。該屬性的默認(rèn)值為YES,即在不做任何額外編碼的情況下,播放器應(yīng)用程序也會(huì)自動(dòng)支持AirPlay功能。雖然通常AirPlay功能 是需要的,不過(guò)如果由于某些強(qiáng)制的原因要禁用該功能,可以通過(guò)設(shè)置allowsExtermalPlayback屬性為NO來(lái)實(shí)現(xiàn)。
線路選擇功能
iOS為選擇AirPlay線路提供了一個(gè)整體的界面。具體的用戶界面和手勢(shì)動(dòng)作取決于iOS的版本。在iOS 6及早期版本中,用戶雙擊Home鍵啟動(dòng)dock,向右滑動(dòng)找到選擇AirPlay線路的界面,如圖4-6所示。

iOs 7及之后的版本提供了一種更方便的方法訪問(wèn)該界面。在屏幕底端向上滑動(dòng)打開(kāi)控制中心(Control Center),選擇AirPlay按鈕即可, 如圖4-7所示。

雖然使用整體線路選擇方法可以幫助用戶實(shí)現(xiàn)期望的功能,不過(guò)其在用戶體驗(yàn)方面做的還不夠理想。尤其是iOS 6版本中需要用戶跳出應(yīng)用程序,會(huì)中斷應(yīng)用程序的工作流。另一個(gè)重要的需要注意的是很多用戶并不知道這個(gè)整體界面,很可能完全忽略這個(gè)強(qiáng)大實(shí)用的功能。所以開(kāi)發(fā)者應(yīng)該以比較明顯的方式在應(yīng)用程序內(nèi)部提供AirPlay線路選擇界面。有趣的是,iOS并沒(méi)有AirPlay框架或API供開(kāi)發(fā)者使用,取而代之的是我們使用MediaPlayer框架中的MPVolumeView類來(lái)實(shí)現(xiàn)這個(gè)功能。
使用這個(gè)組件時(shí),需要關(guān)聯(lián)和導(dǎo)入MediaPlayer框架(<MediaPlayer/MediaPlayer.h>)并創(chuàng)建一個(gè)MPVolumeView實(shí)例,如下面代碼所示:
CGRect rect = // desired frame
MPVolumeView *volumeView = [[MPVolumeView alloc] initwithFrame:rect];
[self.view addSubview:volumeView];
默認(rèn)的MPVolumeView實(shí)例提供兩個(gè)用戶界面元素。顧名思義,其中一個(gè)元素是控制系統(tǒng)音量的滑動(dòng)條。它所提供的功能等同于iOS設(shè)備側(cè)面的硬音量調(diào)節(jié)按鈕(硬件)。如果用戶網(wǎng)絡(luò)中存在可用的AirPlay設(shè)備,則會(huì)額外顯示一個(gè) AirPlay路線選擇按鈕。點(diǎn)擊按鈕會(huì)顯示所有可用AirPlay線路的列表。
如果只需要展示線路選擇按鈕,可以對(duì)應(yīng)用程序做如下修改。
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
volumeView.showsVolumeSlider = NO;
[volumeView sizeToFit];
[transportView addsubview:volumeView];
有一點(diǎn)需要明確,只有當(dāng)用戶具有可用AirPlay目標(biāo)而且WiFi網(wǎng)絡(luò)啟用時(shí)才會(huì)顯示線路選擇按鈕。這兩個(gè)條件只要有一個(gè)不滿足,MPVolumeView就 會(huì)自動(dòng)隱藏按鈕。
注意:
MPVolumeView只有在iOS設(shè)備上才可以顯示,iOS 模擬器是不可以顯示的。
我們不準(zhǔn)備對(duì)實(shí)現(xiàn)過(guò)程進(jìn)行詳細(xì)講解,因?yàn)榕c同前面的示例一樣簡(jiǎn)單。如果可能的話應(yīng)用程序已經(jīng)創(chuàng)建好了線路選擇按鈕,不過(guò)我們還是需要打開(kāi)VideoPlayer Prefix.pch,將
NABLE AIRPLAY定義由0修改為1。如果你有一個(gè)Apple TV或一個(gè)支持AirPlay的音頻系統(tǒng),當(dāng)運(yùn)行應(yīng)用程序時(shí)就會(huì)看到AirPlay線路選擇按鈕了。
要了解更多關(guān)于AirPlay及其用法的高級(jí)技術(shù),可到Apple Developer Center中 查找AirPlay Overview文檔。
4.9 小結(jié)
本章深入探討了AV Foundation的視頻播放功能?,F(xiàn)在我們知道了如何通過(guò)AVPlayer播放AVPlayerItem實(shí)例,并直接將視頻輸出為AVPlayerLayer實(shí)例。我們還第一次接觸了AVAsetlmageGenerator,用它來(lái)創(chuàng)建播放器的可視化搓擦條。開(kāi)發(fā)者會(huì)發(fā)現(xiàn)這是在AVFoundation中的不同場(chǎng)景都有用的類。最后我們通過(guò)整合AirPlay功能提高視頻播放的體驗(yàn),并使用AVMediaSelectionGroup和AVMediaSelectionOption來(lái)展示字幕。本章構(gòu)建的示例應(yīng)用程序?qū)﹂_(kāi)發(fā)者編寫(xiě)任何視頻播放解決方案都是一個(gè)好起點(diǎn)。