本文内容主要来源: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 上,通常是一个AVPlayerLayerAVSynchronizedLayer实例。

多播放器层:可以从单个AVPlayer实例创建许多AVPlayerLayer对象,但只有最新创建的图层才会渲染画面到屏幕上。

对于AVPlayer来说,虽然最终播放的是 asset,但是我们并不直接提供一个AVAsset给它,而是提供一个AVPlayerItem实例。AVPlayerItem是用来管理与之关联的 asset 的播放状态的,一个AVPlayerItem包含了一组AVPlayerItemTrack实例,对应着 asset 中的音视频轨道。它们直接的关系大致如下图所示:

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

可以通过网络来加载 asset,通常简单的初始化AVPlayerItem后并不意味着它就直接能播放,所以我们可以 KVOAVPlayerItemstatus属性来监听它是否及何时可以播放。

处理不同类型的 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会创建出AVAssetAVAssetTrack实例以用来对接 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 效果)。

除了正向播放,有的视频还能反向播放,不过需要检查几个属性:

可以通过 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
  • 如果正在播放远程媒体,监测 loadedTimeRangesseekableTimeRanges 属性可以知道何时播放和 seek 资源时长
  • 当播放 HTTP Live Stream 时,播放器的currentItem可能发生变化

  • 当播放 HTTP Live Stream 时,AVPlayerItemtracks可能发生变化。这种情况可能发生在播放流切换了编码

  • 当播放失败时,AVPlayerAVPlayerItemstatus可能发生变化

响应 status 属性的变化

通过 KVO 监测AVPlayer和正在播放的AVPlayerItemstatus属性,可以获得对应的通知。比如当播放出现错误时,你可能会收到 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];
}

results matching ""

    No results matching ""