原文:AVFoundation Programming Guide
写在前面
简单翻译一下
AVFoundation
的相关文档,本人英语水平一般,有不对的欢迎大家留言指正。
播放
可以使用AVPlayer
对象来控制assets
的播放。在播放中,你可以使用AVPlayerItem实例来管理整个asset
的呈现状态;使用AVPlayerItemTrack对象来管理一个独立的track
的呈现状态。你可以使用AVPlayerLayer对象来显示视频。
播放Assets
一个player是一个控制器对象,你可以使用它来管理asset
的播放,例如控制开始和结束以及查找一个指定的时间位置。你使用一个AVPlayer实例来播放一个单独的asset
。你可以使用一个AVQueuePlayer(AVQueuePlayer
是AVPlayer
子类)对象来播放一系列的项目。在OS X上你可以选择使用AVKit framework
的AVPlayerView
。
一个player
为你提供了有关播放状态的信息。因此,如果你需要的话,你可以根据player的状态来同步的处理你的用户界面。你通常会直接将一个player输出到一个特殊的核心动画层(一个AVPlayerLayer对象或AVSynchronizedLayer对象)。更多的可以参考Core Animation Programming Guide
多播放层:你可以给一个单独的AVPlayer实例创建多个AVPlayerLayer对象,但是只有最后一个创建的才会显示视频内容。
尽管最终你想播放一个asset,但你不需要给AVPlayer对象直接提供assets。你需要提供一个AVPlayerItem对象实例。一个player item
管理他所包含的asset的呈现状态。一个player item
包含player item tracks
(AVPlayerItemTrack实例)它们对应了asset中的tracks。关系可参考Figure 2-1
这代表你可以通过不同的player同时的播放一个给定的asset,在每一个player中呈现不同的效果。Figure 2-2展示了一种可能性,两个不同的player通过不同的设置来播放同一个asset。使用item tracks,你可以在播放中禁用一个特殊的track(如你可能不想播放声音。)
你可以使用存在的asset来初始化一个player item,或者你也可以使用一个URL来初始。同AVAsset一样,简单的初始化一个player item并不意味着它们可以播放了。你可以检测(使用KVO)item的状态属性status来判断它们是否可以播放的。
操作不同类型的Asset
你怎么配置一个asset的播放取决于你想播放的asset类型。一般的说,主要有两种类型:基于文件的assets,那些你可以随意访问的(如本地相册或者媒体库中的本地文件),和基于流的assets(HLS格式的)。
加载和播放本地asset。步骤如下:
- 使用AVURLAsset创建asset。
- 使用asset创建一个
AVPlayerItem
对象实例。 - 关联player item到一个
AVPlayer
对象。 - 等待item的状态表明它是可以播放的(使用KVO来监听状态变化)
可以参照Putting It All Together: Playing a Video File Using AVPlayerLayer.
创建和准备播放HLS。可以使用URL初始化一个AVPlayerItem
的实例。(你不能直接创建一个AVAsset实例来表示HLS中的媒体对象。)
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 item会创建AVAsset
和AVAssetTrack
实例,你可以用来检查直播流的内容。
你可以监听player item的duration属性来得到流对象的时长。当item可以播放的时候,这个属性会更新成正确的值。
注意:使用duration
属性需要iOS 4.3或者更高的系统才可以使用。一个在所有iOS版本中都支持的方法是监听player item的status属性。当status变为AVPlayerItemStatusReadyToPlay的时候,你可以使用下面代码获得它的时长。
[[[[[playerItem tracks] objectAtIndex:0] assetTrack] asset] duration];
如果你只是想简单的播放直播流,你可以使用一个快捷的方法来直接通过一个URL来创建player 代码如下:
self.player = [AVPlayer playerWithURL:<#Live stream URL#>];
[player addObserver:self forKeyPath:@"status" options:0 context:&PlayerStatusContext];
使用assets和items来初始化player都不意味着他们是可以播放的。你应该监听player的status属性,当它变为AVPlayerStatusReadyToPlay的时候就表明它是可以播放的了。你也可以监听currentItem属性来访问通过流创建的player item。
如果你不知道你使用的URL是那种类型的,你可以使用如下步骤:
1.尝试通过URL来创建AVURLAsset
,然后加载它的tracks。
如果tracks加载成功的话,你就可以使用这个asset来创建一个player item了。
2.如果1失败了,直接使用URL来创建AVPlayerItem
。
监听player的status属性来判断他是否是可以播放的。
如果任何一个成功了,你就可以结束判断然后将palyer item关联到player上了。
播放Item
你可以通过发送一个play的消息来开始播放。
- (IBAction)play:sender {
[player play];
}
除了简单的播放外,你还可以管理播放的其他方面,如播放速率和播放头的位置。你也可以检测player的播放状态;如果你需要的话,这是很有用的,如根据asset的状态同步改变用户界面的呈现状态,可以参考Monitoring Playback。
改变播放速率
你可以通过设置player的rate属性来改变播放速率。
aPlayer.rate = 0.5;
aPlayer.rate = 2.0;
1.0表示”以当前Item的正常速率来播放“。设置rate为0.0相当于暂停播放。你当然也可以使用pause方法。
items支持逆向播放,你可以通过设置rate属性为负数来实现。你可以使用playerItem的canPlayReverse(支持设置rate为-1.0),canPlaySlowReverse(支持设置rate为0.0~-1.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公差可能会使framework解码大量的数据。你应该只有当你需要使用的时候再使用。例如:编写一个复杂的媒体编辑程序需要精确的控制。
播放完成后,player的播放头会被设置到item的末尾,此时再次调用play方法会没有任何反应。你需要将播放头重新设置到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];
}
播放多个Items
你可以使用AVQueuePlayer对象来序列化的播放一组items。AVQueuePlayer
类是AVPlayer
的子类。你可以使用一组player items来初始化一个queue player
。
NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];
你可以使用play方法来播放AVQueuePlayer,就像一个AVPlayer
一样。队列会依次播放每一个item。如果你想跳到下一个item,你可以发送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的呈现状态和播放中的player item。当状态的改变不是在你直接控制下的时候,这是非常有用的。如:
- 当用户使用多任务来切换不同的应用的时候,player的rate属性会调整为0.0。
- 如果你正在播放远程媒体,player item的loadedTimeRanges和seekableTimeRanges属性会随着数据的增加而变化。
这些属性告诉你player item的时间轴中哪部分是可以使用的。 - player的currentItem属性会改变,当使用HTTP直播流创建一个player item的时候。
- player item的tracks属性在播放一个HTTP直播流的时候可能会改变。
这个可能发生在播放流支持不同的编码的时候;当选择不同的编码的时候tracks会发生变化。 - player或者player item的状态属性status当因为某些原因播放失败的时候会发生变化。
你可以使用KVO来监测这些属性的变化。
重要事项:
你应该在主线程中注册和注销KVO变化通知。这个避免了接收部分通知如果这个改变发生在其他线程中。AV Foundation会在主线程中触发observeValueForKeyPath:ofObject:change:context: 方法,即使这些变化发生在其他线程。
响应状态变化
当player或者player item的状态发生变化的时候,它发出了一个KVO变化的通知。如果一个对象由于某些原因不能播放(如媒体服务被重置),状态会变成AVPlayerStatusFailed或者AVPlayerItemStatusFailed。在这种情况下,这个对象的error属性会变为描述对象为什么不能播放的error对象。
AV Foundation 并不指定发出通知的线程。如果你想更新用户的界面,你必须确保相关的代码在主线程中被执行。下面的代码使用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;
}
Tracking Readiness for Visual Display
你可以观察AVPlayerLayer对象的readyForDisplay属性,这个属性当layer有用户可以观看的内容的时候会发生变化。通常,你应该只有当有内容可以供用户观看的时候才将player的layer插入到layer层中。
跟踪时间
你可以使用addPeriodicTimeObserverForInterval:queue:usingBlock或者addBoundaryTimeObserverForTimes:queue:usingBlock:来跟踪AVPlayer对象的播放头位置的变化。你可以更新用户界面的信息如已经播放的时间和剩余的时间,或者做一下其他的用户界面的同步处理。
addPeriodicTimeObserverForInterval:queue:usingBlock中的block会在你提供的特殊间隔中调用,如时间跳跃,播放开始或结束的时候。
addBoundaryTimeObserverForTimes:queue:usingBlock:方法中你传递一组
CMTime
,它的block会在这些时间经过的时候触发。
上面两个方法都返回一个不透明对象可以作为一个观察者。你必须对返回值保持一个强引用直到你想要player触发block的时间。你必须相应的调用removeTimeObserver:方法。
对于这些方法, AV Foundation并不能保证在每一个时间间隔或者边界都调用相应的block。AV Foundation不会在前一个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);
}];
播放结束
Item在播放完成的时候会发生一个AVPlayerItemDidPlayToEndTimeNotification通知。
[[NSNotificationCenter defaultCenter] addObserver:<#The observer, typically self#>
selector:@selector(<#The selector name#>)
name:AVPlayerItemDidPlayToEndTimeNotification
object:<#A player item#>];
使用AVPlayerLayer播放视频文件
这个简单的代码示例说明了如何使用一个AVPlayer对象播放一个视频文件。步骤如下:
- 使用
AVPlayerLayer
layer配置view - 创建一个AVPlayer对象
- 使用基于文件的asset创建一个AVPlayerItem对象,并且使用KVC监听status属性
- 当item可以播放的时候激活一个button
- 播放item,然后重置到开始位置
注意:为了关注最主要的代码,这个示例忽略了一些东西,如内存管理、注销一个观察者等。为了使用AV Foundation,你需要对使用Cocoa框架有丰富的经验来推断出忽略的代码。关于playback的概念可以参考Playing Assets。
The Player View
为了播放一个asset,你需要有一个view来包含一个AVPlayerLayer层来输出,你可以简单的使用一个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
A Simple View Controller
假设你有一个简单的ViewController,如下:
@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的状态来同步改变button的状态。
- (void)syncUI {
if ((self.player.currentItem != nil) &&
([self.player.currentItem status] == AVPlayerItemStatusReadyToPlay)) {
self.playButton.enabled = YES;
}
else {
self.playButton.enabled = NO;
}
}
你可以在viewDidLoad中调用syncUI方法来确保在view第一次显示的时候用户界面的一致性。
- (void)viewDidLoad {
[super viewDidLoad];
[self syncUI];
}
其他的一些属性和方法在剩下的内容中介绍。
Creating the 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.
}];
}
在完成的block中你可以创建一个AVPlayerItem对象,并将它设置到player view上。简单的创建一个asset并不意味着它是可用的。为了判断什么时候它是可用的,你可以监听item的status属性。
// 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的状态变化
当player item的状态发生变化的时候,view controller接收到一个KVO变化的通知。AV Foundation并不指定发送消息的线程。如果你需要更新UI,你必须确保你的方法是在主线程中被执行的。这个示例使用了dispatch_async来实现在主线程中更新UI。
- (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;
}
Playing the Item
- (IBAction)play:sender {
[player play];
}
这个item只会播放一次。在播放完成后,player的播放头被设置到了item的末尾,以后再次调用play方法都不会再有作用了。可以重置播放头到item的开始位置,你可以以通过接收AVPlayerItemDidPlayToEndTimeNotification
来处理。在通知的回调方法中,调用seekToTime:
方法,参数设置为kCMTimeZero
.
// 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];
}