AVAudioFoundation(2):音視頻播放

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

本文主要內容來自 AVFoundation Programming Guide

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

播放 Asset

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

AVPlayer 也會提供當前的播放狀態,這樣我們就可以根據當前的播放狀態調整交互。我們需要將 AVPlayer 的畫面輸出到一個特定的 Core Animation Layer 上,通常是一個 AVPlayerLayerAVSynchronizedLayer 實例。

需要注意的是,你可以從一個 AVPlayer 實例創建多個 AVPlayerLayer 對象,但是只有最新創建的那個才會渲染畫面到屏幕。

對于 AVPlayer 來說,雖然最終播放的是 asset,但是我們并不直接提供一個 AVAsset 給它,而是提供一個 AVPlayerItem 實例。AVPlayerItem 是用來管理與之關聯的 asset 的播放狀態的,一個 AVPlayerItem 包含了一組 AVPlayerItemTrack 實例,對應著 asset 中的音視頻軌道。它們直接的關系大致如下圖所示:

image

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

這種實現方式就意味著,我們可以用多個播放器同時播放一個 asset,并且各個播放器可以使用不同的模式來渲染。下圖就展示了一種用兩個不同的 AVPlayer 采用不同的設置播放同一個 AVAsset 的場景。在播放中,還可以禁掉某些 track 的播放。

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];
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容