[AVFoundation]播放

原文:AVFoundation Programming Guide

写在前面

简单翻译一下AVFoundation的相关文档,本人英语水平一般,有不对的欢迎大家留言指正。

播放

可以使用AVPlayer对象来控制assets的播放。在播放中,你可以使用AVPlayerItem实例来管理整个asset的呈现状态;使用AVPlayerItemTrack对象来管理一个独立的track的呈现状态。你可以使用AVPlayerLayer对象来显示视频。

播放Assets

一个player是一个控制器对象,你可以使用它来管理asset的播放,例如控制开始和结束以及查找一个指定的时间位置。你使用一个AVPlayer实例来播放一个单独的asset。你可以使用一个AVQueuePlayer(AVQueuePlayerAVPlayer子类)对象来播放一系列的项目。在OS X上你可以选择使用AVKit frameworkAVPlayerView

一个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

Figure 2-1 Playing an asset

这代表你可以通过不同的player同时的播放一个给定的asset,在每一个player中呈现不同的效果。Figure 2-2展示了一种可能性,两个不同的player通过不同的设置来播放同一个asset。使用item tracks,你可以在播放中禁用一个特殊的track(如你可能不想播放声音。)

Figure 2-2 Playing the same asset in different ways

你可以使用存在的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会创建AVAssetAVAssetTrack实例,你可以用来检查直播流的内容。

你可以监听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的loadedTimeRangesseekableTimeRanges属性会随着数据的增加而变化。
    这些属性告诉你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对象的播放头位置的变化。你可以更新用户界面的信息如已经播放的时间和剩余的时间,或者做一下其他的用户界面的同步处理。

上面两个方法都返回一个不透明对象可以作为一个观察者。你必须对返回值保持一个强引用直到你想要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对象播放一个视频文件。步骤如下:

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

推荐阅读更多精彩内容