AVFoundation Programming Guide -- Playback

为了控制 asset 的播放,你要使用 AVPlayer 对象。在播放期间,你可以使用 AVPlayerItem 实例管理 asset 的整体呈现状态,使用 AVPlayerItemTrack 对象管理独立轨道的呈现状态。为了播放视频,你使用 AVPlayerLayer 对象

播放 Asset

一个播放器是你用来管理 asset 播放的控制器, 例如开始,停止和跳转到特定的时间点。你使用 AVPlayer 对象去播放单个 asset。 也可以使用
AVQueuePlayer 对象播放一系列的 asset (AVQueuePlayer 是AVPlayer 的子类)。在OS X上,您可以选择使用AVKit框架的AVPlayerView类来播放视图中的内容。

播放器提供有关播放状态的信息,因此,如果需要,可以将用户界面与播放器的状态同步。通常将播放器的输出引导 Core Animation layer(AVPlayerLayerAVSynchronizedLayer 的实例,AVPlayerLayer和
AVSynchronizedLayer 都是CALayer的子类,属于 Core Animation)。要了解更多关于图层的信息,参考: Core Animation Programming Guide

多个播放器测layer:可以从单个 AVPlayer 实例创建多个 AVPlayerLayer 对象,但是只有最近创建的层才能在屏幕上显示视频内容。

虽然你想播放一个 asset,但是不能直接提供 asset 给 AVPlayer 对象。要提供一个 AVPlayerItem 实例。player item 管理与其相关联的asset 的呈现状态。player item 包含了轨道——AVPlayerItemTrack 实例——对应于 asset 中的轨道。各种对象之间的关系如图2-1所示。

  • Figure 2-1 Playing an asset


    image.png

这个抽象意味着你可以同时使用不同的 AVPlayer 对象播放一个特定的 asset,但是每个 AVPlayer 对象都以不同的方式呈现。Figure 2-2 展示以一种可能性,两个不同的 AVPlayer 对象使用不同的设置播放同一个 asset,例如使用 AVPlayerItem 你可以在播放过程中禁用特定的 tracks(例如,你可能不想播放声音组件)。

  • Figure 2-2 以不同的方式播放同一个 asset
image.png

你可以使用已存在的 asset 初始化 AVPlayerItem,或者也可以直接通过一个URL初始化 AVPlayerItem以便在特定位置播放资源(AVPlayerItem 稍后将会为资源创建并且配置一个 asset 对象)。与 Asset 一样简单地初始化 AVPlayerItem 并不一定意味着它可以立即播放。您可以使用KVO观察一个 AVPlayerItem 对象的status,以确定是否以及何时准备播放。

处理不同类型的 Asset

配置 asset 进行播放的方式可能取决于您想要播放的 asset 种类。大体上讲,有两种主要的类型:基于文件的 asset,你可以随机访问(例如本地文件,相机胶卷或媒体库)和基于流的 asset (HTTP Live Streaming格式)。
为了加载并播放一个基于文件的 asset。需要以下几步:

  • 使用AVURLAsset 创建一个 asset
  • 使用 asset 创建一个 AVPlayerItem 实例
  • 将 AVPlayerItem 实例与 AVPlayer 实例关联
  • 等待 AVPlayerItem 的属性 status 指示注备好播放了(一般使用 KVO 接受 status 改变的通知)

Putting It All Together: Playing a Video File Using AVPlayerLayer 指示了这种方式

创建和准备 HTTP live stream 进行播放。使用 URL 初始化一个 AVPlayerItem 实例 (不能直接创建AVAsset实例来表示 HTTP Live Stream 中的媒体)??? 为什么不能:

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];

当你将这个 AVPlayerItem 实例与 AVPlayer 实例关联之后,它开始准备播放。当它准备好播放之后,AVPlayerItem 实例创建 AVAsset 和 AVAssetTrack 实例,可以用来播放 HTTP live stream。

要获取 HTTP live stream 的时长,你可以 KVO 观察 AVPlayerItem 的 duration 属性。当 AVPlayerItem 注备好播放的时候,这个属性更新为正确的值。

注意:使用 AVPlayerItem 的 duration 属性,要求 iOS 4.3 以上版本。与所有版本的iOS兼容的方法涉及观察AVPlayerItem 的 status 属性。当 status 变为 AVPlayerItemStatusReadyToPlay 的时候,可以通过如下代码获取 duration

[[[[[playerItem tracks] objectAtIndex:0] assetTrack] asset] duration];

如果你只是想简单的播放流媒体,则可以使用以下代码直接使用URL创建快捷方式并创建播放器:

self.player = [AVPlayer playerWithURL:<#Live stream URL#>];
[player addObserver:self forKeyPath:@"status" options:0 context:&PlayerStatusContext];

和 asset,AVPlayerItem 一样,初始化 player 并不意味着它可以立即播放。应该观察 player 的 status 属性,当变为
AVPlayerStatusReadyToPlay 的时候才可以播放。也可以通过观察 player 的 currentItem 属性获取为当前流创建的 AVPlayerItem 实例。
如果你不知道 URL 是哪种类型,遵循以下步骤:

  1. 尝试使用 URL 初始化 AVURLAsset ,然后获取他的 tracks,如果获取成功,那么你就可以为这个 asset 创建 AVPlayerItem 实例了。
  2. 如果1失败了,直接使用 URL 创建 AVPlayerItem 实例,观察属性 status 的变化以决定它是否可以播放了。

如果任何一个成功了,就可以将 AVPlayerItem 实例与 AVPlayer 实例绑定了。

播放一个 Item

为了开始播放,你要向 AVPlayer 实例发送一条 play 消息。

- (IBAction)play:sender {
    [player play];
}

除了简单的播放之外,还可以管理播放的各个方面,如播放的速率和位置。你可以监听 player 的播放状态,一般来说这对于同步播放状态与用户界面是非常有用的,参考:Monitoring Playback

改变播放速度

可以通过将更改 player 的 rate 属性来改变播放的速度:

aPlayer.rate = 0.5;
aPlayer.rate = 2.0;

值为1.0意味着正常速度播放,设置速度为 0.0 意味着暂定,也可以使用 pause 暂停

支持反向Items 可以将速度设置为负值使得播放回退。你可以通过使用playerItem属性 canPlayReverse(支持-1.0的速率值),canPlaySlowReverse(支持0.0到-1.0之间的速率)和 canPlayFastReverse(支持小于-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可能需要框架解码大量数据。一般只有在编写需要精确控制的复杂媒体编辑应用程序时,才应使用零。
播放完之后,player 的头部被设置到该 playerItem 的结尾并且进一步的播放调用不起作用。要将播放头放到该
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];
}

