本文内容主要来源:AVFoundation Programming Guide
要播放 AVAsset 可以用 AVPlayer 对象。播放期间,可以使用 AVPlayerItem 对象来管理 Asset 的整体播放状态,以及 AVPlayerItemTrack 对象来管理各个 track 的播放状态。 用 AVPlayerLayer 来对视频渲染。
播放 AVAssets
AVPlayer 是一个控制 Asset 播放的控制器。比如:启动和停止播放,以及寻找具体时间。可以使用 AVPlayer 来播放单个 Asset,可以使用 AVQueuePlayer(AVPlayer 子类) 来播放一组 Asset。在OS X上,您可以选择使用 AVKit 框架的 AVPlayerView类在视图中播放内容。
AVPlayer 也会提供当前的播放状态。因此,可以根据当前播放状态调整交互。我们需要将AVPlayer的画面输出到一个特定的Core Animation Layer 上,通常是一个AVPlayerLayer或AVSynchronizedLayer实例。
多播放器层:可以从单个AVPlayer实例创建许多AVPlayerLayer对象,但只有最新创建的图层才会渲染画面到屏幕上。
对于AVPlayer来说,虽然最终播放的是 asset,但是我们并不直接提供一个AVAsset给它,而是提供一个AVPlayerItem实例。AVPlayerItem是用来管理与之关联的 asset 的播放状态的,一个AVPlayerItem包含了一组AVPlayerItemTrack实例,对应着 asset 中的音视频轨道。它们直接的关系大致如下图所示:

这种实现方式就意味着,我们可以用多个播放器同时播放一个 asset,并且各个播放器可以使用不同的模式来渲染。下图就展示了一种用两个不同的AVPlayer采用不同的设置播放同一个AVAsset的场景。在播放中,还可以禁掉某些 track 的播放。

可以通过网络来加载 asset,通常简单的初始化AVPlayerItem后并不意味着它就直接能播放,所以我们可以 KVOAVPlayerItem的status属性来监听它是否及何时可以播放。
处理不同类型的 Asset
我们配置 Asset 来播放的方式可能取决于 asset 的类型。一般有两种类型:
- 基于文件的 asset,可以随时访问,比如:来自本地文件、相册卷或媒体库等
- 流式 asset,比如:HTLS(HTTP Live Streaming)格式
加载和播放基于文件的 asset,一般分为以下几个步骤:
- 使用 AVURLAsset 创建 asset
- 使用 asset 创建 AVPlayerItem
- 将该 item 与 AVPlayer 相关联
- KVO 监听该 item 的状态属性,来指示它已经准备好播放
创建和准备一个HTTP直播流进行播放,可以按照以下步骤:
- 使用 URL 来初始化一个 AVPlayerItem 实例,不能直接创建一个 AVAsset 实例来表示 HLS(HTTP Live Stream)中的媒体。
- 当你将 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];
与 AVAsset 和 AVPlayerItem 一样,初始化 CAPlayer 并不意味着它已准备好播放。 KVO 监听 CAPlayer 的 status 属性,当AVPlayerStatusReadyToPlay 准备播放时,该属性会更改为 AVPlayerStatusReadyToPlay。 还可以监听 currentItem 属性来访问为流创建的播放器项。
如果你不知道 URL 对应的是什么类型的 Asset,按如下要求做:
- 尝试使用 URL 来初始化 CAURLAsset,然后加载其
tracks属性,如果 tracks 加载成功,则为 asset 创建一个 AVPlayerItem 实例- 如果 tracks 属性加载失败,则直接从 URL 创建 CAPlayerItem,并 KVO 监听 CAPlayer 的 status 属性,来确定它是否可以播放
- 如果上述全部失败,就清除 AVPlayerItem
播放一个 CAPlayerItem
要开始播放,向 AVPlayer 发送一个 play 消息:
- (IBAction)play:sender {
[player play];
}
除了简单的播放,还可以通过设置 rate 属性来更改播放速度:
aPlayer.rate = 0.5;
aPlayer.rate = 2.0;
值为 1.0 表示正常播放。 为 0.0 表示暂停(等同调用 pause 效果)。
除了正向播放,有的视频还能反向播放,不过需要检查几个属性:
canPlayReverse:支持值为 -1.0canPlaySlowReverse:支持0.0和1.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];
}
播放多个 AVPlayerItem
我们可以使用 AVQueuePlayer(AVPlayer 的子类)来顺序播放多个 AVPlayerItem 。
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
- 如果正在播放远程媒体,监测
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 树中去展示给用户。
跟踪播放时间变化
我们可以使用 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 通知。
[[NSNotificationCenter defaultCenter] addObserver:<#The observer, typically self#>
selector:@selector(<#The selector name#>)
name:AVPlayerItemDidPlayToEndTimeNotification
object:<#A player item#>];
一个完整的实例
这个简短的代码示例说明了如何使用 AVPlayer 对象来播放视频文件。主要包括下面几个步骤:
- 配置使用 AVPlayerLayer 图层的 view
- 创建一个 AVPlayer 实例
基于文件类型的 asset 创建一个
AVPlayerItem实例,并用 KVO 检测其status属性响应
AVPlayerItem实例可以播放的通知,显示出一个按钮播放
AVPlayerItem并播放完成后将其播放位置调整到开始位置
The Player View
#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];
}