iOS視頻播放器之ZFPlayer剖析(轉(zhuǎn)載)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容