本文轉自:AVAudioFoundation(2):音視頻播放 | www.samirchen.com
本文主要內容來自 AVFoundation Programming Guide。
要播放 AVAsset
可以使用 AVPlayer
。在播放期間,可以使用一個 AVPlayerItem
實例來管理 asset 的整體的播放狀態,使用 AVPlayerItemTrack
來管理各個 track 的播放狀態。對于視頻的渲染,使用 AVPlayerLayer
來處理。
播放 Asset
AVPlayer
是一個控制 asset 播放的控制器,它的功能包括:開始播放、停止播放、seek 等等。你可以使用 AVPlayer
來播放單個 asset。如果你想播放一組 asset,你可以使用 AVQueuePlayer
,AVQueuePlayer
是 AVPlayer
的子類。
AVPlayer
也會提供當前的播放狀態,這樣我們就可以根據當前的播放狀態調整交互。我們需要將 AVPlayer
的畫面輸出到一個特定的 Core Animation Layer 上,通常是一個 AVPlayerLayer
或 AVSynchronizedLayer
實例。
需要注意的是,你可以從一個 AVPlayer
實例創建多個 AVPlayerLayer
對象,但是只有最新創建的那個才會渲染畫面到屏幕。
對于 AVPlayer
來說,雖然最終播放的是 asset,但是我們并不直接提供一個 AVAsset
給它,而是提供一個 AVPlayerItem
實例。AVPlayerItem
是用來管理與之關聯的 asset 的播放狀態的,一個 AVPlayerItem
包含了一組 AVPlayerItemTrack
實例,對應著 asset 中的音視頻軌道。它們直接的關系大致如下圖所示:
注意:該圖的原圖是蘋果官方文檔上的,但是原圖是有錯的,把 AVPlayerItemTrack
所屬的框標成了 AVAsset
,這里做了修正。
這種實現方式就意味著,我們可以用多個播放器同時播放一個 asset,并且各個播放器可以使用不同的模式來渲染。下圖就展示了一種用兩個不同的 AVPlayer
采用不同的設置播放同一個 AVAsset
的場景。在播放中,還可以禁掉某些 track 的播放。
我們可以通過網絡來加載 asset,通常簡單的初始化 AVPlayerItem
后并不意味著它就直接能播放,所以我們可以 KVO AVPlayerItem
的 status
屬性來監聽它是否已經可播再決定后續的行為。
處理不同類型的 Asset
我們配置 asset 來播放的方式多多少少會依賴 asset 的類型,一般我們有兩種不同類型的 asset:
- 1)基于文件的 asset,一般可以來源于本地視頻文件、相冊資源庫等等。
- 2)流式 asset,比如 HLS 格式的視頻。
加載基于文件的 asset 一般分為如下幾步:
- 基于文件路徑的 URL 創建
AVURLAsset
實例。 - 基于
AVURLAsset
實例創建AVPlayerItem
實例。 - 將
AVPlayerItem
實例與一個AVPlayer
實例關聯。 - KVO 監測
AVPlayerItem
的status
屬性來等待其已經可播,即加載完成。
創建并加載一個 HTTP Live Stream(HLS)格式的資源來播放時,可以按照下面幾步來做:
- 基于資源的 URL 初始化一個
AVPlayerItem
實例,因為你無法直接創建一個AVAsset
來表示 HLS 資源。 - 當你將
AVPlayerItem
和AVPlayer
實例關聯起來后,他就開始為播放做準備,當一切就緒時AVPlayerItem
會創建出AVAsset
和AVAssetTrack
實例以用來對接 HLS 視頻流的音視頻內容。 - 要獲取視頻流的時長,你需要 KVO 監測
AVPlayerItem
的duration
屬性,當資源可以播放時,它會被更新為正確的值。
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 監測AVPlayer
的status
屬性來看它何時可以播放。 - 如果上述嘗試都失敗,那就清理掉
AVPlayerItem
。
播放一個 AVPlayerItem
調用 AVPlayer
的 play
接口即可開始播放。
- (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
來順序播放多個 AVPlayerItem
。AVQueuePlayer
是 AVPlayer
的子類。
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。
- 當播放遠程媒體資源(比如網絡視頻)時,監測
AVPlayerItem
的loadedTimeRanges
和seekableTimeRanges
可以知道可以播放和 seek 的資源時長。 - 當播放 HTTP Live Stream 時,播放器的
currentItem
可能發生變化。 - 當播放 HTTP Live Stream 時,
AVPlayerItem
的tracks
可能發生變化。這種情況可能發生在播放流切換了編碼。 - 當播放失敗時,
AVPlayer
或AVPlayerItem
的status
可能發生變化。
響應 status 屬性的變化
通過 KVO 監測 AVPlayer
和正在播放的 AVPlayerItem
的 status
屬性,可以獲得對應的通知,比如當播放出現錯誤時,你可能會收到 AVPlayerStatusFailed
或 AVPlayerItemStatusFailed
通知,這時你就可以做相應的處理。
需要注意的是,由于 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 樹中去展示給用戶。
追蹤播放時間變化
我們可以使用 AVPlayer
的 addPeriodicTimeObserverForInterval: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];
}