1、常见的音视频播放器
iOS开发中不可避免地会遇到音视频播放方面的需求。
常用的音频播放器有 AVAudioPlayer、AVPlayer 等。不同的是,AVAudioPlayer 只支持本地音频的播放,而 AVPlayer 既支持本地音频播放,也支持网络音频播放。
今天我们要介绍的主角就是强大的 AVPlayer。
2、AVPlayer
AVPlayer 存在于 AVFoundation 框架中,所以要使用 AVPlayer,要先在工程中导入 AVFoundation 框架。
AVPlayer 播放界面中不带播放控件,想要播放视频,必须要加入 AVPlayerLayer 中,并添加到其他能显示的 layer 当中。
AVPlayer 中音视频的播放、暂停功能对应着两个方法 play
、pause
来实现。
大多播放器都是通过通知来获取播放器的播放状态、加载状态等,而 AVPlayer 中对于获得播放状态和加载状态有用的通知只有一个:AVPlayerItemDidPlayToEndTimeNotification(播放完成通知)
。播放器的播放状态判断可以通过播放器的播放速度 rate
来获得,如果 rate
为0说明是停止状态,为1时则是正常播放状态。想要获取视频播放情况、缓冲情况等的实时变化,可以通过 KVO 监控 AVPlayerItem 的 status
、loadedTimeRanges
等属性来获得。当 AVPlayerItem 的 status
属性为 AVPlayerStatusReadyToPlay
时说明可以开始播放,只有处于这个状态时才能获得视频时长等信息;当 loadedTimeRanges
改变时(每缓冲一部分数据就会更新此属性),可以获得本次缓冲加载的视频范围(包含起始时间、本次加载时长),这样一来就可以实时获得缓冲情况。
AVPlayer 中播放进度的获取通常是通过:- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block
方法。这个方法会在设定的时间间隔内定时更新播放进度,通过 time 参数通知客户端。至于播放进度的跳转则是依靠 - (void)seekToTime:(CMTime)time
方法。
AVPlayer 还提供了 - (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item
方法用于在不同视频之间的切换(事实上在AVFoundation内部还有一个AVQueuePlayer专门处理播放列表切换,有兴趣的朋友可以自行研究,这里不再赘述)。
3、自定义AVPlayer
下面是我自己在项目中封装的音视频播放器,贴上代码,大家可以参考一下。
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
/**
播放器开始播放的通知
当存在多个播放器,可使用该通知在其他播放器播放时暂停当前播放器
*/
extern NSString * const YDPlayerDidStartPlayNotification;
/**
enum 播放器状态
- YDPlayerStatusUnknown: 未知
- YDPlayerStatusPlaying: 播放中
- YDPlayerStatusLoading: 加载中
- YDPlayerStatusPausing: 暂停中
- YDPlayerStatusFailed: 播放失败
- YDPlayerStatusFinished: 播放完成
*/
typedef NS_ENUM(NSInteger, YDPlayerStatus) {
YDPlayerStatusUnknown,
YDPlayerStatusPlaying,
YDPlayerStatusLoading,
YDPlayerStatusPausing,
YDPlayerStatusFailed,
YDPlayerStatusFinished
};
@interface YDPlayerMananger : NSObject
/**
播放器
*/
@property (nonatomic, strong) AVPlayer *player;
/**
播放器layer层
*/
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
/**
当前PlayerItem
*/
@property (nonatomic, strong) AVPlayerItem *currentItem;
/**
播放器状态
*/
@property (nonatomic, assign) YDPlayerStatus playStatus;
/**
Item总时长回调
*/
@property (nonatomic, copy) void(^currentItemDurationCallBack)(AVPlayer *player, CGFloat duration);
/**
Item播放进度回调
*/
@property (nonatomic, copy) void(^currentPlayTimeCallBack)(AVPlayer *player, CGFloat time);
/**
Item缓冲进度回调
*/
@property (nonatomic, copy) void(^currentLoadedTimeCallBack)(AVPlayer *player, CGFloat time);
/**
Player状态改变回调
*/
@property (nonatomic, copy) void(^playStatusChangeCallBack)(AVPlayer *player, YDPlayerStatus status);
/**
初始化方法
@param url 播放链接
@return YDPlayerMananger对象
*/
- (instancetype)initWithURL:(NSURL *)url;
/**
创建单例对象
@return YDPlayerMananger单例对象
*/
+ (instancetype)shareManager;
/**
将播放器展示在某个View
@param view 展示播放器的View
*/
- (void)showPlayerInView:(UIView *)view withFrame:(CGRect)frame;
/**
替换PlayerItem
@param url 需要播放的链接
*/
- (void)replaceCurrentItemWithURL:(NSURL *)url;
/**
播放某个链接
@param urlStr 需要播放的链接
*/
- (void)playWithUrl:(NSString *)urlStr;
/**
开始播放
*/
- (void)play;
/**
暂停播放
*/
- (void)pause;
/**
停止播放
*/
- (void)stop;
/**
跳转到指定时间
@param time 指定的时间
*/
- (void)seekToTime:(CGFloat)time;
@end
复制代码
#import "YDPlayerMananger.h"
NSString * const YDPlayerDidStartPlayNotification = @"YDPlayerDidStartPlayNotification";
@interface YDPlayerMananger ()
@property (nonatomic, strong) id timeObserver; // 监控播放进度的观察者
@end
@implementation YDPlayerMananger
#pragma mark - 生命周期
- (instancetype)init
{
if (self = [super init]) {
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setActive:YES error:nil];
self.player = [[AVPlayer alloc] init];
[self addNotificationAndObserver];
}
return self;
}
- (instancetype)initWithURL:(NSURL *)url
{
if (self = [self init]) {
[self replaceCurrentItemWithURL:url];
}
return self;
}
+ (instancetype)shareManager
{
static YDPlayerMananger *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] init];
});
return manager;
}
- (void)dealloc
{
[self removeNotificationAndObserver];
}
#pragma mark - 公开方法
- (void)showPlayerInView:(UIView *)view withFrame:(CGRect)frame
{
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
_playerLayer.frame = frame;
_playerLayer.backgroundColor = [UIColor blackColor].CGColor;
_playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
[view.layer addSublayer:_playerLayer];
}
- (void)replaceCurrentItemWithURL:(NSURL *)url
{
// 移除当前观察者
if (_currentItem) {
[_currentItem removeObserver:self forKeyPath:@"status"];
[_currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}
_currentItem = [[AVPlayerItem alloc] initWithURL:url];
[self.player replaceCurrentItemWithPlayerItem:_currentItem];
// 重新添加观察者
[_currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[_currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)playWithUrl:(NSString *)urlStr
{
[self replaceCurrentItemWithURL:[NSURL URLWithString:urlStr]];
[self play];
}
- (void)play
{
[self.player play];
self.playStatus = YDPlayerStatusPlaying;
// 发起开始播放的通知
[[NSNotificationCenter defaultCenter] postNotificationName:YDPlayerDidStartPlayNotification object:_player];
}
- (void)pause
{
[self.player pause];
self.playStatus = YDPlayerStatusPausing;
}
- (void)stop
{
[self.player pause];
[_currentItem cancelPendingSeeks];
self.playStatus = YDPlayerStatusFinished;
}
- (void)seekToTime:(CGFloat)time
{
[_currentItem seekToTime:CMTimeMakeWithSeconds(time, 1.0) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
#pragma mark - 私有方法
// 添加通知、观察者
- (void)addNotificationAndObserver
{
// 添加播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
// 添加打断播放的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionComing:) name:AVAudioSessionInterruptionNotification object:nil];
// 添加插拔耳机的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChanged:) name:AVAudioSessionRouteChangeNotification object:nil];
// 添加观察者监控播放器状态
[self addObserver:self forKeyPath:@"playStatus" options:NSKeyValueObservingOptionNew context:nil];
// 添加观察者监控进度
__weak typeof(self) weakSelf = self;
_timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf.currentPlayTimeCallBack) {
float currentPlayTime = (double)strongSelf.currentItem.currentTime.value / strongSelf.currentItem.currentTime.timescale;
strongSelf.currentPlayTimeCallBack(strongSelf.player, currentPlayTime);
}
}];
}
// 移除通知、观察者
- (void)removeNotificationAndObserver
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self removeObserver:self forKeyPath:@"playStatus"];
[_player removeTimeObserver:_timeObserver];
if (_currentItem) {
[_currentItem removeObserver:self forKeyPath:@"status"];
[_currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}
}
#pragma mark - 观察者
// 观察者
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"status"]) {
AVPlayerStatus status = [[change objectForKey:@"new"] intValue];
if (status == AVPlayerStatusReadyToPlay) {
// 获取视频长度
if (self.currentItemDurationCallBack) {
CGFloat duration = CMTimeGetSeconds(_currentItem.duration);
self.currentItemDurationCallBack(_player, duration);
}
} else if (status == AVPlayerStatusFailed) {
self.playStatus = YDPlayerStatusFailed;
} else {
self.playStatus = YDPlayerStatusUnknown;
}
} else if ([keyPath isEqualToString:@"playStatus"]) {
if (self.playStatusChangeCallBack) {
self.playStatusChangeCallBack(_player, _playStatus);
}
} else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
// 计算缓冲总进度
NSArray *loadedTimeRanges = [_currentItem loadedTimeRanges];
CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];
float startSeconds = CMTimeGetSeconds(timeRange.start);
float durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval loadedTime = startSeconds + durationSeconds;
if (self.playStatus == YDPlayerStatusPlaying && self.player.rate <= 0) {
self.playStatus = YDPlayerStatusLoading;
}
// 卡顿时缓冲完成后自动播放
if (self.playStatus == YDPlayerStatusLoading) {
NSTimeInterval currentTime = self.player.currentTime.value / self.player.currentTime.timescale;
if (loadedTime > currentTime + 5) {
[self play];
}
}
if (self.currentLoadedTimeCallBack) {
self.currentLoadedTimeCallBack(_player, loadedTime);
}
}
}
#pragma mark - 通知
// 播放完成通知
- (void)playbackFinished:(NSNotification *)notification
{
AVPlayerItem *playerItem = (AVPlayerItem *)notification.object;
if (playerItem == _currentItem) {
self.playStatus = YDPlayerStatusFinished;
}
}
// 插拔耳机通知
- (void)routeChanged:(NSNotification *)notification
{
NSDictionary *dic = notification.userInfo;
int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];
// 旧输出不可用
if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
AVAudioSessionRouteDescription *routeDescription = dic[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *portDescription = [routeDescription.outputs firstObject];
// 原设备为耳机则暂停
if ([portDescription.portType isEqualToString:@"Headphones"]) {
[self pause];
}
}
}
// 来电、闹铃打断播放通知
- (void)interruptionComing:(NSNotification *)notification
{
NSDictionary *userInfo = notification.userInfo;
AVAudioSessionInterruptionType type = [userInfo[AVAudioSessionInterruptionTypeKey] intValue];
if (type == AVAudioSessionInterruptionTypeBegan) {
[self pause];
}
}
@end
复制代码
4、注意点
在使用 AVPlayer 时需要注意的是,由于播放状态、缓冲状态等是通过 KVO 监控 AVPlayerItem 的 status、loadedTimeRanges 等属性来获得的,在使用 - (void)replaceCurrentItemWithPlayerItem:(AVPlayerItem *)item
切换视频后,当前的 AVPlayerItem 实际上已经被释放掉了,所以一定要及时移除观察者并重新添加,否则会引起崩溃。
转载自:
如果有大神发现文章中的错误,欢迎指正。有兴趣下载文中 Demo 的朋友,可以前往我的GitHub:GitHud地址