視頻播放
播放視頻是AVFoundation的最核心功能之一,這里我們將學習創(chuàng)建一個自定義視頻播放器.
1. 先了解關于播放功能的類
-
AVPlayer:
AVPlayer
是一個用來播放 基于時間的視聽媒體 的管理對象.支持播放 本地,分布下載以及HTTP Live Streaming 協(xié)議等得到的流媒體.
AVPlayer
只管理一個單獨資源的播放.如果你需要播放多個或循環(huán)播放時可以使用它的子類AVQueuePlayer
.
-
AVPlayerLayer:
由于AVPlayer
是一個不可見組件,所以如果要將視頻資源導出到用戶界面的目標位置,我們需要使用AVPlayerLayer
.
AVPlayerLayer
是構建于Core Animation之上的可視化組件,是擴展了的CALayer
類,作為視頻內(nèi)容的渲染面來在屏幕上顯示視頻.
因此,創(chuàng)建AVPlayerLayer
需要一個指向AVPlayer
對象的指針,來將圖層和播放器緊密綁定在一起,從而保證當播放器基于時間的方法出現(xiàn)時二者能保持同步.
AVPlayerLayer使用比較簡單,所以供開發(fā)者自定義的地方也少.只有一個videoGravity屬性.它定義了三個不同的gravity值,用來確定在承載層的范圍內(nèi) 視頻可以拉伸或縮放的 程度.
-
AVPlayerItem:
我們最終目的是使用AVPLayer
來播放AVAsset
. 但是AVAsset
只包含媒體資源的不可變的靜態(tài)信息.我們無法獲取播放的當前時間等動態(tài)信息.所以引入AVPlayerItem
和AVPlayerItemTrack
來構建相應的動態(tài)內(nèi)容.
AVPlayerItem
會建立媒體資源動態(tài)視角的數(shù)據(jù)模型 并保存AVPlayer在播放資源時的呈現(xiàn)狀態(tài).
AVPlayerItem由一個或多個媒體曲目組成,跟AVAsset中AVAssetTrack對應. AVPlayerItemTrack實例則用于表示播放器條目中的類型統(tǒng)一的媒體流.比如音頻流, 視頻流.
// 應用流程
//1. 創(chuàng)建AVAsset
AVAsset *asset = [AVAsset assetWithURL:assetURL];
//2. 創(chuàng)建對應播放元素
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
//3. 創(chuàng)建播放器
AVPlayer *player = [AVPlayer playerWithPlayerItem: playerItem];
//4. 創(chuàng)建視頻顯示的圖層.
AVPlayerLayer * playerLayer = [AVPlayerLayer playerLayerWithPlayer: player];
// 最后添加圖層到view上
當然,上面只是基本步驟,真正應用時還要看當前媒體是否已經(jīng)加入到了播放隊列中,播放器是否準備完全. 可以使用KVO監(jiān)聽AVPlayerItem的status屬性得知.
-
補充:CMTime
AVPlayer和AVPlayerItem都是基于時間的對象. AVFoundation使用CMTime數(shù)據(jù)結構為時間表示. 它屬于基于C的底層CoreMedia框架.使用更精確的分數(shù)格式來處理時間數(shù)據(jù).
typedef struct {
CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;
// 創(chuàng)建時間例子
CMTime halfSecond = CMTimeMake(1,2); // 0.5s
CMTime fiveSeconds = CMTimeMake(5,1); // 5s
CMTime time = CMTimeMake(1,44100); // 44.1赫茲
2. 視頻播放器
2.1 創(chuàng)建視頻視圖UIView.
由上面的類介紹可知,我們要創(chuàng)建視頻視圖就是使用了AVPlayerLayer圖層的UIView. 我們既可以手動創(chuàng)建layer并添加到UIView的原有圖層,也可以重寫layerClass
方法自定義view的圖層類型來使用. 推薦第二種方法, 這樣就不用考慮圖層級關系了.
// THPlayerView.m
+ (Class)layerClass {
return [AVPlayerLayer class];
}
- (id)initWithPlayer:(AVPlayer *)player {
if (self = [super initWithFrame:CGRectZero]) {
self.backgroundColor = [UIColor blackColor];
self.autoresizingMask = UIViewAutoresizingFlexibleHeight |
UIViewAutoresizingFlexibleWidth;
// 使之成為AVPlayer實例視頻輸出的圖層
[(AVPlayerLayer *) [self layer] setPlayer:player];
// THOverlayView-操作視頻交互界面.
[[NSBundle mainBundle] loadNibNamed:@"THOverlayView"
owner:self
options:nil];
[self addSubview:_overlayView];
}
return self;
}
2.2 創(chuàng)建視頻控制器
static const NSString *PlayerItemStatusContext;
- (id)initWithURL:(NSURL *)assetURL {
if (self = [super init]) {
// 加載本地或遠端視頻資源
_asset = [AVAsset assetWithURL:assetURL];
[self prepareToPlay];
}
return self;
}
- (void)prepareToPlay {
NSArray *keys = @[
@"tracks",
@"duration",
@"commonMetadata",
@"availableMediaCharacteristicsWithMediaSelectionOptions"
];
// iOS 7之后可自動載入資源屬性.不需要再調用loadValuesAsynchronouslyForKeys
self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset
automaticallyLoadedAssetKeys:keys];
// 監(jiān)聽 playerItem 狀態(tài)是否準備完全.
[self.playerItem addObserver:self
forKeyPath:STATUS_KEYPATH
options:0
context:&PlayerItemStatusContext];
// 創(chuàng)建視頻視圖
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
self.playerView = [[THPlayerView alloc] initWithPlayer:self.player];
}
// KVO實現(xiàn)- 監(jiān)聽狀態(tài)改變
- (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) {
/*
// 此部分設置播放器的時間監(jiān)聽. 下面小結會介紹這個功能
[self addPlayerItemTimeObserver];
[self addItemEndObserverForPlayerItem];
// transport 是自定義的關聯(lián)屬性,用來溝通交互界面和實際視頻播放狀態(tài)信息.
CMTime duration = self.playerItem.duration;
// 設置當前時間和視頻總長.將用戶界面的時間和播放媒體進行同步 .
[self.transport setCurrentTime:CMTimeGetSeconds(kCMTimeZero)
duration:CMTimeGetSeconds(duration)];
// 傳遞視頻標題, AVAsset沒有title屬性,這是分類添加的,增加可讀性.
[self.transport setTitle:self.asset.title];
*/
// 播放
[self.player play];
} else {
[UIAlertView showAlertWithTitle:@"Error"
message:@"Failed to load video"];
}
});
}
}
通過上面代碼視頻資源就可以播放了.但是還需要提供用戶界面的控制功能,反饋信息.
2.3 時間監(jiān)聽
上面使用了KVO來監(jiān)聽播放條目的status屬性,KVO確實可以監(jiān)聽AVPlayerItem和AVPlayer很多屬性,但是對于AVPlayer的時間變化這種明顯動態(tài)特性和非常高的精確度要求,KVO難以勝任.
對此AVPlayer提供了兩種基于時間方法進行監(jiān)聽:
- 定期監(jiān)聽:
一定時間間隔獲得通知(如更新時間變化,進度條的移動).
- (void)addPlayerItemTimeObserver {
// 創(chuàng)建0.5s的刷新間隔
CMTime interval =
CMTimeMakeWithSeconds(0.5f, NSEC_PER_SEC);
// 一般主線程更新UI,所以用主隊列.
dispatch_queue_t queue = dispatch_get_main_queue();
__weak THPlayerController *weakSelf = self;
void (^callback)(CMTime time) = ^(CMTime time) {
NSTimeInterval currentTime = CMTimeGetSeconds(time);
NSTimeInterval duration = CMTimeGetSeconds(weakSelf.playerItem.duration);
[weakSelf.transport setCurrentTime:currentTime duration:duration]; // 回調傳入最新值
};
// 返回id型指針, 后面會用此移除監(jiān)聽器. [self.player removeTimeObserver:self.timeObserver];
self.timeObserver =
[self.player addPeriodicTimeObserverForInterval:interval
queue:queue
usingBlock:callback];
}
- 邊界監(jiān)聽:
監(jiān)聽視頻播放視頻通過自定義時間軸邊界點.例如播放到25%,50%等.
- (id)addBoundaryTimeObserverForTimes:(NSArray<NSValue *> *)times queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(void))block;
- 條目結束監(jiān)聽:
還有一個常見監(jiān)聽事件就是播放完畢的時間.當視頻播放完畢,AVPlayerItem會發(fā)送一個AVPlayerItemDidPlayToEndTimeNotification
通知.
2.4 視頻縮略圖
現(xiàn)在流行用視頻中一個或多個縮略圖作為視頻封面. 通過AVAssetImageGenerator
類可以實現(xiàn).
它定義了兩個方法實現(xiàn)從視頻資源中檢索圖片.:
-
- (nullable CGImageRef)copyCGImageAtTime:(CMTime)requestedTime actualTime:(nullable CMTime *)actualTime error:(NSError * _Nullable * _Nullable)outError CF_RETURNS_RETAINED;
在指定時間點捕捉圖片. -
- (void)generateCGImagesAsynchronouslyForTimes:(NSArray<NSValue *> *)requestedTimes completionHandler:(AVAssetImageGeneratorCompletionHandler)handler;
在指定時間段生成一個圖片序列.
AVAssetImageGenerator可以生成本地圖片,也能生成持續(xù)下載的資源.
// 1 創(chuàng)建AVAssetImageGenerator
self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:self.asset];
// 2. 配置生成圖片大小 @2x.
self.imageGenerator.maximumSize = CGSizeMake(200.0f, 0.0f);
// 3. 計算視頻中捕捉位置. 均分20份.
CMTime duration = self.asset.duration;
NSMutableArray *times = [NSMutableArray array];
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;
__block NSMutableArray *images = [NSMutableArray array];
// 定義handler
AVAssetImageGeneratorCompletionHandler handler;
handler = ^(CMTime requestedTime, // 請求的最初時間,對應于生成圖像的調用中指定的times數(shù)組中的值.
CGImageRef imageRef, // 生成的CGImageRef.
CMTime actualTime, // 圖片實際生成的時間.基于實際效率.
AVAssetImageGeneratorResult result,
NSError *error) {
if (result == AVAssetImageGeneratorSucceeded) { // 圖片生成成功.保存圖片和對應時間點到thumbnail 模型中
UIImage *image = [UIImage imageWithCGImage:imageRef];
id thumbnail =
[THThumbnail thumbnailWithImage:image time:actualTime];
[images addObject:thumbnail];
} else {
NSLog(@"Error: %@", [error localizedDescription]);
}
// 每次調用減一,為0表示所有圖片處理完成.
if (--imageCount == 0) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *name = THThumbnailsGeneratedNotification;
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:name object:images];
});
}
};
// 調用
[self.imageGenerator generateCGImagesAsynchronouslyForTimes:times
completionHandler:handler];
2.5 顯示字幕
AVFoundation提供了展示和隱藏字幕的方法,AVPlayerLayer會自動渲染這些元素. 這里要使用兩個類:AVMediaSelectionGroup
和AVMediaSelectionOption
-
AVMediaSelectionOption :
表示AVAsset中備用的媒體呈現(xiàn)方式. 例如通常一個資源可能包含中英雙音頻,不同指定語言的字幕等備用選擇. AVAsset有一個availableMediaCharacteristicsWithMediaSelectionOptions
字符串數(shù)組屬性, 保存了這些可用選項的媒體特征. 特征這些具體來說有AVMediaCharacteristicAudible(音頻)
,AVMediaCharacteristicLegible(字幕或隱式字幕)
和AVMediaCharacteristicVisual(視頻)
三方面. -
AVMediaSelectionGroup:
而這個則作為上面類的實例容器,因為通常一個資源可以有一個和多個互斥的AVMediaSelectionOption實例可供選擇.
// 檢索資源備用呈現(xiàn)方式.(首先需要在一開始加載資源時,添加上availableMediaCharacteristicsWithMediaSelectionOptions屬性.參考上面- (void)prepareToPlay 方法)
- (void)loadMediaOptions {
// 這里只檢索字幕相關的選項
NSString *mc = AVMediaCharacteristicLegible;
AVMediaSelectionGroup *group =
[self.asset mediaSelectionGroupForMediaCharacteristic:mc];
if (group) {
for (AVMediaSelectionOption *option in group.options) {
NSLog(@"字幕可選方式: %@", option.displayName);
}
}
}
// 選擇想要的呈現(xiàn)方式.
- (void)subtitleSelected:(NSString *)subtitle {
NSString *mc = AVMediaCharacteristicLegible;
AVMediaSelectionGroup *group =
[self.asset mediaSelectionGroupForMediaCharacteristic:mc];
BOOL selected = NO;
for (AVMediaSelectionOption *option in group.options) {
// 遍歷所有組選項,找到匹配你要的option.
if ([option.displayName isEqualToString:subtitle]) {
// 激活選擇
[self.playerItem selectMediaOption:option
inMediaSelectionGroup:group];
selected = YES;
}
}
if (!selected) {
[self.playerItem selectMediaOption:nil
inMediaSelectionGroup:group];
}
}
3. 整合AirPlay
AirPlay是蘋果旨在用無線方式將流媒體內(nèi)容在Apple TV或第三方音頻系統(tǒng)上播放.
AVPlayer有一個allowsExternalPlayback
屬性,允許啟用AirPlay功能,默認是YES.
- 線路選擇功能
在播放過程中上滑就能打開系統(tǒng)AirPlay界面,但是其實應用內(nèi)部也可以提供AirPlay線路選擇界面.
UIImage *airplayImage = [UIImage imageNamed:@"airplay"];
self.volumeView = [[MPVolumeView alloc] initWithFrame:CGRectZero];
self.volumeView.showsVolumeSlider = NO;
self.volumeView.showsRouteButton = YES;
[self.volumeView setRouteButtonImage:airplayImage forState:UIControlStateNormal];
[self.volumeView sizeToFit];
NSMutableArray *items = [NSMutableArray arrayWithArray:self.toolbar.items];
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:self.volumeView];
[items addObject:item];
self.toolbar.items = items;
4. AVKit
AVKit簡化了視頻播放器的創(chuàng)建過程,它使用蘋果原生的視頻界面,不需要我們過多的自定義.如果你只是在項目中需要簡單的播放功能,推薦使用AVKit.
iOS平臺AVkit是一個簡單的標準框架,只包含一個AVPlayerViewController
類.用于展示并控制AVPlayer實例的播放.具有簡單的界面,提供以下幾個屬性和方法:
使用方法也十分簡單
NSString * path = [[NSBundle mainBundle]pathForResource:@"iphone" ofType:@"mp4"];
NSURL *url = [NSURL fileURLWithPath:path];
AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];
playerVC.player = [AVPlayer playerWithURL:url];
// 若要使用更多功能.
/// AVPlayerItem *playerItem = [AVPlayerItem alloc] initWithURL: url];
/// playerVC.player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
[self presentViewController:play animated:YES completion:nil];
iOS 8之前提供了MPMoviePlayerController和MPMoviePlayerViewController兩個類,它們提供了一種簡單的方法將完整視頻播放功能整合到應用中,相比較與AVKit, MPMoviePlayerController定義了一些標準播放控件,供我們選擇,但是同時它將所有基礎功能隱藏,讓開發(fā)者無法使用AVPlayer層的更高級的基礎功能.所以iOS9之后被易用.
而新的AVKit提供了一種動態(tài)播放控件,自動為用戶提供最好的體驗. 并且AVPlayerViewController暴露了底層AVPlayer和AVPlayerItem的接口,也支持開發(fā)者使用AVPlayer更高級的功能,