AVAudioFoundation(2):音視頻播放

本文轉自:AVAudioFoundation(2):音視頻播放 | www.samirchen.comhtml

本文主要內容來自 AVFoundation Programming Guideios

要播放 AVAsset 可使用 AVPlayer。在播放期間,可使用一個 AVPlayerItem 實例來管理 asset 的總體的播放狀態,使用 AVPlayerItemTrack 來管理各個 track 的播放狀態。對於視頻的渲染,使用 AVPlayerLayer 來處理。vim

播放 Asset

AVPlayer 是一個控制 asset 播放的控制器,它的功能包括:開始播放、中止播放、seek 等等。你可使用 AVPlayer 來播放單個 asset。若是你想播放一組 asset,你可使用 AVQueuePlayerAVQueuePlayerAVPlayer 的子類。數組

AVPlayer 也會提供當前的播放狀態,這樣咱們就能夠根據當前的播放狀態調整交互。咱們須要將 AVPlayer 的畫面輸出到一個特定的 Core Animation Layer 上,一般是一個 AVPlayerLayerAVSynchronizedLayer 實例。網絡

須要注意的是,你能夠從一個 AVPlayer 實例建立多個 AVPlayerLayer 對象,可是隻有最新建立的那個纔會渲染畫面到屏幕。app

對於 AVPlayer 來講,雖然最終播放的是 asset,可是咱們並不直接提供一個 AVAsset 給它,而是提供一個 AVPlayerItem 實例。AVPlayerItem 是用來管理與之關聯的 asset 的播放狀態的,一個 AVPlayerItem 包含了一組 AVPlayerItemTrack 實例,對應着 asset 中的音視頻軌道。它們直接的關係大體以下圖所示:iphone

image

注意:該圖的原圖是蘋果官方文檔上的,可是原圖是有錯的,把 AVPlayerItemTrack 所屬的框標成了 AVAsset,這裏作了修正。async

這種實現方式就意味着,咱們能夠用多個播放器同時播放一個 asset,而且各個播放器可使用不一樣的模式來渲染。下圖就展現了一種用兩個不一樣的 AVPlayer 採用不一樣的設置播放同一個 AVAsset 的場景。在播放中,還能夠禁掉某些 track 的播放。ide

image

咱們能夠經過網絡來加載 asset,一般簡單的初始化 AVPlayerItem 後並不意味着它就直接能播放,因此咱們能夠 KVO AVPlayerItemstatus 屬性來監聽它是否已經可播再決定後續的行爲。性能

處理不一樣類型的 Asset

咱們配置 asset 來播放的方式多多少少會依賴 asset 的類型,通常咱們有兩種不一樣類型的 asset:

  • 1)基於文件的 asset,通常能夠來源於本地視頻文件、相冊資源庫等等。
  • 2)流式 asset,好比 HLS 格式的視頻。

加載基於文件的 asset 通常分爲以下幾步:

  • 基於文件路徑的 URL 建立 AVURLAsset 實例。
  • 基於 AVURLAsset 實例建立 AVPlayerItem 實例。
  • AVPlayerItem 實例與一個 AVPlayer 實例關聯。
  • KVO 監測 AVPlayerItemstatus 屬性來等待其已經可播,即加載完成。

建立並加載一個 HTTP Live Stream(HLS)格式的資源來播放時,能夠按照下面幾步來作:

  • 基於資源的 URL 初始化一個 AVPlayerItem 實例,由於你沒法直接建立一個 AVAsset 來表示 HLS 資源。
  • 當你將 AVPlayerItemAVPlayer 實例關聯起來後,他就開始爲播放作準備,當一切就緒時 AVPlayerItem 會建立出 AVAssetAVAssetTrack 實例以用來對接 HLS 視頻流的音視頻內容。
  • 要獲取視頻流的時長,你須要 KVO 監測 AVPlayerItemduration 屬性,當資源能夠播放時,它會被更新爲正確的值。
NSURL *url = [NSURL URLWithString:@"<#Live stream URL#>];
// You may find a test stream at <http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8>.
self.playerItem = [AVPlayerItem playerItemWithURL:url];
[playerItem addObserver:self forKeyPath:@"status" options:0 context:&ItemStatusContext];
self.player = [AVPlayer playerWithPlayerItem:playerItem];

當你不知道一個 URL 對應的是什麼類型的 asset 時,你能夠這樣作:

  • 嘗試基於 URL 來初始化一個 AVURLAsset,並加載它的 tracks 屬性。若是 tracks 屬性加載成功,就基於 asset 來建立一個 AVPlayerItem 實例。
  • 若是 tracks 屬性加載失敗,那麼就直接基於 URL 建立一個 AVPlayerItem 實例,並 KVO 監測 AVPlayerstatus 屬性來看它什麼時候能夠播放。
  • 若是上述嘗試都失敗,那就清理掉 AVPlayerItem

播放一個 AVPlayerItem

調用 AVPlayerplay 接口便可開始播放。

- (IBAction)play:sender {
    [player play];
}

除了簡單的播放,還能夠經過設置 rate 屬性設置播放速率。

player.rate = 0.5;
player.rate = 2.0;

播放速率設置爲 1.0 表示正常播放,設置爲 0.0 表示暫停(等同調用 pause 效果)。

除了正向播放,有的音視頻還能支持倒播,不過須要須要檢查幾個屬性:

  • canPlayReverse:支持設置播放速率爲 -1.0。
  • canPlaySlowReverse:支持設置播放速率爲 -1.0 到 0.0。
  • canPlayFastReverse:支持設置播放速率爲小於 -1.0 的值。

能夠經過 seekToTime: 接口來調整播放位置。可是這個接口主要是爲性能考慮,不保證精確。

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn];

