4.1 播放功能綜述
當開發(fā)一個自定義播放器時會用到大量的對象吃既。本節(jié)從一個較高層級的介紹入手毡鉴,通過探究其所扮演的角色和所含類之間的關系來學習AV Foundation的播放功能。后面還會繼續(xù)深入分析具體的API,并通過實際開發(fā)一個 自定義視頻播放器來實際使用這些類涨享。圖4-1 概要顯示了用到的類及其關系缸废。
4.1.1 AVPlayer
AV Foundation的播放都圍繞AVPlayer類展開,AVPlayer是一個用來播放基于時間的視聽媒體的控制器對象咱扣。支持播放從本地绽淘、分步下載或通過HTTP Live Streaming協(xié)議得到的流媒體,并在多種播放場景中播放這些視頻資源闹伪。需要說明的是沪铭,當我們說“控制器”時,是指我們通常的理解偏瓤,它不是一個視圖或窗口控制器杀怠,而是一個對播放和資源時間相關信息進行管理的對象。開發(fā)者通過框架提供的應用程序接口來開發(fā)控制播放基于時間的媒體的用戶界面厅克。
AVPlayer是一個不可見組件赔退。如果播放MP3或AAC音頻文件,那么沒有可視化的用戶界面也不會有什么問題。不過如要播放一個QuickTime電影或一個MPEG-4視頻证舟,會導致非常不好的用戶體驗硕旗。要將視頻資源導出到用戶界面的目標位置,需要使用AVPlayer類女责。
注意:
AVPlayer只管理一個單 獨資源的播放,不過框架還提供了AVPlayer的一個子類AVQueue-Player漆枚,可以用來管理一個資源隊列。當你需要在一個序列中播放多個條目或者為音頻抵知、視頻資源設置播放循環(huán)時可使用該子類墙基。
4.1.2 AVPlayerLayer
AVPlayerLayer構建于Core Animation之上软族,是AV Foundation中能找到的為數(shù)不多的可見組件。Core Animation是Mac和iOS平臺上負責圖形渲染與動畫的基礎框架残制,主要用于這些平臺資源的美化和動畫流暢度提升立砸。Core Animation本身具有基于時間的屬性,并且由于它基于OpenGL痘拆,所以具有很好的性能仰禽,能非常好地滿足AV Foundation的各種需要。
AVPlayerLayer擴展了Core Animation的CALayer類纺蛆,并通過框架在屏幕上顯示視頻內(nèi)容吐葵。這一圖層并不提供任何可視化控件或其他附件(根據(jù)開發(fā)者需求搭建的),但是它用作視頻內(nèi)容的渲染面桥氏。創(chuàng)建AVPlayerLayer需要一個指向AVPlayer實例的指針温峭, 這就將圖層和播放器緊密綁定在一起,保證了當播放器基于時間的方法出現(xiàn)時使二者保持同步字支。AVPlayerLayer與其他CALayer樣凤藏, 可以設置為UIView或NSView的備用層,或者可以手動添加到一個已有的層繼承關系中堕伪。
AVPlayerLayer是一個相對簡單的類揖庄,使用起來也簡單。在這一層中開發(fā)者可以自定義的領域只有video gravity欠雌√闵遥總共可為videoGravity屬性定義三個不同的gravity值,用來確定在承載層的范圍內(nèi)視頻可以拉伸或縮放的程度富俄。圖4-2禁炒、 圖4-3和圖4 4給出了一個16:9的視頻置于4:3矩形范圍內(nèi)的情況,使我們可以看到不同gravity值霍比。
4.1.3 AVPlayerltem
我們最終的目的是使用AVPlayer來播放AVAsset幕袱。如果查看AVAsset文檔,可以找到一些用來獲取數(shù)據(jù)的方法和屬性悠瞬,比如創(chuàng)建日期们豌、元數(shù)據(jù)和時長等信息。不過無法查到如何獲取當前時間的方法浅妆,也沒有在媒體中查找特定位置的方法玛痊。這是因為AVAsset模型只包含媒體資源的靜態(tài)信息,這些不變的屬性用來描述對象的靜態(tài)狀態(tài)狂打。這就意味著僅使用AVAsset對象是無法實現(xiàn)播放功能的。當我們需要對一個資源及其相關曲目進行播放時混弥,首先需要通過AVPlayerltem和AVPlayerltemTrack類構建相應的動態(tài)內(nèi)容趴乡。
AVPlayerltem會建立媒體資源動態(tài)視角的數(shù)據(jù)模型并保存AVPlayer在播放資源時的呈現(xiàn)狀態(tài)对省。在這個類中我們會看到諸如IseekToTime:的方法以及訪問currentTime和presentationSize的屬性。AVPlayerltem由一個或多 個媒體曲目組成晾捏,由AVPlayerItemTrack類建立模型蒿涎。AVPlayerItemTrack實例用于表示播放器條目中的類型統(tǒng)一的媒體流,比如音頻或視頻惦辛。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];
}
該示例中對播放視頻文件所需的基礎架構進行了設置玻淑。不過在實際播放視頻內(nèi)容前還需要一個額外步驟,這是因為播放器的播放控件還沒有為播放動作做好準備呀伙。AVPlayerltem沒有準備播放的界面补履,不過取而代之的是基于“主動發(fā)起請求”("don’tcall me, I'll call you")的機制。
AVPlayertem具有一個名為status的AVPlayerltemStatus類型的屬性剿另。在對象創(chuàng)建之初箫锤,播放條目由AVPlayertemStatusUnknown狀態(tài)開始,該狀態(tài)表示當前媒體還未載入并且還不在播放隊列中雨女。將AVPlayerItem與一個AVPlayer對象 進行關聯(lián)就開始將媒體放入隊列中谚攒,但是在具體內(nèi)容可以播放前,需要等待對象的狀態(tài)由AVPlayerltemStatusSUnknown變?yōu)锳VPlayerftemStatusReadyToPlay氛堕。開發(fā)者可通過Key-Value Observing (KVO)機制監(jiān)視status屬性的值來跟蹤這一變化過程馏臭。
KVO是由Foundation框架提供的Observer模式的由蘋果公司給出的解決方案〔砝蓿可以讓開發(fā)者注冊一個對象作 為其他對象狀態(tài)的觀察者位喂。當被觀察的對象狀態(tài)發(fā)生變化時,觀察對象就會得到通知并采取相應的動作乱灵。在將AVPlayerItem 與AVPlayer關聯(lián)之前塑崖,開發(fā)者需要將代碼設置為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變?yōu)锳VPlayerltemStatusReady ToPlay時规婆,就可以開始播放了。
4.3 處理時間
AVPlayer和AVPlayerltem都是基于時間的對象蝉稳,但是在我們使用它們的功能前抒蚜,需要了解在AV Foundation框架中呈現(xiàn)時間的方式。
人們傾向于用日子耘戚、小時嗡髓、分鐘和秒的方式表示時間。開發(fā)人員經(jīng)常將時間進一步精確到亳秒和納秒收津。所以用一個雙精度浮點型數(shù)據(jù)表示時間也合情合理饿这。實際上浊伙,回顧第2章中介紹的AVAudioPlayer滔驾,可以看到時間是以NSTimeInterval表示的隙袁,其實就是簡單地對double值進行了typedef定義竹捉。不過使用浮點型數(shù)據(jù)類型表示時間存在一定問題速种, 因為浮點型數(shù)據(jù)的運算會導致不精確的情況臣咖。當進行多時間計算累加時這些不精確的情況就會特別嚴重吠昭,經(jīng)常導致時間的明顯偏移饶套,使得媒體的多個數(shù)據(jù)流幾乎無法實現(xiàn)同步鞋喇。此外肌割,以浮點型數(shù)據(jù)呈現(xiàn)時間信息無法做到自我描述卧蜓,這就導致在使用不同時間軸進行比較和運算時比較困難。AV Foundation使用一種可靠性更高的方法來展示時間信息声功,這就是基于CMTime數(shù)據(jù)結構烦却。
CMTime
AV Foundation是基于Core Media的高層封裝。Core Media是基于C的底層框架先巴,提供了許多處理Mac和iOS媒體棧的關鍵功能其爵。雖然這個框架通常都在后臺工作,不過其中一個我們經(jīng)常能夠接觸到的部分就是它的數(shù)據(jù)結構CMTime伸蚯。CMTime 為時間的正確表示給出了一種結構摩渺,即分數(shù)值的方式。具體定義如下:
typedef struct {
CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;
這個結構最關鍵的兩個值是value和timescale剂邮。value是一個64位整數(shù)值摇幻,timescale是一個32位整數(shù)值,在時間呈現(xiàn)樣式中分別作為分子和分母挥萌。
建立以分數(shù)的格式處理時間數(shù)據(jù)的思維方式可能開始不太習慣绰姻,不過當開發(fā)者多使用幾 次這種方式之后就會慢慢習慣。下面看幾個示例引瀑,了解如何使用CMTimeMake函數(shù)創(chuàng)建時間狂芋。
// 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頭文件還定義了大量實用的函數(shù)用于簡化時間的處理憨栽。與大部分蘋果公司的底層C框架一樣帜矾,最好的參考資料就是頭文件,所以這里建議大家仔細閱讀CMTime.h頭文件屑柔,了解其中定義的函數(shù)的功能屡萤。
4.4 創(chuàng)建視頻播放器
本節(jié)通過創(chuàng)建一個iOS 視頻播放器(如圖45所示)來深入學習AV Foundation播放API的細節(jié)。應用程序能播放本地和遠程媒體掸宛,支持播放死陆、暫停和拖動媒體時間軸。完成基本功能后唧瘾,還需要對應用程序進一步優(yōu)化以改善用戶體驗措译。 可以在Chapter 4目錄中找到名為VideoPlayer_Starter的示例項目迫像。
4.4.1 創(chuàng)建視頻視圖
第一步需要創(chuàng)建一個用來在屏 幕上展示視頻內(nèi)容的視圖。在示例項目中的THVideoPlayer/Views文件組下面瞳遍,可以找到一個名為THPlayerView的類。這個類就是用來展示視頻內(nèi)容并為操作視頻播放提供用戶界面的類菌羽。下面看一下這個類的接口掠械,如代碼清單4-1所示。
代碼清單4-1 THPlayerView 接口
#import "THTransport.h"
@class AVPlayer;
@interface THPlayerView : UIView
- (id)initWithPlayer:(AVPlayer *)player;
@property (nonatomic, readonly) id <THTransport> transport;
@end
這是一個僅帶有幾個方法的簡單類注祖。通過調(diào)用其initWithPlayer:初始化方法猾蒂,并傳遞當前AVPlayer實例的引用進行實例化。這樣就可以將播放器輸出的視頻直接展示在這個視圖中是晨,只讀屬性transport負責管理展示在視圖中的可視化控件肚菠。在講解應用程序播放控制器類的實現(xiàn)時會看到它是如何工作的。下 面我們來看這個類的具體實現(xiàn)罩缴。
視圖本身并不是視頻輸出的目標蚊逢,相反,開發(fā)者需要將播放器輸出指向一個AVPlayerLayer實例箫章±雍桑可以手動創(chuàng)建層,并將它添加到視圖的層繼承關系中檬寂,但是在iOS平臺下有一種更便捷的方法终抽。UIView視圖都受Core Animation層的支持,默認情況下桶至,就是CALayer的通用實例昼伴,不過你可以通過在UIView中重寫layerClass方法自定義支持層的類型,以便在實例化一個視圖的時候返回一個要使用的自定義CALayer镣屹。在使用AVPlayerL ayer對象時上述方法更加方便圃郊,因為不需要手動創(chuàng)建和操作層以及層繼承關系。代碼清單4-2給出了THPlayerView類的實現(xiàn)野瘦。
代碼清單4-2 THPlayerView 實現(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)建一個類擴展來定義一個私有屬性用于保存指向THOverlayView視圖實例的指針描沟。這個類提供用戶界面中操作視頻播放的控件。
(2)重寫layerClass類 方法返回一個AVPlayerLayer類鞭光。 每當創(chuàng)建THPlayerView實例時吏廉,就會使用AVPlayerLayer作為它的支持層。
(3)創(chuàng)建時沒有給出默認的尺寸大小惰许,所以開發(fā)者需要調(diào)用帶有zero-sized框架的超類初始化方法席覆。展示視圖的視圖控制器負責設置合適的框架。
(4)這是該類中最關鍵的一行代碼汹买。 我們希望獲得傳入初始化方法的AVPlayer實例并在AVPlayerLayer上對其進行設置佩伤。這一步將從AVPlayer輸出的視頻指向AVPlayerL ayer實例聊倔。
(5)在NIB中定義覆蓋視圖,通過調(diào)用loadNibNamed:owner:options方法創(chuàng)建視圖實例生巡。當視圖創(chuàng)建完成并賦給overlayView屬性后耙蔑,將其作為子視圖進行添加。
完成THPlayerView的實現(xiàn)后孤荣,下面將注意力轉移到THPlayerController類甸陌。
4.4.2 創(chuàng)建視頻控制器
在項目的THVideoPlayer/Controllers組下面,可找到THPlayerController類的具體實現(xiàn)代碼。這個類為應用程序完成了很多功能盐股,也是我們處理核心播放API方法的地方钱豁。代碼清單4-3給出了這個類的接口。
代碼清單4-3 THPlayerController 接口
@interface THPlayerController : NSObject
- (id)initWithURL:(NSURL *)assetURL;
@property (strong, nonatomic, readonly) UIView *view;
@end
創(chuàng)建一個THPlayerController實例時疯汁,需要調(diào)用其initWithURL:方法牲尺,并傳遞需要播放的媒體的NSURL。AVPlayer可用來播放本地或流媒體幌蚊,所以這個URL可以是本地文件URL谤碳,也可以是遠程HTTP URL。該類還為相關視圖提供了一個只讀屬性霹肝,以便客戶端UIViewController可將視圖添加到視圖繼承關系中估蹄。返回的視圖是一個THPlayerView實例,不過由于這些細節(jié) 需要對客戶端隱藏沫换,所以返回一個通用UIView即可臭蚁。
轉過來看類的具體實現(xiàn),首先創(chuàng)建一個類擴展來定義控制器的內(nèi)部屬性(如代碼清單4-4所示)讯赏。
代碼清單4-4 THPlayerController 類擴展
#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
在這個類的實現(xiàn)中垮兑,首先創(chuàng)建了一個類擴展來定義對象需要的存儲屬性。注意該擴展遵循THTransportDelegate協(xié)議并定義了一個transport屬性漱挎。該類和THOverlayView之間會有很多交互操作系枪,用來定義管理視頻播放的用戶界面。雖然這些類需要溝通磕谅,不過它們不必直接了解彼此私爷。要斷開這個關聯(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é)議衬浑,它可以為與覆蓋視圖進行通信提供正式接口。當播放欄(transport)發(fā)生變化時放刨,比如用戶改變時間軸位置或點擊Play/Pause按鈕工秩,控制器對象會執(zhí)行相應的委托回調(diào)。稍后將看到具體的實現(xiàn)過程,代碼清單4-6給出了THPlayerController的實現(xiàn)助币。
代碼清單4-6 THPlayerController 實現(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傳遞給初始化方法來創(chuàng)建一個AVAsset浪听。資源創(chuàng)建完成后,調(diào)用控制器的prepareToPlay方法來設置播放該資源所需的基礎結構眉菱。
(2)框架會自動載入資源的tracks屬性迹栓,省去了通過AVAsynchronousKeyValue oading協(xié)議手動載入該屬性的過程。不過在以前俭缓,開發(fā)者仍然需要執(zhí)行l(wèi)oadValuesAsynchronouslyForKeys:completionHandler:方法來載入需要訪問的其他資源屬性迈螟。iOS 7和Mac OS 10.9在AVPlayertem的處理上有了大幅改進,通過使用新的初始化方法initWithAsset:automaticallyLoadedAssetKeys:或playerItemWithAsset:automaticallyLoadedAssetKeys:創(chuàng)建一個AVPlayerltem實例尔崔,將任意屬性集的載入委托給該框架。兩種方式都將NSArray用作第二個參數(shù)褥民,包含了隨著AVPlayerItem在初始化隊列中的載入過程所需的資源鍵季春。使用這個方法自動載入tracks、duration和commonMetadata屬性消返。
(3)添加Iself作為AVPlayerltem的status屬性監(jiān)聽器载弄。回顧一下創(chuàng)建過程撵颊,播放項開始時的status狀態(tài)為AVPlayerltemStatusUnknown,播放項直到狀態(tài)變?yōu)锳VPlayertemStatusReadyToPlay才可以開始播放宇攻。對status屬性的鍵值觀察可以讓你監(jiān)聽變化。
(4)為新創(chuàng)建的AVPlayerltem對象創(chuàng)建一個 AVPlayer實例倡勇。AVPlayer會立即開始媒體隊列化的過程。
(5)最后,創(chuàng)建一個THPlayerView實例穷劈,傳遞給它一個指向AVPlayer實例的指針趁桃。開發(fā)者還需要為THPlayerController和ITHTransport設置關系。
4.4.3 監(jiān)聽狀態(tài)改變
我們已經(jīng)將THPlayerController設置為播放項的status屬性的監(jiān)聽器扔役。在對該屬性監(jiān)聽前帆喇,需要實現(xiàn)observeValueForKeyPath:ofObject:change:context方法,如代碼清單4-7所示亿胸。
代碼清單4-7監(jiān)聽 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沒有指定在哪個線程執(zhí)行status改變通知坯钦,所以在采取下一步動作前,需要通過dispatch_async確保應用程序返回到主線程侈玄,向其傳遞一個主隊列的引用婉刀。
(2)通過調(diào)用私有方法addPlayerltemTimeObserver和addItemEndObserverForPlayerItem設置播放器的時間監(jiān)聽器。下面的小節(jié)會討論這些方法和時間監(jiān)聽器拗馒。
(3)在ransport對象上設置當前時間和總長路星。將用戶界面上展示的時間與播放的媒體進行同步。transport對象無法識別CMTime,只能處理以秒為單位的NSTimelInterval類型的時間洋丐。我們使用CMTimeGetSeconds函數(shù)將CMTime值轉換為秒呈昔。Core Media定義了常量kCMTimeZero,開發(fā)者可以將它作為開頭的currentTime參數(shù)友绝,使用播放條目的duration屬性值作為第二個參數(shù)堤尾。
(4)向播放欄傳遞一個標題字符串,來展示資源的標題(如果資源的元數(shù)據(jù)中存在標題信
息)迁客。AVAsset沒有title屬性郭宝,這是我們加入AVAsset中的一個分類方法,目的是增加代碼的可讀性掷漱。這個分類方法用到了粘室,上一章介紹的元數(shù)據(jù)API,具體地講卜范,從資源的commonMetadata得到AVMetadataCommonKeyTitle值衔统。具體細節(jié)參考AVAsset+THAdditions。
(5)現(xiàn)在就準備調(diào)用AVPlayer的play方法進行播放了海雪。最后锦爵,在完成對status 關鍵路徑的監(jiān)聽后,我們希望將作為監(jiān)聽器的self移除奥裸。
現(xiàn)在可以啟動應用程序并開始播放其中一個視頻险掀。雖然視頻已經(jīng)播放,不過用戶界面上的控件還沒有提供任何功能湾宙,并且隨著時間的推移用戶界面也沒有相應的反饋信息樟氢。這就又回到了addPlayerItemTimeObserver方法上,我們需要在該方法上實現(xiàn)相關的功能侠鳄,不過在此之前我們需要先學習如何得知AVPlayer的時間變化嗡害。
4.5 時間監(jiān)聽
我們已經(jīng)討論過并了解到如何使用KVO來觀察播放條目的status屬性。KVO對于常見的狀態(tài)監(jiān)控表現(xiàn)得很出色畦攘,并且可以監(jiān)聽AVPlayerltem和AVPlayer的許多屬性霸妹。不過KVO也有不能勝任的場景,比如需要監(jiān)聽AVPlayer的時間變化知押。這些監(jiān)聽類型都是自身具有明顯的動態(tài)特性并需要非常高的精確度叹螟,這一點要比標準的鍵值監(jiān)聽要求高。為滿足這一需求台盯,AVPlayer提供了兩種基于時間的監(jiān)聽方法罢绽,讓應用程序可以對時間變化進行精準的監(jiān)聽。下面分別看一下這兩個方法静盅。
4.5.1 定期監(jiān)聽
通常情況下良价,我們希望以一定的時間間隔獲得通知寝殴。如果需要隨著時間的變化移動播放頭位置或更新時間顯示,這非常重要明垢。利用AVPlayer的addPeriodic TimeObserverForInterval:queue:usingBlock:方法可以很容易地監(jiān)聽到此類變化蚣常。這個方法需要傳遞如下參數(shù):
●interv: 一個用于指定通知周期間隔的CMTime值。
●queue: 通知發(fā)送的順序調(diào)度隊列痊银。大多數(shù)時候抵蚊,我們希望這些通知消息發(fā)生在主隊列,在如果沒有明確指定的情況下則默認為主隊列。需要重點注意的是不可以使用并行調(diào)度隊列溯革,因為API沒有處理并行隊列的方法贞绳,否則會導致一些不可 知的問題。
●block:一個在指定的時間間隔中將會在隊列上調(diào)用的回調(diào)塊致稀。這個塊傳遞一個CMTime值用于指示播放器的當前時間冈闭。
4.5.2 邊界時間監(jiān)聽
AVPlayer還提供了一種更有針對性的方法來監(jiān)聽時間,應用程序可以得到播放器時間軸中多個邊界點的遍歷結果抖单。這一方法 主要用于同步用戶界面變更或隨著視頻播放記錄一些非可視化數(shù)據(jù)拒秘。比如,可以定義25%臭猜、50%和75%邊界的標記,以此判斷用戶播放進度押蚤。要使用這個功能蔑歌,需要用到addBoundaryTimeObserverForTimes:queue:usingBlock:方法,并提供如下參數(shù):
●times: CMTime 值組成的一個NSArray數(shù)組定義了需要通知的邊界點揽碘。
●queue: 與定期監(jiān)聽類似次屠,為方法提供一個用來發(fā)送通知的順序調(diào)度隊列。指定NULL等同于明確設置主隊列雳刺。
●block: 每當正常播放中跨越一個邊界點時就會在隊列中調(diào)用這個回調(diào)塊劫灶。有趣的是,該塊不提供遍歷的CMTime值掖桦,所以開發(fā)者需要為此執(zhí)行一些額外計算進行確定本昏。
本示例應用程序沒有用到邊界時間監(jiān)聽,不過定期監(jiān)聽對應用程序的功能非常重要枪汪。下面通過addPlayerltemTimeObserver方法的實現(xiàn)看一下如何在實際中使用定期監(jiān)聽法涌穆,如代碼清單4-8所示。
代碼清單4-8定期監(jiān)聽法
- (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)首先創(chuàng)建一個用于定義通知時間間隔的CMTime值罩锐。這里將間隔定義為0.5秒奉狈,這個時間粒度足以更新播放器的時間顯示。
(2)定義發(fā)送回調(diào)通知的調(diào)度隊列唯欣。大多數(shù)情況下嘹吨,由于我們所要更新的用戶界面處于主線程,所以一般使用主隊列境氢。
(3)定義一個回調(diào)塊蟀拷,在前面定義的時間周期內(nèi)會調(diào)用該代碼塊。非常重要的一點是代碼塊要獲取self的弱引用萍聊。不這樣做會出現(xiàn)難以診斷的內(nèi)存泄漏问芬。
(4)在回調(diào)塊內(nèi)部,我們希望通過CMTimeGetSeconds函數(shù)將代碼塊的CMTime值轉換成一個NSTimeInterval寿桨。同樣此衅,還需要將播放條目的duration進行轉換。傳遞這個duration信息看起來是多余的亭螟,因為我們已經(jīng)在KVO回調(diào)中傳遞duration到transport中挡鞍,不過transport會隨著媒體的載入而改變,所以要保持用戶界面的同步预烙,最好還是傳遞最新的值墨微。
(5)最后調(diào)用addPeriodicTimeObserverForInterval:queue:usingBlock:方法并傳遞定義好的參數(shù)。調(diào)用這個方法會返回一個隱含id類型指針扁掸。對這些回調(diào)必須保持一個強引用翘县。這個指針還會用于移除監(jiān)聽器。
4.5.3 條目結束監(jiān)聽
另一常見的需要監(jiān)聽的事件就是條目播放完畢的時間谴分,雖然這不同于上面介紹的基于時間的監(jiān)聽锈麸,不過我們傾向于認為二者有著類似的原理。當播放完成時牺蹄,AVPlayerItem會發(fā)送一個AVPlayerItemDidPlayToEndTimeNotification通知忘伞。THPlayerController實例應 該注冊為該通知的監(jiān)聽器,這樣就可以采取相應的動作沙兰。代碼清單4_9給出了addItemEndObserverForPlayerItem方法的實現(xiàn)虑省。
代碼清單4-9條目結束監(jiān)聽
- (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的弱引用僧凰。與定期監(jiān)聽使用的回調(diào)塊類似探颈,如果沒有建立對self的弱引用將會導致內(nèi)存泄漏。這些基于塊的計數(shù)循環(huán)診斷起來非常難训措。
(2)當播放完畢時伪节,需要通過調(diào)用播放器實例的seekToTime:kCMTimeZero方法重新定位播放頭光標回到0位置光羞。
(3)當#2的搜索調(diào)用完成時,通知播放欄播放已經(jīng)完成了怀大,這樣就可以重新設置展示時間和搓擦條纱兑。
(4)通過注冊NSNotificationCenter來添 加itemEndObserver作為通知的監(jiān)聽器,并將定義好的參數(shù)傳遞給它。
(5)最后重寫dealloc方法化借,當控制器被釋放時移除作為監(jiān)聽器的itemEndObserver潜慎。
運行應用程序”涂担可以看到在視頻播放過程中铐炫,隨著時間的變動,當前時間和剩余時間標簽中的值不斷更新蒜焊,并且可以看到時間搓擦條相應地更新播放頭的位置倒信。
下面繼續(xù)實現(xiàn)其他委托回調(diào)方法,使播放欄控件正常工作泳梆。
4.5.4播放欄委托回調(diào)
我們先來看一下THTransportDelegate協(xié) 議提供的簡單播放欄回調(diào)的實現(xiàn)鳖悠。代碼清單4-10給出了這些方法的實現(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的實現(xiàn)不需要過多解釋优妙,因為它委托給播放器的同名方法乘综。同樣,pause方法委托播放器的pause方法套硼,不過為了條理清晰卡辰,仍獲取lastPlaybackRate。 stop方法調(diào)用setRate:并傳遞參數(shù)0熟菲,相當于調(diào)用了pause,只是采用不同方法實現(xiàn)同一效果朴恳。還對播放欄調(diào)用了playbackComplete來更新搓擦條的位置抄罕。jumpedToTime:方法 利用播放器的seekToTime:方法跳轉到時間軸上的任意位置。這個方法的使用會在本章后面看到于颖。
接下來呆贿,看一下如何實現(xiàn)搓擦條相關的方法。一共有三個方法需要實現(xiàn)森渐,分別對應著當用戶與UISlider控件交互時產(chǎn)生的三個事件做入,如代碼清單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)會調(diào)用scrubbingDidStart方法同衣。在這個方法中開發(fā)者將獲取當前播放率并暫停播放器竟块。獲取當前播放率是為了在搓擦進度結束時恢復播放。此外耐齐,還需要移除當前定期監(jiān)聽器浪秘,因為我們不希望在用戶直接控制媒體進度時觸發(fā)此類事件蒋情。
(2)當UISlider實例的值發(fā)生變化時(UIControlEventValueChanged)會調(diào)用scrubbedToTime方法。由于這個方法在用戶移動滑動條位置時會迅速觸發(fā)耸携,所以首先應該在播放條目上調(diào)用cancelPendingeeks棵癣。這是經(jīng)過性能優(yōu)化的,如果前一個搜索請求沒有完成夺衍,則避免出現(xiàn)搜索操作堆積情況的出現(xiàn)狈谊。開發(fā)者可調(diào)用seekToTime:發(fā)起一個新的搜索, 并將NSTimeInterval值轉換為CMTime沟沙。
(3)區(qū)域內(nèi)觸控事件(UIControlEventTouchUpInside)會調(diào)用scrubbingDidEnd方法河劝,用來表示用戶已經(jīng)完成了搓擦操作。在這個方法中尝胆,需要調(diào)用addPlayerltemTimeObserver重新添加定期監(jiān)聽器丧裁。之后查看lastPlaybackRate值,如果該值大于0含衔,則表示視頻已經(jīng)播放過了煎娇,需要重新播放該視頻。
通過上述過程贪染,最主要的視頻播放功能都已經(jīng)完成了!運行應用程序缓呛,現(xiàn)在可以播放、暫停和調(diào)整視頻播放進度杭隙。完成了這些核心的播放功能哟绊,下 面就需要對播放中涉及的各功能進行優(yōu)化,通過添加一些功能提高視頻播放的用戶體驗痰憎。
4.6 創(chuàng)建可視搓擦條
你可能已經(jīng)注意到了播放器右上角有一個帶 有Show標簽的按鈕票髓。如果點擊這個按鈕,會發(fā)現(xiàn)在主導航欄下面出現(xiàn)了一個黑色的欄铣耘。目前它還沒有實際的功能洽沟,不過下面看一下能否把這個地方有效利用起來。
可在AV Foundation中找到一個名為AVAssetImageGenerator的工具類蜗细。這個類可用來從一個AVAsset視頻曲目中提取圖片裆操。這樣可以生成-一個或多 個縮略圖,用來提升應用程序用戶界面的效果炉媒。
AVAssetImageGenerator定義了兩個方法實現(xiàn)從視頻資源中檢索圖片踪区,分別為:
●copyCGImageAtTime:actualTime:error:允許 在指定時間點捕捉圖片。如果開發(fā)者希望捕捉一張圖片那么這個方法是最適合的吊骤,可能用于在視頻列表中展示視頻縮略圖缎岗。
●generateCGlmagesAsynchronouslyForTimes:completionHandler: 允許按照第一個參數(shù)所指定的時間段生成一個圖片序列。該方法具有很高的性能白粉,只需要調(diào)用這一個方法就可以生成一組圖片密强。
注意:
AVAssetlmageGenerator既可以生成本地圖片茅郎,也可以生成持續(xù)下載的資源。不過它不能從HTTP Live Stream生成圖片或渤。
由此實現(xiàn)的一個優(yōu)秀功能就是創(chuàng)建可視搓擦條系冗。不同于在工具欄底部展示的標準搓擦條,這里創(chuàng)建一個可視化的搓擦條薪鹦,這樣用戶可以更簡單地在時間軸中指定位置并立即跳轉到指定位置掌敬。下面看一下如何實現(xiàn)這個功能(如代碼清單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
將導入THThumbnail.h頭文件池磁。THThumbnail類是項目中的-一個簡單模型對象奔害,用來保存我們捕捉到的圖片及其相關的時間。還需要添加一一個 AVAssetImageGenerator類型的新屬性地熄。
接下來添加一個新方法generateThumbnails并在status監(jiān)聽器回調(diào)方法中調(diào)用這個方法华临,如代碼清單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 {
}
構建基礎結構后端考,下面具體實現(xiàn)方法雅潭。代碼清單4-14給出了generateThumbnails方法的實現(xiàn)。
代碼清單4-11 generateThumbnails的實現(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)建一個新的AVAssetlmageGenerator實例却特,為其傳遞一個對控制器asset屬性的引用扶供。保持對該對象的強引用非常關鍵。如果沒有注意到這一點將遇到麻煩裂明,因為會導致無法調(diào)用回調(diào)椿浓。
(2) AVAssetImageGenerator為配置圖片生成定義了一些屬性。 雖然為大部分屬性提供了合理的默認值闽晦,不過有一個屬性在每次使用時都需要明確配置扳碍,就是maximumSize屬性。 默認情況下仙蛉,捕捉的圖片都保持原始維度笋敞。如果處理720p或1080p視頻的話,則創(chuàng)建的圖片會非常大捅儒。設置maximumSize屬性會自動對圖片的尺寸進行縮放并顯著提高性能液样。指定一個width值為200振亮、height值 為0的CGSize巧还。這樣可以確保生成的圖片都遵循一定寬度, 并且會根據(jù)視頻的寬高比自動設置高度值坊秸。
(3)下面需要做的是執(zhí)行一些計算來生成CMTime值的集合,這些值用來指定視頻中的捕捉位置麸祷。代碼中將視頻時間軸平均分成20個CMTime值。循環(huán)遍歷視頻的duration褒搔,使用CMTimeMake函數(shù)創(chuàng)建了一個新的時間阶牍,之后將結果CMTime封裝成一個NSValue保存 在times數(shù)組中喷面。
(4)基于times數(shù)組中元素的個數(shù),定義一個名為imageCount的_block變量走孽。 這用于確定所有圖片處理完成的時間惧辈。還定義一個block變量,類型為NSMutableArray磕瓷,名為images盒齿。用于保存生成圖片的集合。 _block修飾詞用來確崩常回調(diào)block操作直接發(fā)生在這些指針上而非副本上边翁。
(5)接下來定義了一個AVAssetlmageGeneratorCompletionHandler類型的回調(diào)塊。這是其中一個較長的代碼塊定義硕盹,下面看一下它的參數(shù):
●requestedTime: 請求的最初時間符匾。它對應于生成圖像的調(diào)用中指定的times數(shù)組中的值。
●imageRef: 生成的CGImageRef,如果在給定的時間點沒有生成圖片則賦值NULL.
●actualTime: 圖片實際生成的時間瘩例“〗海基于實際效率,這個值可能與請求時間不同仰剿〈吹可以在生成圖片前通過在AVAssetImageGenerator實例設置requestedTime ToleranceBefore和requestedTimeToleranceAfter 值來調(diào)整requestedTime和actualTime的接近程度。
●result: AVAssetImageGeneratorResult 用來表示圖片是成功生成南吮、失敗還是取消琳彩。
●error:一個NSError指針,如果收到AVAssetlmageGeneratorFailed的AVAssetlmageGeneratorResult,可以通過這個NSError指針診斷問題部凑。
(6)如果result值為AVAssetlmageGeneratorSucceeded,則表示圖片已經(jīng)成功生成了露乏,基于返回的CGImageRef創(chuàng)建一個新的Ullmage。接下來創(chuàng)建一個新的THThumbnail實例將圖片和時間信息打包,并添加到數(shù)組中。
(7)在回調(diào)塊的每次調(diào)用中枕磁,使imageCount屬性減1并判斷其是否等于0阱持,如果等于0則表明所有圖片都處理完成了。之后發(fā)送一個新的名為THThumbnails- GeneratedNotification的應用程序專用通知消息谱净,將圖片集合作為object參數(shù)傳遞。視圖層會接收該通知并用它生成可視化搓擦條。
再次運行該應用程序」畚希現(xiàn)在當我們點擊Show按鈕時會看到黑色的條被一串縮略圖所替代,縮略圖對應于視頻文件中的不同時間點衣洁。點擊一張圖片會調(diào)用我們之前實現(xiàn)的委托的jumpedToTime:方法墓捻。注意AVAssetlmageGenerator可為本地資源和遠程資源生成圖片,不過可以預料的是坊夫,當為遠程資源生成圖片會消耗比較長的時間砖第。這種情況下可以使用效率更好的方法以提升用戶體驗撤卢,比如為每個返回的圖片創(chuàng)建可視化布局,或將其寫入圖片緩存并讓視圖定期輪詢緩存梧兼。
注意:
大部分播放用例都可以在iOs模擬器中進行測試放吩。不過在實際設備.上測試時性能表現(xiàn)更好。
4.7顯示字幕
使應用程序被盡可能多的用戶接受是一件非常重要的事羽杰,這就意味著我們需要讓用戶可以使用本國母語訪問我們的應用程序屎慢,同時還要考慮存在聽覺障礙或有其他輔助功能需求的用戶。視頻播放器在這一點上提高用戶體驗常用的方法就是隨時提供字幕忽洛。AV Foundation在展示字幕或隱藏式字幕方面提供了可靠方法腻惠。AVPlayerLayer會自動渲染這些元素,并且可以讓開發(fā)者告訴應用程序哪些元素需要渲染欲虚。完成這些操作要用到AVMediaSelectionGroup和AVMediaSelectionOption兩個類集灌。
AVMediaSelectionOption表示AVAsset中的備用媒體呈現(xiàn)方式。一個資源可能包含備用媒體呈現(xiàn)方式复哆,比如備用音頻欣喧、視頻或文本軌道。這些軌道可能是指定語言的音頻軌道梯找、備用相機角度或此刻我們所感興趣的指定語言的字幕唆阿。確定存在哪些備用軌道要用到一個名為availableMediaCharacteristicsWithMediaSelectionOptions的AVAsset屬性(我之前就說過AVFoundation團隊喜歡這種長名字)。這個屬性會返回一個包含字符串的數(shù)組锈锤,這些字符串用于表示保存在資源中可用選項的媒體特征驯鳖。具體來說,返回數(shù)組所包含的字符串值為AVMediaCharacteristicVisual(視頻)久免、AVMediaCharacteristicAudible(音頻)浅辙、AVMediaCharacteristicLegible (字幕或隱藏式字幕)。
請求可用媒體特性數(shù)據(jù)后,調(diào)用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);
}
}
為包含一個或多個字幕的資源運行這段代碼所生成的輸出內(nèi)容如下所示:
[AVMediaCharacteristicLegible]
Option: English
Option: Italian
option: Portuguese
Option: Russian
[AVMedi aCharacteristicAudible]
Option: English
在示例中可以看到多個字幕軌道以及一個English音頻軌道泽腮。
當我們載入正確的AVMediaSelectionGroup并定義好需要的AVMediaSelectionOption之后,下一步就是付諸實際行動了衣赶。通過在激活的AVPlayerltem上調(diào)用selectMediaOption:inMediaSelectionGroup:來實現(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應用程序中具體實施豪筝,在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屬性。在調(diào)用任何媒體選擇API前載入該屬性很有必要团搞,這樣會避免主線程擁堵严望。當播放器條目準備播放就緒時調(diào)用loadMediaOptions方法。
loadMediaOptions方法的實現(xiàn)如代碼清單4-16所示逻恐。
代碼清單4-16 loadMediaOptions 實現(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)我們只對查找資源中的字幕選項感興趣像吻,所以只定義了一個媒體特性字符串,并令它的值為AVMediaCharacteristicLegible复隆。
(2)請求與已定義媒體特性對應的AVMediaSelectionGroup拨匆。
(3)假設找到一組數(shù)據(jù)(應用程序本地資源包含字幕、遠程資源不包含)挽拂,創(chuàng)建一個包含要傳遞給視圖層的用戶可呈現(xiàn)字符串的數(shù)組惭每,做法是請求每個選項的displayName屬性。
(4)最后亏栈,設置播放欄上的字幕字符串集合台腥,使其可在字幕選擇界面中呈現(xiàn)。在else條件中绒北,傳遞nil黎侈,表示沒有可呈現(xiàn)的界面。
當用戶選擇一個字幕時闷游,需要一個方法來處理該選擇峻汉,并在當前播放器條目上激活相應的AVMediaSelectionOption。代碼清單4-17給出了這個方法的實現(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])為資源中包含的有效選項檢索AVMediaSelectionGroup俱济。
(2)循環(huán)遍歷所有組中的選項,并找到與傳遞給subtitleSelected:方法的字幕字符串匹配的AVMediaSelectionOption钙勃。找到正確選項后蛛碌,在播放器條目上調(diào)用selectMediaOption:inMediaSelectionGroup:方法激活它。這樣選中的字暮就會立即出現(xiàn)在AVPlayerLayer上辖源。
(3)如果用戶在字幕選項列表中選擇None蔚携,則為選中媒體選項設置nil,以便移除展示中的字幕克饶。
最后需要做的一件事是打開VideoPlayer-Prefix.pch文件并將ENABLE SUBTITLES的定義由0改為1酝蜒。如果當前媒體中有可用字幕,則播放視圖會展示合適的字幕選擇界面矾湃。
再次運行應用程序亡脑,在播放欄右下角會看到一個新的按鈕。選中按鈕查看可用的字幕,選擇一個選項霉咨,瞧!神奇的一幕出現(xiàn)了蛙紫。
4.8 Airplay
最后一個需要討論的優(yōu)化問題是在Video Player應用程序中整合AirPlay功能。AirPlay是蘋果公司推出的一項技術途戒,旨在用無線方式將流媒體音頻和視頻內(nèi)容在Apple TV上播放坑傅,或者將純音頻內(nèi)容在多種第三方音頻系統(tǒng)中播放。如果用戶擁有Apple TV或其他音頻系統(tǒng)中的一個喷斋,就會知道這個功能簡直太神奇了唁毒。好消息是將這個功能整合到應用程序非常容易實現(xiàn)。
AVPlayer有一個屬性allowsExternalPlayback,允許啟用或禁用AirPlay播放功能星爪。該屬性的默認值為YES浆西,即在不做任何額外編碼的情況下,播放器應用程序也會自動支持AirPlay功能顽腾。雖然通常AirPlay功能 是需要的室谚,不過如果由于某些強制的原因要禁用該功能,可以通過設置allowsExtermalPlayback屬性為NO來實現(xiàn)崔泵。
線路選擇功能
iOS為選擇AirPlay線路提供了一個整體的界面秒赤。具體的用戶界面和手勢動作取決于iOS的版本。在iOS 6及早期版本中憎瘸,用戶雙擊Home鍵啟動dock入篮,向右滑動找到選擇AirPlay線路的界面,如圖4-6所示幌甘。
iOs 7及之后的版本提供了一種更方便的方法訪問該界面潮售。在屏幕底端向上滑動打開控制中心(Control Center),選擇AirPlay按鈕即可锅风, 如圖4-7所示酥诽。
雖然使用整體線路選擇方法可以幫助用戶實現(xiàn)期望的功能,不過其在用戶體驗方面做的還不夠理想皱埠。尤其是iOS 6版本中需要用戶跳出應用程序肮帐,會中斷應用程序的工作流。另一個重要的需要注意的是很多用戶并不知道這個整體界面边器,很可能完全忽略這個強大實用的功能训枢。所以開發(fā)者應該以比較明顯的方式在應用程序內(nèi)部提供AirPlay線路選擇界面。有趣的是忘巧,iOS并沒有AirPlay框架或API供開發(fā)者使用恒界,取而代之的是我們使用MediaPlayer框架中的MPVolumeView類來實現(xiàn)這個功能。
使用這個組件時砚嘴,需要關聯(lián)和導入MediaPlayer框架(<MediaPlayer/MediaPlayer.h>)并創(chuàng)建一個MPVolumeView實例十酣,如下面代碼所示:
CGRect rect = // desired frame
MPVolumeView *volumeView = [[MPVolumeView alloc] initwithFrame:rect];
[self.view addSubview:volumeView];
默認的MPVolumeView實例提供兩個用戶界面元素涩拙。顧名思義,其中一個元素是控制系統(tǒng)音量的滑動條耸采。它所提供的功能等同于iOS設備側面的硬音量調(diào)節(jié)按鈕(硬件)兴泥。如果用戶網(wǎng)絡中存在可用的AirPlay設備,則會額外顯示一個 AirPlay路線選擇按鈕洋幻。點擊按鈕會顯示所有可用AirPlay線路的列表。
如果只需要展示線路選擇按鈕翅娶,可以對應用程序做如下修改文留。
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
volumeView.showsVolumeSlider = NO;
[volumeView sizeToFit];
[transportView addsubview:volumeView];
有一點需要明確,只有當用戶具有可用AirPlay目標而且WiFi網(wǎng)絡啟用時才會顯示線路選擇按鈕竭沫。這兩個條件只要有一個不滿足燥翅,MPVolumeView就 會自動隱藏按鈕。
注意:
MPVolumeView只有在iOS設備上才可以顯示蜕提,iOS 模擬器是不可以顯示的森书。
我們不準備對實現(xiàn)過程進行詳細講解,因為與同前面的示例一樣簡單谎势。如果可能的話應用程序已經(jīng)創(chuàng)建好了線路選擇按鈕凛膏,不過我們還是需要打開VideoPlayer Prefix.pch,將
NABLE AIRPLAY定義由0修改為1脏榆。如果你有一個Apple TV或一個支持AirPlay的音頻系統(tǒng)猖毫,當運行應用程序時就會看到AirPlay線路選擇按鈕了。
要了解更多關于AirPlay及其用法的高級技術须喂,可到Apple Developer Center中 查找AirPlay Overview文檔吁断。
4.9 小結
本章深入探討了AV Foundation的視頻播放功能。現(xiàn)在我們知道了如何通過AVPlayer播放AVPlayerItem實例坞生,并直接將視頻輸出為AVPlayerLayer實例仔役。我們還第一次接觸了AVAsetlmageGenerator,用它來創(chuàng)建播放器的可視化搓擦條是己。開發(fā)者會發(fā)現(xiàn)這是在AVFoundation中的不同場景都有用的類又兵。最后我們通過整合AirPlay功能提高視頻播放的體驗,并使用AVMediaSelectionGroup和AVMediaSelectionOption來展示字幕卒废。本章構建的示例應用程序對開發(fā)者編寫任何視頻播放解決方案都是一個好起點寒波。