本文主要是对视频播放器项目的介绍。本文主要实现了视频的播放和对播放情况的监听。
1. AVFoundation简介
播放视频苹果提供了非常强大的AVFoundation框架,几乎可以满足我们所有的需求,播放短视频仅仅需要几行代码就可以搞定。
#import "ViewController.h"
// 导入AVFoundation框架
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 播放视频的链接
NSString *strURL = @"https://www.apple.com/105/media/cn/home/2018/da585964_d062_4b1d_97d1_af34b440fe37/films/behind-the-mac/mac-behind-the-mac-tpl-cn_848x480.mp4";
NSURL *url = [NSURL URLWithString:strURL];
// 创建播放资源
AVURLAsset *asset = [AVURLAsset assetWithURL:url];
// 创建播放单元
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
// 创建播放器
AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
// 播放视图
AVPlayerLayer *avLayer = [AVPlayerLayer playerLayerWithPlayer:player];
avLayer.frame = self.view.bounds;
[self.view.layer addSublayer:avLayer];
// 播放视频
[player play];
}
@end
下文主要是介绍一下视频相关类的作用以及常用接口。
1.1 AVURLAsset:播放资源
它是AVFoundation的视频资源模型,提供媒体资源的不会随着视频播放变化的信息,例如视频的长度,格式等。虽然AVURLAsset是不可变的,但是它的属性却是异步加载的, 所以它的属性值并不是一直可用的,但是一旦可用了,值就不会再变了。它包含视频资源的音频、视频、字幕等。
1.2 AVPlayerItem:播放单元
包含媒体资源的动态信息。是否可以播放,播放进度,缓存进度,视频的尺寸,是否播放完,缓冲情况(可以正常播放还是网络情况不好)等。
// 通过一个asset来实例化AVPlayerItem对象,相当于调用[AVPlayerItem playerItemWithAsset:_asset automaticallyLoadedAssetKeys:@[@"duration"]];
+ (instancetype)playerItemWithAsset:(AVAsset *)asset;
// 创建一个AVPlayerItem,将任意属性集委托给该框架,就可以自动载入对应的属性,省去了loadValuesAsynchronouslyForKeys: completionHandler载入需要访问其他资源属性。
+ (instancetype)playerItemWithAsset:(AVAsset *)asset automaticallyLoadedAssetKeys:(nullable NSArray<NSString *> *)automaticallyLoadedAssetKeys ;
// 当暂停的时候,是否可以继续使用网络资源继续缓冲。设置为NO,不可以,可以省电。
// ios9以后默认为NO,iOS9以前默认为YES
@property (nonatomic, assign) BOOL canUseNetworkResourcesForLiveStreamingWhilePaused;
// 设置播放器提前缓冲的时间,以防止播放中断。该属性定义了首选的前向缓冲区持续时间(秒)。如果设置为0,就不缓冲了,会经常卡顿。播放器将为大多数使用情况选择适当的缓冲级别。将此属性设置为较低值会增加播放停顿和重新缓冲的机会,而将其设置为较高值会增加对系统资源的需求;
@property (nonatomic) NSTimeInterval preferredForwardBufferDuration;
1.3 AVPlayer:播放器
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item;
// 播放
- (void)play;
// 暂停
- (void)pause;
// 播放速度,正常是1,小于1就是慢放,大于1就是快放
@property (nonatomic) float rate;
// 当前播放时间
- (CMTime)currentTime;
// iOS10之后的新属性,播放器是否应自动延迟播放以尽量减少停顿
// 设置为NO,解决在新系统下有时会播放不了的问题
@property (nonatomic) BOOL automaticallyWaitsToMinimizeStalling;
// 以下三个接口都是播放跳转
// toleranceBefore和toleranceAfter分别是允许之前和之后误差的时间
// completionHandler 跳转之后的回调
// 调用 - (void)seekToTime:(CMTime)time;也是调用- (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter;,只不过toleranceBefore和toleranceAfter都是kCMTimeZero。
- (void)seekToTime:(CMTime)time;
- (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter;
- (void)seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler NS_AVAILABLE(10_7, 5_0);
1.4 AVPlayerLayer:播放器界面
// 是视频适配AVPlayerLayer的方式
// 如果视频和AVPlayerLayer长宽比例不一致,就需要对视频做拉伸。
// 有三个值:AVLayerVideoGravityResizeAspect(视频的长宽比例保持不变拉伸,留空白);AVLayerVideoGravityResizeAspectFill(视频的长宽比例保持不变拉伸,铺满整个AVPlayerLayer,这样视频会有截掉一部分);AVLayerVideoGravityResize(改变视频的长宽比例,铺满整个AVPlayerLayer,这样视频会变形)
// 一般使用AVLayerVideoGravityResizeAspect
@property(copy) AVLayerVideoGravity videoGravity;
2.监听视频的播放情况
以上的代码仅仅可以让我播放一个视频,除此之外我们还有很多需求。例如视频的长度,缓冲情况,播放情况等, 这就需要对AVPlayerItem进行KVO监听。
2.1AVPlayerItem的几个属性
1.视频资源加载的状态
@property (nonatomic, readonly) AVPlayerItemStatus status;
这个属性有三个值:AVPlayerItemStatusUnknown(未知的)、
AVPlayerItemStatusReadyToPlay(准备好了,马上开始播放)、
AVPlayerItemStatusFailed (加载失败)。
//2.视频的尺寸
@property (nonatomic, readonly) CGSize presentationSize;
3. 缓冲的情况
@property (nonatomic, readonly) NSArray<NSValue *> *loadedTimeRanges;
这是一个数组,里面的元素是CMTimeRange结构体,它表示视频缓冲到哪里了
// 获取缓存的进度
- (NSTimeInterval)loadedTime {
NSArray *timeRanges = _playerItem.loadedTimeRanges;
// 播放的进度
CMTime currentTime = _player.currentTime;
// 判断播放的进度是否在缓存的进度内
BOOL included = NO;
CMTimeRange firstTimeRange = {0};
if (timeRanges.count > 0)
{
firstTimeRange = [[timeRanges objectAtIndex:0] CMTimeRangeValue];
if (CMTimeRangeContainsTime(firstTimeRange, currentTime)) {
included = YES;
}
}
// 存在返回缓存的进度
if (included) {
CMTime endTime = CMTimeRangeGetEnd(firstTimeRange);
NSTimeInterval loadedTime = CMTimeGetSeconds(endTime);
if (loadedTime > 0) {
return loadedTime;
}
}
return 0;
}
4. 视频是否可以正常播放
@property (nonatomic, readonly, getter=isPlaybackLikelyToKeepUp) BOOL playbackLikelyToKeepUp;
这这个属性是对视频是否可以继续播放的一种预测,如果为NO,视频就会暂停。视频不能继续播放的原因主要有两个,视频没有缓冲了和缓存的数据不能正确解码(视频播放器不支持视频的格式)。所以当playbackBufferEmpty为NO,playbackBufferFull(是否已经全部缓存)为YES时,playbackLikelyToKeepUp也有可能为NO。
5.缓冲是否为空
@property (nonatomic, readonly, getter=isPlaybackBufferEmpty) BOOL playbackBufferEmpty;
这个值为YES,视频就会暂停。当这个值为NO,视频也可能不能继续播放。具体原因参考上面的属性。
2.2 监听视频的播放情况
- (void)addObserver
{
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"presentationSize" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
[self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
// 表示0.5s
CMTime interval = CMTimeMakeWithSeconds(0.5, NSEC_PER_SEC);
__weak typeof(self) weakSelf = self;
// 增加播放进度的监听 每0.5秒调用一次
_timeObserver = [self.player addPeriodicTimeObserverForInterval:interval queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
if (!weakSelf) return;
NSArray *loadedRanges = weakSelf.playerItem.seekableTimeRanges;
if (loadedRanges.count > 0 && weakSelf.playerItem.duration.timescale != 0) {
NSLog(@"播放进度 = %.2f",CMTimeGetSeconds(time));
NSLog(@"视频总时长 = %.2f",CMTimeGetSeconds(weakSelf.playerItem.duration));
}
}];
// 增加播放结束的监听
_itmePlaybackEndObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"本视频播放结束了");
}];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"status"]) {
switch (self.playerItem.status) {
case AVPlayerItemStatusUnknown:
NSLog(@"未知的播放状态");
[self.player play];
break;
case AVPlayerItemStatusReadyToPlay:
NSLog(@"马上可以播放了");
break;
case AVPlayerItemStatusFailed:
NSLog(@"发生错误:%@",self.player.error);
break;
default:
break;
}
}
if ([keyPath isEqualToString:@"presentationSize"]) {
NSLog(@"视频的尺寸:%@",NSStringFromCGSize(self.playerItem.presentationSize));
}
if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
NSLog(@"缓冲进度:%.2f",[self loadedTime]);
}
if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
NSLog(@"%@可以正常播放",self.playerItem.playbackLikelyToKeepUp ? @"" : @"不");
}
if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
NSLog(@"%@有缓冲",self.playerItem.playbackBufferEmpty ? @"没": @"");
}
}
// 移除观察者,否则会内存泄漏
- (void)removeObserver
{
@try{
[self.playerItem removeObserver:self forKeyPath:@"status"];
[self.playerItem removeObserver:self forKeyPath:@"presentationSize"];
[self.playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
[self.playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
[self.playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
} @catch(NSException *e){
NSLog(@"failed to remove observer");
}
}
// 获取缓存的进度
- (NSTimeInterval)loadedTime {
NSArray *timeRanges = _playerItem.loadedTimeRanges;
// 播放的进度
CMTime currentTime = _player.currentTime;
// 判断播放的进度是否在缓存的进度内
BOOL included = NO;
CMTimeRange firstTimeRange = {0};
if (timeRanges.count > 0) {
firstTimeRange = [[timeRanges objectAtIndex:0] CMTimeRangeValue];
if (CMTimeRangeContainsTime(firstTimeRange, currentTime)) {
included = YES;
}
}
// 存在返回缓存的进度
if (included) {
CMTime endTime = CMTimeRangeGetEnd(firstTimeRange);
NSTimeInterval loadedTime = CMTimeGetSeconds(endTime);
if (loadedTime > 0) {
return loadedTime;
}
}
return 0;
}
@end
通过KVO、通知和系统提供的方法,可以完美监测播放的缓冲情况、播放进度、播放结束等,这样我们就可以给视频增加播放进度条、缓冲进度条等UI,可以对播放情况不好时做一些处理。
当self.playerItem.status = AVPlayerItemStatusReadyToPlay
的时候,我们要再执行一次[self play];
。因为当self.playerItem.playbackLikelyToKeepUp
为NO的时候会暂停播放,为YES的时候确不会自动播放。如果我们不执行,即使网络好了,视频也不会继续播放了。
当没有缓冲的时候,就要暂停,因为如果还继续播放,就会卡顿,还可能没有声音,所以我们就要缓冲一会。
// 当网络不好的时候,会多次调用这里,
- (void)buffingSomeSeconds
{
// 需要先暂停一小会之后再播放,否则网络状况不好的时候时间在走,声音播放不出来
[self.player pause];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 如果此时用户已经暂停了,则不再需要开启播放了
if (!self.playing) {return;}
[self play];
// 如果执行了play还是没有播放则说明还没有缓存好,则再次缓存一段时间
if (!self.playerItem.isPlaybackLikelyToKeepUp)
{
[self buffingSomeSeconds];
}
});
}
关于视频播放器,还有很多其他的注意事项,下文会慢慢介绍。