若是要精確調整,能夠用 seekToTime:toleranceBefore:toleranceAfter: 接口。

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];

須要注意的是,設置 tolerance 爲 zero 會耗費較大的計算性能,因此通常只在編寫複雜的音視頻編輯功能是這樣設置。

咱們能夠經過監聽 AVPlayerItemDidPlayToEndTimeNotification 來得到播放結束事件,在播放結束後能夠用 seekToTime: 調整播放位置到 zero,不然調用 play 會無效。

// Register with the notification center after creating the player item.
[[NSNotificationCenter defaultCenter] 
    addObserver:self 
       selector:@selector(playerItemDidReachEnd:)
           name:AVPlayerItemDidPlayToEndTimeNotification
         object:<#The player item#>];
 
- (void)playerItemDidReachEnd:(NSNotification *)notification {
    [player seekToTime:kCMTimeZero];
}

此外,咱們還能設置播放器的 actionAtItemEnd 屬性來設置其在播放結束後的行爲,好比 AVPlayerActionAtItemEndPause 表示播放結束後會暫停。

播放多個 AVPlayerItem

咱們能夠用 AVQueuePlayer 來順序播放多個 AVPlayerItemAVQueuePlayerAVPlayer 的子類。

NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];

經過調用 play 便可順序播放,也能夠調用 advanceToNextItem 跳到下個 item。除此以外,咱們還能夠用 insertItem:afterItem:removeItem:removeAllItems 來控制播放資源。

當插入一個 item 的時候,能夠須要用 canInsertItem:afterItem: 檢查下是否能夠插入, 對 afterItem 傳入 nil,則檢查是否能夠插入到隊尾。

AVPlayerItem *anItem = <#Get a player item#>;
if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
    [queuePlayer insertItem:anItem afterItem:nil];
}

監測播放狀態

咱們能夠監測一些 AVPlayer 的狀態和正在播放的 AVPlayerItem 的狀態,這對於處理那些不在你直接控制下的 state 是頗有用的,好比:

  • 若是用戶使用多任務處理切換到另外一個應用程序,播放器的 rate 屬性將降低到 0.0。
  • 當播放遠程媒體資源(好比網絡視頻)時,監測 AVPlayerItemloadedTimeRangesseekableTimeRanges 能夠知道能夠播放和 seek 的資源時長。
  • 當播放 HTTP Live Stream 時,播放器的 currentItem 可能發生變化。
  • 當播放 HTTP Live Stream 時,AVPlayerItemtracks 可能發生變化。這種狀況可能發生在播放流切換了編碼。
  • 當播放失敗時,AVPlayerAVPlayerItemstatus 可能發生變化。

響應 status 屬性的變化

經過 KVO 監測 AVPlayer 和正在播放的 AVPlayerItemstatus 屬性,能夠得到對應的通知,好比當播放出現錯誤時,你可能會收到 AVPlayerStatusFailedAVPlayerItemStatusFailed 通知,這時你就能夠作相應的處理。

須要注意的是,因爲 AVFoundation 不會指定在哪一個線程發送通知,因此若是你須要在收到通知後更新用戶界面的話,你須要切到主線程。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == <#Player status context#>) {
        AVPlayer *thePlayer = (AVPlayer *) object;
        if ([thePlayer status] == AVPlayerStatusFailed) {
            NSError *error = [<#The AVPlayer object#> error];
            // Respond to error: for example, display an alert sheet.
            return;
        }
        // Deal with other status change if appropriate.
    }
    // Deal with other change notifications if appropriate.
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    return;
}

跟蹤視覺內容就緒狀態

咱們能夠監測 AVPlayerLayer 實例的 readyForDisplay 屬性來得到播放器已經能夠開始渲染視覺內容的通知。

基於這個能力,咱們就能實如今播放器的視覺內容就緒時纔將 player layer 插入到 layer 樹中去展現給用戶。

追蹤播放時間變化

咱們可使用 AVPlayeraddPeriodicTimeObserverForInterval:queue:usingBlock:addBoundaryTimeObserverForTimes:queue:usingBlock: 這兩個接口來追蹤當前播放位置的變化,這樣咱們就能夠在用戶界面上作出更新,反饋給用戶當前的播放時間和剩餘的播放時間等等。

  • addPeriodicTimeObserverForInterval:queue:usingBlock:,這個接口將會在播放時間發生變化時在回調 block 中通知咱們當前播放時間。
  • addBoundaryTimeObserverForTimes:queue:usingBlock:,這個接口容許咱們傳入一組時間(CMTime 數組)當播放器播到這些時間時會在回調 block 中通知咱們。

