iOS視頻播放器之ZFPlayer剖析
字?jǐn)?shù)1943 閱讀4838 評論63 喜歡64
引言
本文主要針對ZFPlayer的功能實現(xiàn)來剖析,以及總結(jié)一下大家遇到的問題和解決方案
首先ZFPlayer現(xiàn)在擁有的功能:
支持橫、豎屏切換,在全屏播放模式下還可以鎖定屏幕方向
支持本地視頻、網(wǎng)絡(luò)視頻播放
支持在TableviewCell播放視頻
左側(cè)1/2位置上下滑動調(diào)節(jié)屏幕亮度(模擬器調(diào)不了亮度,請在真機調(diào)試)
右側(cè)1/2位置上下滑動調(diào)節(jié)音量(模擬器調(diào)不了音量,請在真機調(diào)試)
左右滑動調(diào)節(jié)播放進(jìn)度
全屏狀態(tài)下拖動slider控制進(jìn)度,顯示視頻的預(yù)覽圖
斷點下載功能
切換視頻分辨率
ZFPlayer是對AVPlayer的封裝,有人會問它支持什么格式的視頻播放,問這個問題的可以自行搜索AVPlayer支持的格式。
跟AVPlayer聯(lián)系密切的名詞:
Asset:AVAsset是抽象類,不能直接使用,其子類AVURLAsset可以根據(jù)URL生成包含媒體信息的Asset對象。
AVPlayerItem:和媒體資源存在對應(yīng)關(guān)系,管理媒體資源的信息和狀態(tài)。
AVPlayerLayer: CALayer的subclass,它主要用來在iOS中播放視頻內(nèi)容
具體功能實現(xiàn)
1、通過一個網(wǎng)絡(luò)鏈接播放視頻
AVURLAsset *urlAsset = [AVURLAsset assetWithURL:videoURL];
// 初始化playerItem
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:urlAsset];
// 也可以使用來初始化playerItem
// AVPlayerItem * playerItem = [AVPlayerItem playerItemWithURL:videoURL];
// 初始化Player
AVPlayer *player = [AVPlayer playerWithPlayerItem:self.playerItem];
// 初始化playerLayer
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
// 添加playerLayer到self.layer
[self.layer insertSublayer:self.playerLayer atIndex:0];
2、播放器的常用操作
播放:
[player play];
需要注意的是初始化完player之后不一定會馬上開始播放,需要等待player的狀態(tài)變?yōu)镽eadyToPlay才會進(jìn)行播放。
暫停:
[player pause];
3、播放多個items
這里我們有兩種方式可以實現(xiàn),一種是由你自行控制下一首歌曲的item,將其替換到當(dāng)前播放的item
[player replaceCurrentItemWithPlayerItem:playerItem];
在iOS9后,AVPlayer的replaceCurrentItemWithPlayerItem方法在切換視頻時底層會調(diào)用信號量等待然后導(dǎo)致當(dāng)前線程卡頓,如果在UITableViewCell中切換視頻播放使用這個方法,會導(dǎo)致當(dāng)前線程凍結(jié)幾秒鐘。遇到這個坑還真不好在系統(tǒng)層面對它做什么,后來找到的解決方法是在每次需要切換視頻時,需重新創(chuàng)建AVPlayer和AVPlayerItem。
另一種可以使用AVQueuePlayer播放多個items,AVQueuePlayer是AVPlayer的子類,可以用一個數(shù)組來初始化一個AVQueuePlayer對象。代碼如下:
NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];
和AVPlayer一樣,直接調(diào)用play方法來播放,queue player順序播放隊列中的item,如果想要跳過一個item,播放下一個item,可以調(diào)用方法advanceToNextItem。
可以對隊列進(jìn)行插入和刪除操作,調(diào)用方法insertItem:afterItem:, removeItem:, 和 removeAllItems。正常情況下當(dāng)插入一個item之前,應(yīng)該檢查是否可以插入,通過使用canInsertItem:afterItem:方法,第二個參數(shù)傳nil,代碼如下:
AVPlayerItem *anItem = <#Get a player item#>;
if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
[queuePlayer insertItem:anItem afterItem:nil];
}
4、seekToTime指定從某一秒開始播放
可以使用seekToTime:定位播放頭到指定的時間,如下代碼:
CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn];
seekTime:不能精確定位,如果需要精確定位,可以使用seekToTime:toleranceBefore:toleranceAfter:,代碼如下:
CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
當(dāng)tolerance=0的時候,framework需要進(jìn)行大量解碼工作,比較耗性能,所以,只有當(dāng)你必須使用的時候才用這個方法,比如開發(fā)一個復(fù)雜的多媒體編輯應(yīng)用,這需要精確的控制。
關(guān)于重播什么的就不用我多說了吧,點擊重播seekToTime:kCMTimeZero。還有關(guān)于下次播放的時候從上次離開的那個時間開始播放,大家都有思路啦吧,當(dāng)離開當(dāng)前視頻時候記錄播放到哪一秒了,下次點開直接seekToTime到那一秒開始播放就好了嘛。
5、監(jiān)聽播放進(jìn)度
使用addPeriodicTimeObserverForInterval:queue:usingBlock:來監(jiān)聽播放器的進(jìn)度
(1)方法傳入一個CMTime結(jié)構(gòu)體,每到一定時間都會回調(diào)一次,包括開始和結(jié)束播放
(2)如果block里面的操作耗時太長,下次不一定會收到回調(diào),所以盡量減少block的操作耗時
(3)方法會返回一個觀察者對象,當(dāng)播放完畢時需要移除這個觀察者
添加觀察者:
id timeObserve = [player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
float current = CMTimeGetSeconds(time);
float total = CMTimeGetSeconds(songItem.duration);
if (current) {
weakSelf.progress = current / total;
weakSelf.playTime = [NSString stringWithFormat:@"%.f",current];
weakSelf.playDuration = [NSString stringWithFormat:@"%.2f",total];
}
}];
移除觀察者:
if (timeObserve) {
[player removeTimeObserver:_timeObserve];
timeObserve = nil;
}
6、監(jiān)聽改播放器狀態(tài)
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
播放器的三種狀態(tài),當(dāng)playerItem的狀態(tài)變?yōu)锳VPlayerItemStatusReadyToPlay才會進(jìn)行播放。
typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
AVPlayerItemStatusUnknown,
AVPlayerItemStatusReadyToPlay,
AVPlayerItemStatusFailed
};
播放完了需要移除觀察者
[playerItem removeObserver:self forKeyPath:@"status"];
7、監(jiān)聽緩沖進(jìn)度
[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
播放完了需要移除觀察者
[playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
8、監(jiān)聽網(wǎng)絡(luò)緩沖狀態(tài)
// 緩沖區(qū)空了,需要等待數(shù)據(jù)
[playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
// 緩沖區(qū)有足夠數(shù)據(jù)可以播放了
[playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
播放完了需要移除觀察者
[playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
[playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
9、監(jiān)聽AVPlayer播放完成通知
監(jiān)聽通知AVPlayerItemDidPlayToEndTimeNotification,來處理一些播放完的事情
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
10、 系統(tǒng)音量相關(guān)
/**
*? 獲取系統(tǒng)音量
*/
- (void)configureVolume
{
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
_volumeViewSlider = nil;
for (UIView *view in [volumeView subviews]){
if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
_volumeViewSlider = (UISlider *)view;
break;
}
}
// 使用這個category的應(yīng)用不會隨著手機靜音鍵打開而靜音,可在手機靜音下播放聲音
NSError *setCategoryError = nil;
BOOL success = [[AVAudioSession sharedInstance]
setCategory: AVAudioSessionCategoryPlayback
error: &setCategoryError];
if (!success) { /* handle the error in setCategoryError */ }
// 監(jiān)聽耳機插入和拔掉通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];
}
/**
*? 耳機插入、拔出事件
*/
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification
{
NSDictionary *interuptionDict = notification.userInfo;
NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
switch (routeChangeReason) {
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
// 耳機插入
break;
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
{
// 耳機拔掉
// 拔掉耳機繼續(xù)播放
[self play];
}
break;
case AVAudioSessionRouteChangeReasonCategoryChange:
// called at start - also when other audio wants to play
NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange");
break;
}
}
設(shè)置系統(tǒng)音量
// 0 ... 1.0的數(shù)值, 1.0是最大的聲音.
self.volumeViewSlider.value = ...
11、屏幕亮度相關(guān)
// 0 ... 1.0的數(shù)值, 1.0是最大的亮度.
[UIScreen mainScreen].brightness = ...
12、屏幕旋轉(zhuǎn)相關(guān)
蘋果手機除iPhone 4s(320*480)屏幕寬高比不是16:9外,其他都為16:9,所以橫豎屏可以這樣實現(xiàn),這里必須使用autolayout,這里提供兩種方法實現(xiàn):
使用Xib或者Storyboard的話,必須把播放器view的寬高比設(shè)置成16:9,4s的話可以單獨適配加約束(使用sizeClasses)
使用masonry,具體代碼如下:
[self.playerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view).offset(20);
make.left.right.equalTo(self.view);
// 注意此處,寬高比16:9優(yōu)先級比1000低就行,在因為iPhone 4S寬高比不是16:9
make.height.equalTo(self.playerView.mas_width).multipliedBy(9.0f/16.0f).with.priority(750);
}];
關(guān)于屏幕旋轉(zhuǎn)可以這樣強制讓屏幕轉(zhuǎn)屏,有人會問了,在我demo中為啥能轉(zhuǎn)屏,而集成到自己項目中不能轉(zhuǎn)屏,我可以明確的告訴你,是你們項目的橫屏給禁止掉了,你可以看一下這里是否打鉤啦:
設(shè)備方向
有人又會問了,我們想實現(xiàn)這么個需求,只有在播放器頁面支持橫屏,其他頁面不支持橫屏。好了,那下邊我來告訴怎么實現(xiàn),首先上圖中的橫屏必須勾選,其次在你需要轉(zhuǎn)屏的ViewController中來實現(xiàn)三個方法:
//? 是否支持自動轉(zhuǎn)屏
- (BOOL)shouldAutorotate
{
// 調(diào)用ZFPlayerSingleton單例記錄播放狀態(tài)是否鎖定屏幕方向
return !ZFPlayerShared.isLockScreen;
}
// 支持哪些轉(zhuǎn)屏方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
// 頁面展示的時候默認(rèn)屏幕方向(當(dāng)前ViewController必須是通過模態(tài)ViewController(模態(tài)帶導(dǎo)航的無效)方式展現(xiàn)出來的,才會調(diào)用這個方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationPortrait;
}
ZFPlayer內(nèi)部已經(jīng)實現(xiàn)屏幕旋轉(zhuǎn)的分類(UITabBarController+ZFPlayerRotation.h UINavigationController+ZFPlayerRotation UIViewController+ZFPlayerRotation),不管你項目的rootViewController的是UINavigationController還是UITabBarController,則只需要在支持除豎屏以外的控制器實現(xiàn)上邊三個方法就行。
下邊來說說強制屏幕旋轉(zhuǎn),即使用戶的手機鎖定了屏幕方法,調(diào)用這個方法照樣可以旋轉(zhuǎn):
/**
*? 強制屏幕轉(zhuǎn)屏
*
*? @param orientation 屏幕方向
*/
- (void)interfaceOrientation:(UIInterfaceOrientation)orientation
{
// arc下
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
SEL selector? ? ? ? ? ? = NSSelectorFromString(@"setOrientation:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevice currentDevice]];
int val? ? ? ? ? ? ? ? ? = orientation;
// 從2開始是因為0 1 兩個參數(shù)已經(jīng)被selector和target占用
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}
/*
// 非arc下
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
[[UIDevice currentDevice] performSelector:@selector(setOrientation:)
withObject:@(orientation)];
}
// 直接調(diào)用這個方法通不過apple上架審核
[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIInterfaceOrientationLandscapeRight] forKey:@"orientation"];
*/
}
監(jiān)聽設(shè)備旋轉(zhuǎn)通知,來處理一些UI顯示問題
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange)
name:UIDeviceOrientationDidChangeNotification
object:nil
];
你可能遇到的問題
1、clone下來demo,直接運行報錯。
解決辦法:請確認(rèn)你電腦安裝cocopods,然后cd到項目主目錄,執(zhí)行pod install,然后運行Player.xcworkspace
2、clone下來demo,pod install后發(fā)現(xiàn),ZFDownload缺少幾個類
解決辦法:可能是pod ZFDownload的版本不對,ZFDownload->1.0.0,如果發(fā)現(xiàn)pod search ZFDownload 搜索出來的不是最新版本,需要在終端執(zhí)行cd轉(zhuǎn)換文件路徑命令退回到desktop,然后執(zhí)行pod setup命令更新本地spec緩存(可能需要幾分鐘),然后再搜索就可以了。
未完待續(xù)....
Demo
本文demo詳見:https://github.com/renzifeng/ZFPlayer