0%

播放Assets

本章译自Apple官方文档AVFoundation Programming Guide, 是AVFoundation系列译文第三篇, 介绍了AVFoundation框架中对资源进行播放的相关内容, 全部译文参见我的GitBook: AVFoundation编程指南.

AVPlayer对象可以控制资源的播放, 在播放过程中, 可以使用AVPlayerItem对象管理资源的播放状态, AVPlayerItemTrack对象则可以用来管理一个独立轨道的播放状态. 要展示一段视频, 可以使用AVPlayerLayer 对象.

播放Asset

AVPlayer对象是一个用来管理资源播放的控制中心, 比如控制播放的开始和停止, 定位到一个指定的时间点进行播放等等. 使用单个AVPlayer对象来播放单个资源, 使用AVQueuePlayer对象顺序播放多个资源(AVQueuePlayerAVPlayer的子类). 在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对象). 这些变量直接的关系如下图:

Playing an asset

这种抽象意味着你可以使用不同的player来播放给定的asset, 但是每个player又有各自的渲染方式. 下图展示了其中一种可能性, 两个播放器以不同的设置来播放同一个asset. 通过使用item tracks, 可以在播放过程中禁用某些track(比如禁用音频track,即静音).

Playing the same asset in different ways

可以使用已存在的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会创建AVAssetAVAssetTrack对象.

要获取流媒体的时长, 可以监听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, 参照以下步骤:

  1. 尝试根据URL初始化一个AVURLAsset对象, 然后加载其tracks属性.
  2. 如果上一步失败, 直接根据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的loadedTimeRangesloadedTimeRanges属性将会随着数据的加载而变化
  • 播放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:, 传递一个包含了CMTimeNSValue对象的数组. 当播放到数组中的时间点时, 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];
}