iOS 視頻開發-AVPlayer

若是我只是簡單的播放一個視頻,而不須要考慮播放器的界面。iOS9.0 以前使用 MPMoviePlayerController, 或者內部自帶一個 view 的 MPMoviePlayerViewController.  iOS9.0 以後,可使用 AVPictureInPictureControllerAVPlayerViewController, 或者 WKWebView緩存

以上系統提供的播放器因爲高度的封裝性, 使得自定義播放器變的很難。 因此,若是我須要自定義播放器樣式的時候,可使用 AVPlayer。 AVPlayer 存在於 AVFoundtion 中,更接近於底層,也更加靈活。網絡

326377-7603681aef1c08f9.jpg

Representing and Using Media with AVFoundation

AVFoundtion 框架中主要使用 AVAsset 類來展現媒體信息,好比: title, duration, size 等等。框架

  • AVAsset : 存儲媒體信息的一個抽象類,不能直接使用。ide

  • AVURLAsset : AVAsset 的一個子類,使用 URL 進行實例化,實例化對象包換 URL 對應視頻資源的全部信息。函數

  • AVPlayerItem :  有一個屬性爲 asset。起到觀察和管理視頻信息的做用。 好比,asset, tracks , status, duration ,loadedTimeRange 等。佈局

個人理解是, AVPlayItem 至關於 Model 層,包含 Media 的信息和播放狀態,並提供這些數據給視頻觀察者 好比:屬性 asset ,URL視頻的信息. loadedTimeRanges ,已緩衝進度。post

AVPlayerItem 使用

1. 初始化

playerItemWithURL 或者 initWithURL:url

在使用 AVPlayer 播放視頻時,提供視頻信息的是 AVPlayerItem,一個 AVPlayerItem 對應着一個URL視頻資源。spa

初始化一個 AVPlayItem 對象後,其屬性並非立刻就可使用。咱們必須確保 AVPlayerItem 已經被加載好了,能夠播放了,才能使用。 畢竟凡是和網絡扯上關係的都須要時間去加載。 那麼,何時屬性才能正常使用呢。 官方文檔給出瞭解決方案:code

  1. 直到 AVPlayerItem 的 status 屬性爲 AVPlayerItemStatusReadyToPlay.

  2. 使用 KVO 鍵值觀察者,其屬性。

所以咱們在使用的時候,使用 URL 初始化 AVPlayerItem 後,還要給它添加觀察者。

2. 添加觀察者

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 & AVPlayerLayer

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 上後,就沒有什麼事兒了。 思考一下,整個播放視頻的步驟。

  1. 首先,獲得視頻的URL

  2. 根據URL建立AVPlayerItem

  3. 把AVPlayerItem 提供給 AVPlayer

  4. AVPlayerLayer 顯示視頻。

  5. AVPlayer 控制視頻, 播放, 暫停, 跳轉 等等。

  6. 播放過程當中獲取緩衝進度,獲取播放進度。

  7. 視頻播放完成後作些什麼,是暫停仍是循環播放,仍是獲取最後一幀圖像。

播放步驟

1. 佈局頁面,初始化 AVPlayer 和 AVPlayerLayer

 // setAVPlayer
 self.player = [[AVPlayer alloc] init];
 _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
 [self.playerView.layer addSublayer:_playerLayer];

2. 根據 URL 獲取 AVPayerItem,並替換 AVPlayer 的 AVPlayerItem

在第一步,佈局初始化時,AVPlayer 並無 AVPlayerItem,AVPlayer 提供了 - (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;  方法,用於切換視頻。

- (void)updatePlayerWithURL:(NSURL *)url {
    _playerItem = [AVPlayerItem playerItemWithURL:url]; // create item
    [_player  replaceCurrentItemWithPlayerItem:_playerItem]; // replaceCurrentItem
    [self addObserverAndNotification]; // 註冊觀察者,通知
}

3. KVO 獲取視頻信息, 觀察緩衝進度

觀察 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]; // 更新緩衝條
    }
}

4. 播放過程當中響應:播放、 暫停、 跳轉

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) {        
    // 跳轉完成後
    }];
}

5. 觀察 AVPlayer 播放進度

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}

6. AVPlayerItem 通知

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(當前播放時間)。

相關文章
相關標籤/搜索