本文轉自:AVAudioFoundation(2):音視頻播放 | www.samirchen.comhtml
本文主要內容來自 AVFoundation Programming Guide。ios
要播放 AVAsset
可使用 AVPlayer
。在播放期間,可使用一個 AVPlayerItem
實例來管理 asset 的總體的播放狀態,使用 AVPlayerItemTrack
來管理各個 track 的播放狀態。對於視頻的渲染,使用 AVPlayerLayer
來處理。vim
AVPlayer
是一個控制 asset 播放的控制器,它的功能包括:開始播放、中止播放、seek 等等。你可使用 AVPlayer
來播放單個 asset。若是你想播放一組 asset,你可使用 AVQueuePlayer
,AVQueuePlayer
是 AVPlayer
的子類。數組
AVPlayer
也會提供當前的播放狀態,這樣咱們就能夠根據當前的播放狀態調整交互。咱們須要將 AVPlayer
的畫面輸出到一個特定的 Core Animation Layer 上,一般是一個 AVPlayerLayer
或 AVSynchronizedLayer
實例。網絡
須要注意的是,你能夠從一個 AVPlayer
實例建立多個 AVPlayerLayer
對象,可是隻有最新建立的那個纔會渲染畫面到屏幕。app
對於 AVPlayer
來講,雖然最終播放的是 asset,可是咱們並不直接提供一個 AVAsset
給它,而是提供一個 AVPlayerItem
實例。AVPlayerItem
是用來管理與之關聯的 asset 的播放狀態的,一個 AVPlayerItem
包含了一組 AVPlayerItemTrack
實例,對應着 asset 中的音視頻軌道。它們直接的關係大體以下圖所示:iphone
注意:該圖的原圖是蘋果官方文檔上的,可是原圖是有錯的,把 AVPlayerItemTrack
所屬的框標成了 AVAsset
,這裏作了修正。async
這種實現方式就意味着,咱們能夠用多個播放器同時播放一個 asset,而且各個播放器可使用不一樣的模式來渲染。下圖就展現了一種用兩個不一樣的 AVPlayer
採用不一樣的設置播放同一個 AVAsset
的場景。在播放中,還能夠禁掉某些 track 的播放。ide
咱們能夠經過網絡來加載 asset,一般簡單的初始化 AVPlayerItem
後並不意味着它就直接能播放,因此咱們能夠 KVO AVPlayerItem
的 status
屬性來監聽它是否已經可播再決定後續的行爲。性能
咱們配置 asset 來播放的方式多多少少會依賴 asset 的類型,通常咱們有兩種不一樣類型的 asset:
加載基於文件的 asset 通常分爲以下幾步:
AVURLAsset
實例。AVURLAsset
實例建立 AVPlayerItem
實例。AVPlayerItem
實例與一個 AVPlayer
實例關聯。AVPlayerItem
的 status
屬性來等待其已經可播,即加載完成。建立並加載一個 HTTP Live Stream(HLS)格式的資源來播放時,能夠按照下面幾步來作:
AVPlayerItem
實例,由於你沒法直接建立一個 AVAsset
來表示 HLS 資源。AVPlayerItem
和 AVPlayer
實例關聯起來後,他就開始爲播放作準備,當一切就緒時 AVPlayerItem
會建立出 AVAsset
和 AVAssetTrack
實例以用來對接 HLS 視頻流的音視頻內容。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 時,你能夠這樣作:
AVURLAsset
,並加載它的 tracks
屬性。若是 tracks
屬性加載成功,就基於 asset 來建立一個 AVPlayerItem
實例。tracks
屬性加載失敗,那麼就直接基於 URL 建立一個 AVPlayerItem
實例,並 KVO 監測 AVPlayer
的 status
屬性來看它什麼時候能夠播放。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
表示播放結束後會暫停。
咱們能夠用 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 是頗有用的,好比:
AVPlayerItem
的 loadedTimeRanges
和 seekableTimeRanges
能夠知道能夠播放和 seek 的資源時長。currentItem
可能發生變化。AVPlayerItem
的 tracks
可能發生變化。這種狀況可能發生在播放流切換了編碼。AVPlayer
或 AVPlayerItem
的 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
實例。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]; }