如果我只是簡單的播放一個視頻,而不需要考慮播放器的界面。iOS9.0 之前使用 MPMoviePlayerController
, 或者內部自帶一個 view 的 MPMoviePlayerViewController
. iOS9.0 之後,可以使用 AVPictureInPictureController
, AVPlayerViewController
, 或者 WKWebView
。
以上系統提供的播放器由於高度的封裝性, 使得自定義播放器變的很難。 所以,如果我需要自定義播放器樣式的時候,可以使用 AVPlayer
。 AVPlayer 存在於 AVFoundtion 中,更接近於底層,也更加靈活。
AVFoundtion 框架中主要使用 AVAsset
類來展示媒體信息,比如: title, duration, size 等等。
AVAsset : 存儲媒體信息的一個抽象類,不能直接使用。
AVURLAsset : AVAsset 的一個子類,使用 URL 進行實例化,實例化對象包換 URL 對應視頻資源的所有信息。
AVPlayerItem : 有一個屬性為 asset。起到觀察和管理視頻信息的作用。 比如,asset, tracks , status, duration ,loadedTimeRange 等。
我的理解是, AVPlayItem 相當於 Model 層,包含 Media 的信息和播放狀態,並提供這些數據給視頻觀察者 比如:屬性 asset
,URL視頻的信息. loadedTimeRanges
,已緩沖進度。
playerItemWithURL
或者 initWithURL:
在使用 AVPlayer 播放視頻時,提供視頻信息的是 AVPlayerItem,一個 AVPlayerItem 對應著一個URL視頻資源。
初始化一個 AVPlayItem 對象後,其屬性並不是馬上就可以使用。我們必須確保 AVPlayerItem 已經被加載好了,可以播放了,才能使用。 畢竟凡是和網絡扯上關系的都需要時間去加載。 那麼,什麼時候屬性才能正常使用呢。 官方文檔給出了解決方案:
直到 AVPlayerItem 的 status
屬性為 AVPlayerItemStatusReadyToPlay
.
使用 KVO 鍵值觀察者,其屬性。
因此我們在使用的時候,使用 URL 初始化 AVPlayerItem 後,還要給它添加觀察者。
AVPlayreItem 的屬性需要當 status 為 ReadyToPlay 的時候才可以正常使用。
觀察status屬性
[_playerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew) context:nil]; // 觀察status屬性,
觀察loadedTimeRanges
如果想做緩沖進度條,顯示當前視頻的緩存進度,則需要觀察 loadedTimeRanges
.
[_playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; // 觀察緩沖進度
AVPlayer創建方式
AVPlayer 有三種創建方式:
init
,initWithURL:
,initWithPlayerItem:
(URL,Item遍歷構造器方法)
使用 AVPlayer 時需要注意,AVPlayer 本身並不能顯示視頻, 顯示視頻的是 AVPlayerLayer。 AVPlayerLayer 繼承自 CALayer,添加到 view.layer 上就可以使用了。
AVPlayerLayer創建方式
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player]; [superlayer addSublayer:playerLayer];
AVPlayerLayer 顯示視頻,AVPlayerItem 提供視頻信息, AVPlayer 管理和調控。 這是不是非常熟悉。 我覺得這裡也體現了 MVC 的思想(雖然AVPlayer繼承自NSObject), 把響應層, 顯示層, 信息層, 三層分離了。 明確了每層做的任務,使用起來就會更加得心應手。
使用 AVPlayer 的核心,在於 AVPlayer 和 AVPlayerItem, AVPlayerLayer 添加到視圖的layer 上後,就沒有什麼事兒了。 思考一下,整個播放視頻的步驟。
首先,得到視頻的URL
根據URL創建AVPlayerItem
把AVPlayerItem 提供給 AVPlayer
AVPlayerLayer 顯示視頻。
AVPlayer 控制視頻, 播放, 暫停, 跳轉 等等。
播放過程中獲取緩沖進度,獲取播放進度。
視頻播放完成後做些什麼,是暫停還是循環播放,還是獲取最後一幀圖像。
// setAVPlayer self.player = [[AVPlayer alloc] init]; _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; [self.playerView.layer addSublayer:_playerLayer];
在第一步,布局初始化時,AVPlayer 並沒有 AVPlayerItem,AVPlayer 提供了 - (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;
方法,用於切換視頻。
- (void)updatePlayerWithURL:(NSURL *)url { _playerItem = [AVPlayerItem playerItemWithURL:url]; // create item [_player replaceCurrentItemWithPlayerItem:_playerItem]; // replaceCurrentItem [self addObserverAndNotification]; // 注冊觀察者,通知 }
觀察 AVPlayerItem 的 status
屬性,當狀態變為 AVPlayerStatusReadyToPlay
時才可以使用。
也可以觀察 loadedTimeRanges
獲取緩沖進度
注冊觀察者:
[_playerItem addObserver:self forKeyPath:@"status" options:(NSKeyValueObservingOptionNew) context:nil]; // 觀察status屬性
執行觀察者方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { AVPlayerItem *item = (AVPlayerItem *)object; if ([keyPath isEqualToString:@"status"]) { AVPlayerStatus status = [[change objectForKey:@"new"] intValue]; // 獲取更改後的狀態 if (status == AVPlayerStatusReadyToPlay) { CMTime duration = item.duration; // 獲取視頻長度 // 設置視頻時間 [self setMaxDuration:CMTimeGetSeconds(duration)]; // 播放 [self play]; } else if (status == AVPlayerStatusFailed) { NSLog(@"AVPlayerStatusFailed"); } else { NSLog(@"AVPlayerStatusUnknown"); } } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) { NSTimeInterval timeInterval = [self availableDurationRanges]; // 緩沖時間 CGFloat totalDuration = CMTimeGetSeconds(_playerItem.duration); // 總時間 [self.loadedProgress setProgress:timeInterval / totalDuration animated:YES]; // 更新緩沖條 } }
AVPlayer 提供了 play
, pause
, 和 - (void)seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler
方法。
在看 AVPlayer 的 seekToTime 之前,先來認識一個結構體。
CMTime 是專門用於標識電影時間的結構體.
typedef struct{ CMTimeValue value; // 幀數 CMTimeScale timescale; // 幀率(影片每秒有幾幀) CMTimeFlags flags; CMTimeEpoch epoch; } CMTime;
AVPlayerItem 的 duration 屬性就是一個 CMTime 類型的數據。 如果我們想要獲取影片的總秒數那麼就可以用 duration.value / duration.timeScale 計算出來。也可以使用 CMTimeGetSeconds 函數
CMTimeGetSeconds(CMtime time)
double seconds = CMTimeGetSeconds(item.duration); // 相當於 duration.value / duration.timeScale
如果一個影片為60frame(幀)每秒, 當前想要跳轉到 120幀的位置,也就是兩秒的位置,那麼就可以創建一個 CMTime 類型數據。
CMTime,通常用如下兩個函數來創建.
CMTimeMake(int64_t value, int32_t scale)
CMTime time1 = CMTimeMake(120, 60);
CMTimeMakeWithSeconds(Flout64 seconds, int32_t scale)
CMTime time2 = CMTimeWithSeconds(120, 60);
CMTimeMakeWithSeconds 和CMTimeMake 區別在於,第一個函數的第一個參數可以是float,其他一樣。
拖拽方法如下:
- (IBAction)playerSliderValueChanged:(id)sender { _isSliding = YES; [self pause]; // 跳轉到拖拽秒處 // self.playProgress.maxValue = value / timeScale // value = progress.value * timeScale // CMTimemake(value, timeScale) = (progress.value, 1.0) CMTime changedTime = CMTimeMakeWithSeconds(self.playProgress.value, 1.0); [_playerItem seekToTime:changedTime completionHandler:^(BOOL finished) { // 跳轉完成後 }]; }
AVPlayerItem 是使用 KVO 模式觀察狀態,和 緩沖進度。而 AVPlayer 給我們直接提供了 觀察播放進度更為方便的方法。
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;
方法名如其意, “添加周期時間觀察者” ,參數1 interal
為CMTime 類型的,參數2 為一個 返回值為空,參數為 CMTime 的block類型。
簡而言之就是,每隔一段時間後執行 block。
比如: 我把時間間隔設置為, 1/ 30 秒,然後 block 裡面更新 UI。就是一秒鐘更新 30次UI。
播放進度代碼如下:
// 觀察播放進度 - (void)monitoringPlayback:(AVPlayerItem *)item { __weak typeof(self)WeakSelf = self; // 觀察間隔, CMTime 為30分之一秒 _playTimeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 30.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { if (_touchMode != TouchPlayerViewModeHorizontal) { // 獲取 item 當前播放秒 float currentPlayTime = (double)item.currentTime.value/ item.currentTime.timescale; // 更新slider, 如果正在滑動則不更新 if (_isSliding == NO) { [WeakSelf updateVideoSlider:currentPlayTime]; } } else { return; } }]; }
注意: 給 palyer 添加了 timeObserver 後,不使用的時候記得移除 removeTimeObserver
否則會占用大量內存。
比如,我在dealloc裡面做了移除:
- (void)dealloc { [self removeObserveAndNOtification]; [_player removeTimeObserver:_playTimeObserver]; // 移除playTimeObserver}
AVPlaerItem 播放完成後,系統會自動發送通知,通知的定義詳情請見 AVPlayerItem.h
.
/* Note that NSNotifications posted by AVPlayerItem may be posted on a different thread from the one on which the observer was registered. */ // notifications description AVF_EXPORT NSString *const AVPlayerItemTimeJumpedNotification NS_AVAILABLE(10_7, 5_0); // the item's current time has changed discontinuously AVF_EXPORT NSString *const AVPlayerItemDidPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_0); // item has played to its end time AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_3); // item has failed to play to its end time AVF_EXPORT NSString *const AVPlayerItemPlaybackStalledNotification NS_AVAILABLE(10_9, 6_0); // media did not arrive in time to continue playback AVF_EXPORT NSString *const AVPlayerItemNewAccessLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new access log entry has been added AVF_EXPORT NSString *const AVPlayerItemNewErrorLogEntryNotification NS_AVAILABLE(10_9, 6_0); // a new error log entry has been added // notification userInfo key type AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeErrorKey NS_AVAILABLE(10_7, 4_3); // NSError
因此,如果我們想要在某個狀態下,執行某些操作。監聽 AVPlayerItem 的相關通知就行了。 比如,我想要播放完成後,暫停播放。 給AVPlayerItemDidPlayToEndTimeNotification
添加觀察者。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; // 播放完成後 - (void)playbackFinished:(NSNotification *)notification { NSLog(@"視頻播放完成通知"); _playerItem = [notification object]; [_playerItem seekToTime:kCMTimeZero]; // item 跳轉到初始 //[_player play]; // 循環播放 }
使用 AVPlayer 的時候,一定要注意 AVPlayer 、 AVPlayerLayer 和 AVPlayerItem 三者之間的關系。 AVPlayer 負責控制播放, layer 顯示播放, item 提供數據,當前播放時間, 已加載情況。 Item 中一些基本的屬性, status, duration, loadedTimeRanges, currentTime(當前播放時間)。
最重要的還是多總結,6月份寫的這個 Demo ,現在才總結,懶癌到晚期 =。 =
當然,如果我寫的文章有幸讓你看到了最後,那麼, 或許 你想要更多的功能。比如,橫豎屏旋轉,一些交互動畫等等。 我有個簡單地 demo,實現了一些小小的功能,放到了 github 上, 裡面還有很多不足,多溝通交流 = W = 。
githubDemo地址