简单介绍一下,AVPlayer是基于AVFoundation框架的一个类,很接近底层,灵活性强,方便自定义各种需求,使用之前需要先导入
#import <AVKit/AVKit.h>
这个简易播放器非常简单,是我拿了练手玩的,功能只包括播放、暂停、滑动播放、显示缓冲进度。
- 下面开始解释实现思路,很简单的
- 1、我想要一个可以播放url地址的播放器
- 2、这个播放器,我需要显示网络状态
- 3、除了播放、暂停按钮、可拖拽的进度条、显示当前播放时间和视频总时长以外,我还想要一个显示缓冲的进度条
好,我的需求就这么简单,我就是想要自己写一个这样的播放器,至于其他的更复杂更好的用户体验的功能,暂时不考虑。目标明确了,开工。
1、工具条
这个工具条上面要包括:
1、播放(暂停)按钮的UIButton
2、可以拖拽的进度条UISlider
3、显示当前播放时间和显示视频总时长的UILabel
4、显示缓冲进度的UIProgressView
- 首先创建一个UIView,生成.h和.m文件,开始添加我需要的这些东西,开始之前我考虑到播放和暂停按钮我是用的一个Button,所以在切换状态的时候,我还要对应着改变按钮的icon,所以我为了方便,在工具条这个View里添加了一个Delegate,为了改变icon的同时,把状态传递出去,所以.h文件我这样写,代码如下:
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol VideoPlayerToolsViewDelegate <NSObject>
-(void)playButtonWithStates:(BOOL)state;
@end
@interface VideoPlayerToolsView : UIView
@property (nonatomic, strong) UIButton *bCheck;//播放暂停按钮
@property (nonatomic, strong) UISlider *progressSr;//进度条
@property (nonatomic, strong) UIProgressView *bufferPV;//缓冲条
@property (nonatomic, strong) UILabel *lTime;//时间进度和总时长
@property (nonatomic, weak) id<VideoPlayerToolsViewDelegate> delegate;
@end
NS_ASSUME_NONNULL_END
- .m文件
#import "VideoPlayerToolsView.h"
@interface VideoPlayerToolsView ()
@end
@implementation VideoPlayerToolsView
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
[self createUI];//创建UI
}
return self;
}
#pragma mark - 创建UI
-(void)createUI{
[self addSubview:self.bCheck];//开始暂停按钮
[self addSubview:self.bufferPV];//缓冲条
[self addSubview:self.progressSr];//创建进度条
[self addSubview:self.lTime];//视频时间
}
#pragma mark - 视频时间
-(UILabel *)lTime{
if (!_lTime) {
_lTime = [UILabel new];
_lTime.frame = CGRectMake(CGRectGetMaxX(_progressSr.frame) + 20, 0, self.frame.size.width - CGRectGetWidth(_progressSr.frame) - 40 - CGRectGetWidth(_bCheck.frame), self.frame.size.height);
_lTime.text = @"00:00/00:00";
_lTime.textColor = [UIColor whiteColor];
_lTime.textAlignment = NSTextAlignmentCenter;
_lTime.font = [UIFont systemFontOfSize:12];
_lTime.adjustsFontSizeToFitWidth = YES;
}
return _lTime;
}
#pragma mark - 创建进度条
-(UISlider *)progressSr{
if (!_progressSr) {
_progressSr = [UISlider new];
_progressSr.frame = CGRectMake(CGRectGetMinX(_bufferPV.frame) - 2, CGRectGetMidY(_bufferPV.frame) - 10, CGRectGetWidth(_bufferPV.frame) - 4, 20);
_progressSr.maximumTrackTintColor = [UIColor clearColor];
_progressSr.minimumTrackTintColor = [UIColor whiteColor];
[_progressSr setThumbImage:[UIImage imageNamed:@"point"] forState:0];
}
return _progressSr;
}
#pragma mark - 缓冲条
-(UIProgressView *)bufferPV{
if (!_bufferPV) {
_bufferPV = [UIProgressView new];
_bufferPV.frame = CGRectMake(CGRectGetMaxX(_bCheck.frame) + 20, CGRectGetMidY(_bCheck.frame) - 2, 200, 4);
_bufferPV.trackTintColor = [UIColor grayColor];
_bufferPV.progressTintColor = [UIColor cyanColor];
}
return _bufferPV;
}
#pragma mark - 开始暂停按钮
-(UIButton *)bCheck{
if (!_bCheck) {
_bCheck = [UIButton new];
_bCheck.frame = CGRectMake(0, 0, self.frame.size.height, self.frame.size.height);
[_bCheck setImage:[UIImage imageNamed:@"pause"] forState:0];
[_bCheck addTarget:self action:@selector(btnCheckSelect:) forControlEvents:UIControlEventTouchUpInside];
}
return _bCheck;
}
-(void)btnCheckSelect:(UIButton *)sender{
sender.selected = !sender.isSelected;
if (sender.selected) {
[_bCheck setImage:[UIImage imageNamed:@"play"] forState:0];
}else{
[_bCheck setImage:[UIImage imageNamed:@"pause"] forState:0];
}
if ([_delegate respondsToSelector:@selector(playButtonWithStates:)]) {
[_delegate playButtonWithStates:sender.selected];
}
}
@end
- 随便把这个工具条加载到任一一个页面看下效果,没错,目前看来就是我要的样子,先放着,后面再调用。
2、网络状态监听器
这个网络监听器是网上找到的,本来想把原文地址留下来的,结果忘记了,在这里表示抱歉,至于这个工具怎么实现的,实话实说,我看不懂,我就知道它就是我想要的东西,是不是很尴尬……那也没办法,能力有限!这个工具的使用我单独拿出去写了个文章,这里不再重复黏贴代码了。
3、AVPlayer播放器
这里是重头戏了,首先,要知道AVPlayer是怎么用的。
AVPlayer是个播放器,但是呢,它又不能直接播放视频,它需要和AVPlayerLayer配合着使用,并且需要把AVPlayerLayer添加到视图的layer上才行,比如:[self.layer addSublayer:self.playerLayer];
。
AVPlayer加载视频地址的方式是什么呢?我得需要知道,查看api,control+command+鼠标左键,进去瞅瞅,发现系统有提供以下几种方式:
+ (instancetype)playerWithURL:(NSURL *)URL;
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item;
- (instancetype)initWithURL:(NSURL *)URL;
- (instancetype)initWithPlayerItem:(nullable AVPlayerItem *)item;
那么问题来了,上面的四种方法里面有两个是用AVPlayerItem初始化的,这个是什么东西。再继续看api,什么东西啊,乱七八糟一大推,于是乎,不看了,看看前辈们是咋玩的,后来发现,前辈们用了一个叫做:
replaceCurrentItemWithPlayerItem:
的方法给AVPlayer添加播放地址,从字面上的意思我的理解是:用PlayerItem替换当前的item??
完整代码是这样写的:
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:@"视频地址"]];
[self.player replaceCurrentItemWithPlayerItem:item];
然后AVPlayer怎么添加到AVPlayerLayer上呢?代码如下:
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
- 这里要说明一下,AVPlayerLayer是需要设置frame的。
好,这里假设各个控件的初始化啊布局什么的都完事了,接下来要考虑的是控件之间相互关联显示的问题了。
1、我要先让视频播放出来再说,别的先不管,拿到地址之后,先让self.player调一下播放方法,然后监听网络,再然后用视频地址初始化一个AVPlayerItem,最后用这个AVPlayerItem播放视频,好像没毛病,就这么干了。
2、视频成功播放出来之后,我得要显示视频总时长和当前播放时间进度,方法如下:
NSTimeInterval totalTime = CMTimeGetSeconds(self.player.currentItem.duration);//总时长
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentTime);//当前时间进度
3、经过了七七四十九天的调整,时间终于显示正确了,接下来需要显示缓冲进度了,这里就需要用的KVO来对初始化self.player的时候用到的那个AVPlayerItem的属性进行监听了,我就说这个东西肯定是有用的嘛,不然为啥那么多人都用这玩意儿。
4、又经过了一个七七四十九天的调整,通过网络监听工具看着的网络变化,缓冲条好像也显示正确了,最后到了进度条的显示了……
春夏秋冬,年复一年,日复一日,不知道经过了多少个岁月……
5、上代码吧,先声明一下,代码里面肯定是包含了一些经过自己的加工让它改头换面的内容,大家都来自五湖四海,组到一起也是缘分,代码如下:
.h文件
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface VideoPlayerContainerView : UIView
@property (nonatomic, strong) NSString *urlVideo;
-(void)dealloc;
@end
NS_ASSUME_NONNULL_END
- .m文件
#import "VideoPlayerContainerView.h"
#import <AVKit/AVKit.h>
#import "NetworkSpeedMonitor.h"
#import "VideoPlayerToolsView.h"
@interface VideoPlayerContainerView ()<VideoPlayerToolsViewDelegate>
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, strong) NetworkSpeedMonitor *speedMonitor;//网速监听
@property (nonatomic, strong) UILabel *speedTextLabel;//显示网速Label
@property (nonatomic, strong) VideoPlayerToolsView *vpToolsView;//工具条
@property (nonatomic, strong) id playbackObserver;
@property (nonatomic) BOOL buffered;//是否缓冲完毕
@end
@implementation VideoPlayerContainerView
//设置播放地址
-(void)setUrlVideo:(NSString *)urlVideo{
[self.player seekToTime:CMTimeMakeWithSeconds(0, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
[self.player play];//开始播放视频
[self.speedMonitor startNetworkSpeedMonitor];//开始监听网速
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkSpeedChanged:) name:NetworkDownloadSpeedNotificationKey object:nil];
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:urlVideo]];
[self vpc_addObserverToPlayerItem:item];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.player replaceCurrentItemWithPlayerItem:item];
[self vpc_playerItemAddNotification];
});
}
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor groupTableViewBackgroundColor];
[self.layer addSublayer:self.playerLayer];
[self addSubview:self.speedTextLabel];
[self addSubview:self.vpToolsView];
}
return self;
}
- (void)networkSpeedChanged:(NSNotification *)sender {
NSString *downloadSpped = [sender.userInfo objectForKey:NetworkSpeedNotificationKey];
self.speedTextLabel.text = downloadSpped;
}
#pragma mark - 工具条
-(VideoPlayerToolsView *)vpToolsView{
if (!_vpToolsView) {
_vpToolsView = [[VideoPlayerToolsView alloc]initWithFrame:CGRectMake(0, CGRectGetHeight(self.frame) - 40, CGRectGetWidth(self.frame), 40)];
_vpToolsView.delegate = self;
[_vpToolsView.progressSr addTarget:self action:@selector(vpc_sliderTouchBegin:) forControlEvents:UIControlEventTouchDown];
[_vpToolsView.progressSr addTarget:self action:@selector(vpc_sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
[_vpToolsView.progressSr addTarget:self action:@selector(vpc_sliderTouchEnd:) forControlEvents:UIControlEventTouchUpInside];
}
return _vpToolsView;
}
-(void)playButtonWithStates:(BOOL)state{
if (state) {
[self.player pause];
}else{
[self.player play];
}
}
- (void)vpc_sliderTouchBegin:(UISlider *)sender {
[self.player pause];
}
- (void)vpc_sliderValueChanged:(UISlider *)sender {
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentItem.duration) * _vpToolsView.progressSr.value;
NSInteger currentMin = currentTime / 60;
NSInteger currentSec = (NSInteger)currentTime % 60;
_vpToolsView.lTime.text = [NSString stringWithFormat:@"%02ld:%02ld",currentMin,currentSec];
}
- (void)vpc_sliderTouchEnd:(UISlider *)sender {
NSTimeInterval slideTime = CMTimeGetSeconds(self.player.currentItem.duration) * _vpToolsView.progressSr.value;
if (slideTime == CMTimeGetSeconds(self.player.currentItem.duration)) {
slideTime -= 0.5;
}
[self.player seekToTime:CMTimeMakeWithSeconds(slideTime, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
[self.player play];
}
#pragma mark - 网速监听器
- (NetworkSpeedMonitor *)speedMonitor {
if (!_speedMonitor) {
_speedMonitor = [[NetworkSpeedMonitor alloc] init];
}
return _speedMonitor;
}
#pragma mark - 显示网速Label
- (UILabel *)speedTextLabel {
if (!_speedTextLabel) {
_speedTextLabel = [UILabel new];
_speedTextLabel.frame = CGRectMake(0, 0, self.frame.size.width, 20);
_speedTextLabel.textColor = [UIColor whiteColor];
_speedTextLabel.font = [UIFont systemFontOfSize:12.0];
_speedTextLabel.textAlignment = NSTextAlignmentCenter;
_speedTextLabel.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
}
return _speedTextLabel;
}
#pragma mark - AVPlayer
-(AVPlayer *)player{
if (!_player) {
_player = [[AVPlayer alloc] init];
__weak typeof(self) weakSelf = self;
// 每秒回调一次
self.playbackObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:NULL usingBlock:^(CMTime time) {
[weakSelf vpc_setTimeLabel];
NSTimeInterval totalTime = CMTimeGetSeconds(weakSelf.player.currentItem.duration);//总时长
NSTimeInterval currentTime = time.value / time.timescale;//当前时间进度
weakSelf.vpToolsView.progressSr.value = currentTime / totalTime;
}];
}
return _player;
}
#pragma mark - AVPlayerLayer
-(AVPlayerLayer *)playerLayer{
if (!_playerLayer) {
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
_playerLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
return _playerLayer;
}
#pragma mark ---------华丽的分割线---------
#pragma mark - lTime
- (void)vpc_setTimeLabel {
NSTimeInterval totalTime = CMTimeGetSeconds(self.player.currentItem.duration);//总时长
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentTime);//当前时间进度
// 切换视频源时totalTime/currentTime的值会出现nan导致时间错乱
if (!(totalTime >= 0) || !(currentTime >= 0)) {
totalTime = 0;
currentTime = 0;
}
NSInteger totalMin = totalTime / 60;
NSInteger totalSec = (NSInteger)totalTime % 60;
NSString *totalTimeStr = [NSString stringWithFormat:@"%02ld:%02ld",totalMin,totalSec];
NSInteger currentMin = currentTime / 60;
NSInteger currentSec = (NSInteger)currentTime % 60;
NSString *currentTimeStr = [NSString stringWithFormat:@"%02ld:%02ld",currentMin,currentSec];
_vpToolsView.lTime.text = [NSString stringWithFormat:@"%@/%@",currentTimeStr,totalTimeStr];
}
#pragma mark - 观察者
- (void)vpc_playerItemAddNotification {
// 播放完成通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(vpc_playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
}
-(void)vpc_playbackFinished:(NSNotification *)noti{
[self.player pause];
}
- (void)vpc_addObserverToPlayerItem:(AVPlayerItem *)playerItem {
// 监听播放状态
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 监听缓冲进度
[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)vpc_playerItemRemoveNotification {
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
}
- (void)vpc_playerItemRemoveObserver {
[self.player.currentItem removeObserver:self forKeyPath:@"status"];
[self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerStatus status= [[change objectForKey:@"new"] intValue];
if (status == AVPlayerStatusReadyToPlay) {
[self vpc_setTimeLabel];
}
} else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
NSArray *array = self.player.currentItem.loadedTimeRanges;
CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围
NSTimeInterval startSeconds = CMTimeGetSeconds(timeRange.start);//本次缓冲起始时间
NSTimeInterval durationSeconds = CMTimeGetSeconds(timeRange.duration);//缓冲时间
NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度
float totalTime = CMTimeGetSeconds(self.player.currentItem.duration);//视频总长度
float progress = totalBuffer/totalTime;//缓冲进度
NSLog(@"progress = %lf",progress);
//如果缓冲完了,拖动进度条不需要重新显示缓冲条
if (!self.buffered) {
if (progress == 1.0) {
self.buffered = YES;
}
[self.vpToolsView.bufferPV setProgress:progress];
}
NSLog(@"yon = %@",self.buffered ? @"yes" : @"no");
}
}
- (void)dealloc {
[self.speedMonitor stopNetworkSpeedMonitor];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NetworkDownloadSpeedNotificationKey object:nil];
[self.player removeTimeObserver:self.playbackObserver];
[self vpc_playerItemRemoveObserver];
[self.player replaceCurrentItemWithPlayerItem:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
4、整合
最后全部封装完了之后,调用的时候,只需要引入头文件
#import "VideoPlayerContainerView.h"
,在需要用的地方,直接声明,传值就ok了
VideoPlayerContainerView *vpcView = [[VideoPlayerContainerView alloc]initWithFrame:CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, 200)];
[self.view addSubview:vpcView];
vpcView.urlVideo = @"https://www.apple.com/105/media/cn/researchkit/2016/a63aa7d4_e6fd_483f_a59d_d962016c8093/films/carekit/researchkit-carekit-cn-20160321_848x480.mp4";