播放多个 Items

您可以使用 AVQueuePlayer 对象按顺序播放多个 items。AVQueuePlayer 是 AVPlayer 的子类。使用 player items 的数组初始化一个 AVQueuePlayer 实例

NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];

可以像 AVPlayer 对象一样使用 play 方法播放视频。 queue player 将依次播放每个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 和正在被播放的 player item 的多个方面。这对于不受你直接控制的状态更改非常有用。例如:

  • 如果用户使用多任务,切换到别的app,player 的 速度将会被置为 0.0
  • 如果播放一个远端媒体,player item的 loadedTimeRangesseekableTimeRanges 属性将随着更多数据可用而更改。这些属性告诉你player item的时间线的哪些部分是可用的。
  • player 的 currentItem 属性切换为一个为 HTTP live stream 创建的 player item
  • 当播放一个 HTTP live stream 的时候 player item的 tracks 可能会改变,如果流为内容提供了不同的编码,则可能发生这种情况;如果 player 切换到不同的编码,tracks 会改变。
  • 如果播放失败,player 或者 player item 的 status 属性会改变

你可以使用 KVO 监控这些属性值的改变

要在主线程注册和注销 KVO 监听通知。这样可以避免在另一个线程正在进行更改时收到部分通知的可能性。AV Foundation在主线程调用 observeValueForKeyPath:ofObject:change:context: ,即使是在其他线程发生的操作改变。

应对状态变化

当 player 或者 player item 的 status 变化的时候,将会触发 KVO 通知。如果一个对象因为一些原因不能播放了(例如媒体服务被重置了),status将根据情况变为 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;
}

监控视觉显示的准备状态

可以通过观察 AVPlayerLayer 对象的 readyForDisplay 属性,在 player 有用户可见内容时得到通知。特别是,只有当有东西供用户查看然后执行转换时,才可以将播放器图层插入到图层树中。

监控时间

要跟踪AVPlayer对象中播放头位置的变化,你可以使用addPeriodicTimeObserverForInterval:queue:usingBlock: 或者ddBoundaryTimeObserverForTimes:queue:usingBlock: 。例如可以使用有关已用时间或剩余时间的信息来更新用户界面,或者执行其他用户界面同步。

这两个方法都返回一个作为观察者的不透明对象。只要你希望 player 调用时间观察块,就必须保持对返回对象的强引用。想注销观察的时候需要调用 removeTimeObserver: 传递对应的 观察者。
使用这两种方法,AV Foundation不保证在每个间隔或边界通过时调用您的块。如果先前调用的块的执行尚未完成,则AV Foundation不调用块。因此,您必须确保您在该区域执行的工作不会对系统的负担过重。

// 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对象播放视频文件。如下:

  • 配置一个 view 使用 AVPlayerLayer 作为底层layer
  • 生成一个 AVPlayer 对象
  • 为基于文件的 asset 创建一个 AVPlayerItem 对象,然后使用 KVO 观察 item 的 status
  • 通过启用按钮能够点击来响应 item 准备好播放了
  • 播放 item 结束后将 player 的头部放置到开始处

为了专注相关代码,这个例子从完整实例中删除了几个方面,例如内存管理,注销 KVO 观察。为了使用 AV Foundation,希望你有足够的 cocoa 编程经验以便补足遗漏的代码

有关播放的概念介绍,查看 Playing Assets

播放视图

为了播放一个 asset 的可见部分,需要一个 AVPlayerLayer 层的视图作为 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

简单的视图控制器

假设你有一个简单的视图控制器,声明如下:

@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,以确保在首次显示视图时保持一致的用户界面:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self syncUI];
}

其他属性和方法在其余部分中进行了介绍。

创建一个 Asset

你可以利用 URL 创建一个 AVURLAsset 对象,(以下示例假定您的项目包含合适的视频资源):

- (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 一样,简单的创建了 player item 并不意味着它立即可用。为了确定是否准备好了,可以观察他的 status 属性。应该在将 player item 和 player 关联起来之前配置 layer item 的观察者。

当将 player item 和 player 关联起来的时候触发 player item 准备

// 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 status 的改变

当 player item 的 status 改变的时候, 视图控制器会收到 KVO 通知。AV Foundation不会指定通知发送的具体线程。如果你想更新用户界面,你必须确保相关代码是在主线程被执行。这个例子使用 dispatch_async 保证在主线程更新用户界面。

- (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

向 player 发送 play 消息来播放 item

- (IBAction)play:sender {
    [player play];
}

item 只播放一次。播放之后,player 的头部被设置为该 item 的结尾,并且进一步的播放方法的调用将不起作用。要将播放头放回到 item 的开头,你可以监听 item 的 AVPlayerItemDidPlayToEndTimeNotification

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容