一、 前言
边学边记录AVPlayer封装一个功能十分 全的自定义播放器,目前在学习阶段,demo和文章会根据学习进度与总结情况去更新,欢迎各位批评指正。
2020年8月1日更新3.2
二、相关知识点
- AVPlayer本身并不显示视频!需要一个AVPlayerLayer播放层来显示视频,然后添加到父视图的layer中。
- AVPlayer只负责视频管理和调控!而视频资源是由AVPlayerItem提供的,每个AVPlayerItem对应一个视频地址。
- 工程内需要引入AVFoundation库
- 持续补充……
2.1 AVplayerItem
(instancetype)playerItemWithURL:(NSURL *)URL; 初始化视频资源方法
duration 视频总时长
status 视频资源的状态 (需要监听的属性)
loadedTimeRanges 在线视频的缓冲进度 (需要监听的属性)
playbackBufferEmpty 进行跳转,无缓冲 (可选监听)
playbackLikelyToKeepUp 进行跳转,有缓冲 (可选监听)
2.2 AVplayer
(void) play;
(void) pause; 播放暂停
(void) seekToTime:(CMTime)time;跳转到指定时间
(CMTime) currentTime;获取当前播放进度
(void)removeTimeObserver:(id)observer;移除监听 (销毁播放器的时候调用)
(void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;切换视频资源
2.3 AVPlayerLayer
videoGravity 视频的填充方式
- AVQueuePlayer:这个是用于处理播放列表的操作,待学习…
三、代码部分
3.1 单例
- 声明相关属性
// AVAsset对象
@property (nonatomic, strong) AVAsset *videoAsset;
// 播放器对象
@property (nonatomic, strong) AVPlayer *player;
// 播放属性
@property (nonatomic, strong) AVPlayerItem *playerItem;
// 播放容器,用于放playerlayer
@property (nonatomic, strong) UIView *videoView;
// 用于播放的layer
@property (nonatomic, strong) AVPlayerLayer *playLayer;
- 实现播放器
- (UIView *)createPlayViewFrame:(CGRect)frame withStringURL:(NSString *)stringURL isLocalFile:(BOOL)isTrue{
if (isTrue == YES){
self.videoAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:stringURL] options:nil];
} else {
self.videoAsset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:stringURL] options:nil];
}
self.playerItem = [AVPlayerItem playerItemWithAsset:self.videoAsset];
// 添加观察者
[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 获取当前视频的尺寸
[self getVideoSize:self.videoAsset];
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
// 监听是否 播放完毕
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playBackFinished:withBlock:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem];
self.playLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playLayer.frame = frame;
// 填充模式
self.playLayer.videoGravity = AVLayerVideoGravityResizeAspect;
UIView *videoView = [[UIView alloc] initWithFrame:frame];
[videoView.layer addSublayer:self.playLayer];
return videoView;
}
- 调用
- (void)viewDidLoad {
[super viewDidLoad];
UIView *videoView = [[UIView alloc] initWithFrame:self.view.frame];
videoView = [[SMZVideoManager sharedInstance] createPlayViewFrame:CGRectMake(0, 0, creenWidth, creenHeight) withStringURL:videoUrl isLocalFile:NO];
[self.view addSubview:videoView];
// 也可以将视频播放完毕的监听加在这里,需要将manager中的移除掉
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playBackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:[[SMZVideoManager sharedInstance] getCurrentPlayerItem]];
[[SMZVideoManager sharedInstance] startVideo];
}
- 观察者监听相关
#pragma mark - 观察者相关
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"status"]){
AVPlayerItem *playItem = (AVPlayerItem *)object;
if (playItem.status == AVPlayerStatusReadyToPlay){
// 可以播放
} else if (playItem.status == AVPlayerStatusFailed){
// 失败
} else {
// 未知错误AVPlayerStatusUnknown
}
}
}
- (void)playBackFinished:(NSNotification *)notification withBlock:(void (^)(void))code{
// 播放完毕的操作,这里 提供重新播放的能力
self.playerItem = [notification object];
[self.playerItem seekToTime:kCMTimeZero completionHandler:nil];
code();
}
3.2 将播放器封装成view
单例形式适合多个视频由一个播放器控制,但是上述写法明显存在问题 ,控制不了每个视图,所以现在想让每个视图有自己的播放器
// 相关方法
- (instancetype)initWithFrame:(CGRect)frame;
- (void)setupPlayerWith:(NSURL *)videoURL;
- (void)play;
- (void)pause;
- (void)replay;
- (void)destory;
// 相关实现
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self){
self.backgroundColor = [UIColor whiteColor];
}
return self;
}
// 准备播放器
- (void)setupPlayerWith:(NSURL *)videoURL{
AVURLAsset *sset = [[AVURLAsset alloc] initWithURL:videoURL options:nil];
self.videoScale = [self getVideoSize:sset];
[self creatPlayer:videoURL];
}
// 获取播放item
- (AVPlayerItem *)getPlayerItem:(NSURL *)videoURL {
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:videoURL];
return item;
}
// 创建播放器
- (void)creatPlayer:(NSURL *)videoURL {
if (!_player) {
self.currentItem = [self getPlayerItem:videoURL];
_player = [AVPlayer playerWithPlayerItem:self.currentItem];
[self creatPlayerLayer];
[self addPlayerObserver];
[self addObserverWithPlayItem:self.currentItem];
[self addNotificatonForPlayer];
}
}
// 创建视图
- (void)creatPlayerLayer {
CGFloat origin_x = 15;
CGFloat main_width = kScreenWidth - (origin_x * 2);
self.videoView = [[UIView alloc] initWithFrame:CGRectMake(origin_x, 85, main_width, main_width * self.videoScale)];
AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:self.player];
layer.frame = CGRectMake(0, 0, self.videoView.frame.size.width, self.videoView.frame.size.height);
layer.videoGravity = AVLayerVideoGravityResizeAspect;
[self.videoView.layer addSublayer:layer];
[self addSubview:self.videoView];
}
// 获取视频的比例
- (CGFloat)getVideoSize:(AVAsset *)videoAsset{
NSArray *array = videoAsset.tracks;
CGSize videoSize = CGSizeZero;
for(AVAssetTrack *track in array)
{
if([track.mediaType isEqualToString:AVMediaTypeVideo])
{
videoSize = track.naturalSize;
}
}
CGFloat videoH = videoSize.height;
CGFloat videoW = videoSize.width;
return videoH / videoW;
}
#pragma mark - 播放器功能
// 播放
- (void)play {
if (self.player.rate == 0) {
[self addNotificatonForPlayer];
[self addPlayerObserver];
}
[self.player play];
}
// 暂停
- (void)pause {
if (self.player.rate == 1.0) {
[self.player pause];
[self.player seekToTime:kCMTimeZero];
}
}
// 重新开始
- (void)replay {
if (self.player.rate == 1.0) {
[self.player pause];
[self.player seekToTime:kCMTimeZero];
[self removeNotification];
} else if (self.player.rate == 0){
[self addNotificatonForPlayer];
[self play];
}
}
- (void)destory {
[self pause];
[self removeNotification];
[self removePlayerObserver];
}
#pragma mark - 添加 监控
// 给player 添加 time observer
- (void)addPlayerObserver {
__weak typeof(self)weakSelf = self;
_timeObser = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
AVPlayerItem *playerItem = weakSelf.player.currentItem;
float current = CMTimeGetSeconds(time);
float total = CMTimeGetSeconds([playerItem duration]);
NSLog(@"当前播放进度 %.2f/%.2f.",current,total);
}];
}
// 移除 time observer
- (void)removePlayerObserver {
[_player removeTimeObserver:_timeObser];
}
- (void)addObserverWithPlayItem:(AVPlayerItem *)item {
[item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
[item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
[item addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
[item addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
}
// 移除 item 的 observer
- (void)removeObserverWithPlayItem:(AVPlayerItem *)item {
[item removeObserver:self forKeyPath:@"status"];
[item removeObserver:self forKeyPath:@"loadedTimeRanges"];
[item removeObserver:self forKeyPath:@"playbackBufferEmpty"];
[item removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
AVPlayerItem *item = object;
if ([keyPath isEqualToString:@"status"]) {// 播放状态
[self handleStatusWithPlayerItem:item];
}
}
- (void)handleStatusWithPlayerItem:(AVPlayerItem *)item {
AVPlayerItemStatus status = item.status;
switch (status) {
case AVPlayerItemStatusReadyToPlay: // 准备好播放
NSLog(@"AVPlayerItemStatusReadyToPlay");
break;
case AVPlayerItemStatusFailed: // 播放出错
NSLog(@"AVPlayerItemStatusFailed");
break;
case AVPlayerItemStatusUnknown: // 状态未知
NSLog(@"AVPlayerItemStatusUnknown");
break;
default:
break;
}
}
- (void)addNotificatonForPlayer {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(videoPlayEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
[center addObserver:self selector:@selector(videoPlayError:) name:AVPlayerItemPlaybackStalledNotification object:nil];
[center addObserver:self selector:@selector(videoPlayEnterBack:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[center addObserver:self selector:@selector(videoPlayBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
}
// 移除 通知
- (void)removeNotification {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
// [center removeObserver:self name:AVPlayerItemTimeJumpedNotification object:nil];
[center removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:nil];
[center removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
[center removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
[center removeObserver:self];
}
// 视频播放结束
- (void)videoPlayEnd:(NSNotification *)notification {
NSLog(@"视频播放结束");
// self.currentItem = [notic object];
// [self.currentItem seekToTime:kCMTimeZero completionHandler:nil];
// [self.player play];
[self.player seekToTime:kCMTimeZero];
// [self.player play];
}
// 视频异常中断
- (void)videoPlayError:(NSNotification *)notification {
NSLog(@"视频异常中断");
}
// 进入后台
- (void)videoPlayEnterBack:(NSNotification *)notification {
NSLog(@"进入后台");
}
// 返回前台
- (void)videoPlayBecomeActive:(NSNotification *)notification {
NSLog(@"返回前台");
}
#pragma mark - 销毁 release
- (void)dealloc {
NSLog(@"--- %@ --- 销毁了",[self class]);
[self removeNotification];
[self removePlayerObserver];
[self removeObserverWithPlayItem:self.player.currentItem];
}
四、demo
文章只是记录一下相关部分代码,会在慢慢进行改善。
demo会持续更新,完善功能 ,一次比一次好。