本章译自Apple官方文档AVFoundation Programming Guide, 是AVFoundation系列译文第三篇, 介绍了AVFoundation框架中对资源进行播放的相关内容, 全部译文参见我的GitBook: AVFoundation编程指南.
AVPlayer
对象可以控制资源的播放, 在播放过程中, 可以使用AVPlayerItem
对象管理资源的播放状态, AVPlayerItemTrack
对象则可以用来管理一个独立轨道的播放状态. 要展示一段视频, 可以使用AVPlayerLayer
对象.
播放Asset
AVPlayer
对象是一个用来管理资源播放的控制中心, 比如控制播放的开始和停止, 定位到一个指定的时间点进行播放等等. 使用单个AVPlayer
对象来播放单个资源, 使用AVQueuePlayer
对象顺序播放多个资源(AVQueuePlayer
是AVPlayer
的子类). 在OS X上, 还可以使用AVKit框架中的AVPlayerView
类在view上直接播放一段内容.
AVPlayer
对象提供了资源播放状态相关的信息, 你可以根据播放状态的变化来同步更新UI. 通常会将一个AVPlayer
对象输出到一个特定的Core Animation layer
上进行展示(AVPlayerLayer
对象或AVSynchronizedLayer
对象). 更多layer相关的信息, 参考 Core Animation Programming Guide.
可以使用一个
AVPlayer
对象创建多个AVPlayerLayer
实例, 但是最后一个被创建的layer才会在屏幕上展示播放的内容.
虽然我们的最终目的是播放某个资源, 但并不需要直接向AVPlayer
对象提供要播放的资源, 而是提供一个AVPlayerItem
对象. 一个player item管理与其相绑定的资源(asset)的呈现状态. 个player item包含多个player item tracks( AVPlayerItemTrack
对象). 这些变量直接的关系如下图:
这种抽象意味着你可以使用不同的player来播放给定的asset, 但是每个player又有各自的渲染方式. 下图展示了其中一种可能性, 两个播放器以不同的设置来播放同一个asset. 通过使用item tracks, 可以在播放过程中禁用某些track(比如禁用音频track,即静音).
可以使用已存在的asset对player进行初始化, 也可以直接使用URL初始化以便于用来播放某个指定位置的资源. 使用AVAsset
时要注意, 初始化一个player item并不意味着就可以直接播放了. 你需要使用KVO监听item的status
属性来判断是否为可播放状态.
处理不同类型的Asset
asset的播放配置取决于asset的类型. 总的来说, asset有两个主要类型:基于文件的asset(file-based,比如本地文件),以及基于流的asset(stream-based, 比如HLS流媒体).
要播放一个file-based asset, 有以下一些步骤:
- 创建一个
AVURLAsset
- 根据asset创建一个
AVPlayerItem
对象 - 将item关联到一个AVPlayer对象上
- 使用KVO监听item的
status
属性, 当播放准备就绪后,开始播放
要播放一个HTTP流媒体, 使用URL初始化一个AVPlayerItem
对象(不能直接创建一个AVAsset对象用来代表HTTP直播流媒体).
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];
当把一个player item关联到一个player对象时, player就开始准备播放了. 当准备就绪后, player item会创建AVAsset
和AVAssetTrack
对象.
要获取流媒体的时长, 可以监听player item的duration
属性.
如果只需要简单的播放一个直播流, 可以更快捷的使用URL来创建player:
self.player = [AVPlayer playerWithURL:<#Live stream URL#>];
[player addObserver:self forKeyPath:@"status" options:0 context:&PlayerStatusContext];
与使用AVAsset和AVPlayerItem相同, 初始化一个player并不意味了就可以直接播放了, 还是需要监听player的status
属性, 当其变为 AVPlayerStatusReadyToPlay
时才能开始播放. 也可以监听 currentItem
属性来访问为流媒体创建的AVPlayerItem.
如果不确定是何种类型的URL, 参照以下步骤:
- 尝试根据URL初始化一个
AVURLAsset
对象, 然后加载其tracks
属性. - 如果上一步失败, 直接根据URL创建AVPlayerItem对象, 监听player的status属性判断是否可以播放
播放单个AVPlayerItem
可以向player发送play
消息来开始播放:
- (IBAction)play:sender {
[player play];
}
除了简单的播放, 你还可以控制一些播放的细节. 比如播放速率和播放状态.
播放速率
可以通过设置player的rate
属性改变播放速率:
aPlayer.rate = 0.5;
aPlayer.rate = 2.0;
rate为1.0表示正常速率, 设置为0表示暂停播放(同pause
).
可以将rate设置为一个负值来进行倒放. 通过playerItem的以下属性可以判断是否支持倒放:canPlayReverse
(支持rate -1.0), canPlaySlowReverse
(支持rate - 1.0 到 0.0), canPlayFastReverse
(支持rate 小于 -1.0).
定位播放点
要定位到指定时间点进行播放, 可以使用 seekToTime:如下:
CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn];
seekToTime:
方法牺牲了精确度来优化性能, 如果需要精确定位, 使用 seekToTime:toleranceBefore:toleranceAfter:方法.
CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
将容忍值设置为0意味着框架会解码大量的数据来保证精确度. 确保只有在真正需要的时候才会如此设置.
播放完毕之后, 播放点会被设置为item的结束点. 要将播放点重新设置到item开头, 可以注册接收 AVPlayerItemDidPlayToEndTimeNotification
通知, 在处理通知的方法中, 调用seekToTime:
并传入参数kCMTimeZero.
// 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
对象顺序播放多个AVPlayerItem. AVQueuePlayer
类是AVPlayer
的一个子类. 可以使用一个 player items的数组来初始化一个queue player:
NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];
接下来可以像使用AVPlayer对象一样发送play
消息开始播放player items. queue player依次播放每个item, 如果需要跳到下一个item, 可以向queue player发送advanceToNextItem
消息.
可以使用 insertItem:afterItem:, removeItem:
以及removeAllItems
对播放队列进行操作. 添加一个新的item时, 需要使用canInsertItem:afterItem:
第二个参数可以传递nil
用来判断该item是否可以被添加到队列中.
AVPlayerItem *anItem = <#Get a player item#>;
if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
[queuePlayer insertItem:anItem afterItem:nil];
}
监听播放
可以监听包括播放状态以及正在被播放的player item在内的许多方面的信息. 当状态变化不可控时, 这一点十分有用. 例如:
- 当用户切换的其他应用时, player的rate将会降至0.0
- 如果正在播放远程媒体, player的
loadedTimeRanges
和loadedTimeRanges
属性将会随着数据的加载而变化 - 播放HTTP流媒体时, 一个player的
currentItem
属性会被自动创建. - 播放HTTP流媒体时, 一个player的
tracks
属性可能会随着播放而变化 - 由于某些原因导致播放失败时, 一个player或者item的
status
属性会随之变化
可以使用KVO来监听这些属性的变化.
响应播放状态的改变
当一个player或者item的状态发生改变时, 会发出对应KVO变化的notification. 如果一个资源因为某些原因不能播放, status会变为 AVPlayerStatusFailed
或者 AVPlayerItemStatusFailed
. 这种情况下, error
对象会包含相关的错误信息.
AV Foundation并不会指定notification发出的线程. 如果需要更新UI, 你必须手动切换到主线程. 下面是使用 dispatch_async
切换到主线程的示例代码:
- (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
属性判断当前是否可以展示可视化的内容. 特别是当你只需要在有可视化内容时才将layer插入到视图层级中的情况.
跟踪播放时间
要跟踪AVPlayer对象当前的播放位置, 可以使用 addPeriodicTimeObserverForInterval:queue:usingBlock:
或者addBoundaryTimeObserverForTimes:queue:usingBlock:
.
当你需要进行更新播放时间, 或者其他UI同步操作时可能会用到这些方法.
addPeriodicTimeObserverForInterval:queue:usingBlock:
, block会在指定的时间间隔被调用, 哪怕时间发生了跳动,播放开始或结束.addBoundaryTimeObserverForTimes:queue:usingBlock:
, 传递一个包含了CMTime
的NSValue
对象的数组. 当播放到数组中的时间点时, block会被调用.
这两个方法都会返回一个不可见的观察者对象, 必须对其保持强引用. 当需要注销这个观察者时, 可以使用removeTimeObserver:
.
对于这两个方法, AV Foundation并不能确保blcok都会被按时调用. 如果有上一个block尚未执行完毕, 那么本次将不会调用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
通知来监听 player item 的结束.
[[NSNotificationCenter defaultCenter] addObserver:<#The observer, typically self#>
selector:@selector(<#The selector name#>)
name:AVPlayerItemDidPlayToEndTimeNotification
object:<#A player item#>];
使用AVPlayerLayer播放视频文件
下面的代码简要展示了如何使用AVPlayer
对象播放视频文件, 包括:
- 使用
AVPlayerLayer
- 创建
AVPlayer
对象 - 创建基于file-based asset的
AVPlayerItem
对象,并监听其状态 - 响应播放状态的变化, 同步改变播放按钮的可用状态
- 播放item
提示: 为了展示核心代码, 这份示例省略了某些内容, 比如内存管理和通知的移除等. 使用AV Foundation之前, 你最好已经拥有Cocoa框架的使用经验.
Player View
要播放一个asset的可视部分, 你需要一个包含 AVPlayerLayer
对象的view, 用来接收AVPlayer
对象的输出. 可以简单的定义一个UIView的子类来实现这一功能:
#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
View Controller
假设有一个简单的View Controller 声明如下:
@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
syncUI
方法用来根据player的状态同步按钮的可用状态:
- (void)syncUI {
if ((self.player.currentItem != nil) &&
([self.player.currentItem status] == AVPlayerItemStatusReadyToPlay)) {
self.playButton.enabled = YES;
}
else {
self.playButton.enabled = NO;
}
}
可以在view controller的viewDidLoad
方法中就调用syncUI
同步按钮的状态:
- (void)viewDidLoad {
[super viewDidLoad];
[self syncUI];
}
创建Asset
使用 AVURLAsset
根据URL创建asset.(下面的代码假设项目中包含了一个视频资源)
- (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.
}];
}
在completion block中创建 AVPlayerItem
, 并设置player view的player.
// Define this constant for the key-value observation context.
static const NSString *ItemStatusContext;
// Completion handler block.
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]);
}
});
响应Player Item的状态改变
- (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;
}
播放Item
- (IBAction)play:sender {
[player play];
}
item只被播放一次, 播放结束后, 播放点会被设置为item的结束点, 这样下一次调用play方法将会失效. 要将播放点设置到item的起始处,参考如下代码:
// Register with the notification center after creating the player item.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[self.player currentItem]];
- (void)playerItemDidReachEnd:(NSNotification *)notification {
[self.player seekToTime:kCMTimeZero];
}