這兩個接口都會返回一個 observer 角色的對象給咱們,咱們須要在監測時間的這個過程當中強引用這個對象,同時在不須要使用它時調用 removeTimeObserver: 接口來移除它。

此外,AVFoundation 也不保證在每次時間變化或設置時間到達時都回調 block 來通知你。好比當上一次回調 block 還沒完成的狀況時,又到了這次回調 block 的時機,AVFoundation 此次就不會調用 block。因此咱們須要確保不要在 block 回調裏作開銷太大、耗時太長的任務。

// Assume a property: @property (strong) id playerObserver;
 
Float64 durationSeconds = CMTimeGetSeconds([<#An asset#> duration]);
CMTime firstThird = CMTimeMakeWithSeconds(durationSeconds/3.0, 1);
CMTime secondThird = CMTimeMakeWithSeconds(durationSeconds*2.0/3.0, 1);
NSArray *times = @[[NSValue valueWithCMTime:firstThird], [NSValue valueWithCMTime:secondThird]];
 
self.playerObserver = [<#A player#> addBoundaryTimeObserverForTimes:times queue:NULL usingBlock:^{
 
    NSString *timeDescription = (NSString *)
        CFBridgingRelease(CMTimeCopyDescription(NULL, [self.player currentTime]));
    NSLog(@"Passed a boundary at %@", timeDescription);
}];

播放結束

監聽 AVPlayerItemDidPlayToEndTimeNotification 這個通知便可。上文有提到,這裏再也不重複。

一個完整示例

這裏的示例將展現若是使用 AVPlayer 來播放一個視頻文件,主要包括下面幾個步驟:

  • 配置一個使用 AVPlayerLayer layer 的 UIView
  • 建立一個 AVPlayer 實例。
  • 基於文件類型的 asset 建立一個 AVPlayerItem 實例,並用 KVO 監測其 status 屬性。
  • 響應 AVPlayerItem 實例能夠播放的通知,顯示出一個按鈕。
  • 播放 AVPlayerItem 並播放完成後將其播放位置調整到開始位置。

首先是 PlayerView:

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
 
@interface PlayerView : UIView
@property (nonatomic) AVPlayer *player;
@end
 
@implementation PlayerView
+ (Class)layerClass {
    return [AVPlayerLayer class];
}
- (AVPlayer*)player {
    return [(AVPlayerLayer *)[self layer] player];
}
- (void)setPlayer:(AVPlayer *)player {
    [(AVPlayerLayer *)[self layer] setPlayer:player];
}
@end

一個簡單的 PlayerViewController:

@class PlayerView;
@interface PlayerViewController : UIViewController
 
@property (nonatomic) AVPlayer *player;
@property (nonatomic) AVPlayerItem *playerItem;
@property (nonatomic, weak) IBOutlet PlayerView *playerView;
@property (nonatomic, weak) IBOutlet UIButton *playButton;
- (IBAction)loadAssetFromFile:sender;
- (IBAction)play:sender;
- (void)syncUI;
@end

同步 UI 的方法:

- (void)syncUI {
    if ((self.player.currentItem != nil) &&
        ([self.player.currentItem status] == AVPlayerItemStatusReadyToPlay)) {
        self.playButton.enabled = YES;
    }
    else {
        self.playButton.enabled = NO;
    }
}

viewDidLoad 時先調用一下 syncUI

- (void)viewDidLoad {
    [super viewDidLoad];
    [self syncUI];
}

建立並加載 AVURLAsset,在加載成功時,建立 item、初始化播放器以及添加各類監聽:

static const NSString *ItemStatusContext;

- (IBAction)loadAssetFromFile:sender {
 
    NSURL *fileURL = [[NSBundle mainBundle] URLForResource:<#@"VideoFileName"#> withExtension:<#@"extension"#>];
 
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
    NSString *tracksKey = @"tracks";
 
    [asset loadValuesAsynchronouslyForKeys:@[tracksKey] completionHandler: ^{
        // The completion block goes here.
        dispatch_async(dispatch_get_main_queue(), ^{
            NSError *error;
            AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];

            if (status == AVKeyValueStatusLoaded) {
                self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
                 // ensure that this is done before the playerItem is associated with the player
                [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionInitial context:&ItemStatusContext];
                [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
                self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
                [self.playerView setPlayer:self.player];
            } else {
                // You should deal with the error appropriately.
                NSLog(@"The asset's tracks were not loaded:\n%@", [error localizedDescription]);
            }
        });
    }];
}

響應 status 的監聽通知:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == &ItemStatusContext) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self syncUI];
        });
        return;
    }
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    return;
}

播放,以及播放完成時的處理:

- (IBAction)play:sender {
    [self.player play];
}


- (void)playerItemDidReachEnd:(NSNotification *)notification {
    [self.player seekToTime:kCMTimeZero];
}
相關文章
相關標籤/搜索