簡單介紹一下,AVPlayer是基于AVFoundation框架的一個(gè)類,很接近底層,靈活性強(qiáng),方便自定義各種需求,使用之前需要先導(dǎo)入
#import <AVKit/AVKit.h>
這個(gè)簡易播放器非常簡單,是我拿了練手玩的,功能只包括播放、暫停、滑動(dòng)播放、顯示緩沖進(jìn)度。
- 下面開始解釋實(shí)現(xiàn)思路,很簡單的
- 1、我想要一個(gè)可以播放url地址的播放器
- 2、這個(gè)播放器,我需要顯示網(wǎng)絡(luò)狀態(tài)
- 3、除了播放、暫停按鈕、可拖拽的進(jìn)度條、顯示當(dāng)前播放時(shí)間和視頻總時(shí)長以外,我還想要一個(gè)顯示緩沖的進(jìn)度條
好,我的需求就這么簡單,我就是想要自己寫一個(gè)這樣的播放器,至于其他的更復(fù)雜更好的用戶體驗(yàn)的功能,暫時(shí)不考慮。目標(biāo)明確了,開工。
1、工具條
這個(gè)工具條上面要包括:
1、播放(暫停)按鈕的UIButton
2、可以拖拽的進(jìn)度條UISlider
3、顯示當(dāng)前播放時(shí)間和顯示視頻總時(shí)長的UILabel
4、顯示緩沖進(jìn)度的UIProgressView
- 首先創(chuàng)建一個(gè)UIView,生成.h和.m文件,開始添加我需要的這些東西,開始之前我考慮到播放和暫停按鈕我是用的一個(gè)Button,所以在切換狀態(tài)的時(shí)候,我還要對(duì)應(yīng)著改變按鈕的icon,所以我為了方便,在工具條這個(gè)View里添加了一個(gè)Delegate,為了改變icon的同時(shí),把狀態(tài)傳遞出去,所以.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;//進(jìn)度條
@property (nonatomic, strong) UIProgressView *bufferPV;//緩沖條
@property (nonatomic, strong) UILabel *lTime;//時(shí)間進(jìn)度和總時(shí)長
@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];//創(chuàng)建UI
}
return self;
}
#pragma mark - 創(chuàng)建UI
-(void)createUI{
[self addSubview:self.bCheck];//開始暫停按鈕
[self addSubview:self.bufferPV];//緩沖條
[self addSubview:self.progressSr];//創(chuàng)建進(jìn)度條
[self addSubview:self.lTime];//視頻時(shí)間
}
#pragma mark - 視頻時(shí)間
-(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 - 創(chuàng)建進(jìn)度條
-(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
- 隨便把這個(gè)工具條加載到任一一個(gè)頁面看下效果,沒錯(cuò),目前看來就是我要的樣子,先放著,后面再調(diào)用。
2、網(wǎng)絡(luò)狀態(tài)監(jiān)聽器
這個(gè)網(wǎng)絡(luò)監(jiān)聽器是網(wǎng)上找到的,本來想把原文地址留下來的,結(jié)果忘記了,在這里表示抱歉,至于這個(gè)工具怎么實(shí)現(xiàn)的,實(shí)話實(shí)說,我看不懂,我就知道它就是我想要的東西,是不是很尷尬……那也沒辦法,能力有限!這個(gè)工具的使用我單獨(dú)拿出去寫了個(gè)文章,這里不再重復(fù)黏貼代碼了。
3、AVPlayer播放器
這里是重頭戲了,首先,要知道AVPlayer是怎么用的。
AVPlayer是個(gè)播放器,但是呢,它又不能直接播放視頻,它需要和AVPlayerLayer配合著使用,并且需要把AVPlayerLayer添加到視圖的layer上才行,比如:[self.layer addSublayer:self.playerLayer];。
AVPlayer加載視頻地址的方式是什么呢?我得需要知道,查看api,control+command+鼠標(biāo)左鍵,進(jìn)去瞅瞅,發(fā)現(xiàn)系統(tǒng)有提供以下幾種方式:
+ (instancetype)playerWithURL:(NSURL *)URL;
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item;
- (instancetype)initWithURL:(NSURL *)URL;
- (instancetype)initWithPlayerItem:(nullable AVPlayerItem *)item;
那么問題來了,上面的四種方法里面有兩個(gè)是用AVPlayerItem初始化的,這個(gè)是什么東西。再繼續(xù)看api,什么東西啊,亂七八糟一大推,于是乎,不看了,看看前輩們是咋玩的,后來發(fā)現(xiàn),前輩們用了一個(gè)叫做:
replaceCurrentItemWithPlayerItem:的方法給AVPlayer添加播放地址,從字面上的意思我的理解是:用PlayerItem替換當(dāng)前的item??
完整代碼是這樣寫的:
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:@"視頻地址"]];
[self.player replaceCurrentItemWithPlayerItem:item];
然后AVPlayer怎么添加到AVPlayerLayer上呢?代碼如下:
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
- 這里要說明一下,AVPlayerLayer是需要設(shè)置frame的。
好,這里假設(shè)各個(gè)控件的初始化啊布局什么的都完事了,接下來要考慮的是控件之間相互關(guān)聯(lián)顯示的問題了。
1、我要先讓視頻播放出來再說,別的先不管,拿到地址之后,先讓self.player調(diào)一下播放方法,然后監(jiān)聽網(wǎng)絡(luò),再然后用視頻地址初始化一個(gè)AVPlayerItem,最后用這個(gè)AVPlayerItem播放視頻,好像沒毛病,就這么干了。
2、視頻成功播放出來之后,我得要顯示視頻總時(shí)長和當(dāng)前播放時(shí)間進(jìn)度,方法如下:
NSTimeInterval totalTime = CMTimeGetSeconds(self.player.currentItem.duration);//總時(shí)長
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentTime);//當(dāng)前時(shí)間進(jìn)度
3、經(jīng)過了七七四十九天的調(diào)整,時(shí)間終于顯示正確了,接下來需要顯示緩沖進(jìn)度了,這里就需要用的KVO來對(duì)初始化self.player的時(shí)候用到的那個(gè)AVPlayerItem的屬性進(jìn)行監(jiān)聽了,我就說這個(gè)東西肯定是有用的嘛,不然為啥那么多人都用這玩意兒。
4、又經(jīng)過了一個(gè)七七四十九天的調(diào)整,通過網(wǎng)絡(luò)監(jiān)聽工具看著的網(wǎng)絡(luò)變化,緩沖條好像也顯示正確了,最后到了進(jìn)度條的顯示了……
春夏秋冬,年復(fù)一年,日復(fù)一日,不知道經(jīng)過了多少個(gè)歲月……
5、上代碼吧,先聲明一下,代碼里面肯定是包含了一些經(jīng)過自己的加工讓它改頭換面的內(nèi)容,大家都來自五湖四海,組到一起也是緣分,代碼如下:
.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;//網(wǎng)速監(jiān)聽
@property (nonatomic, strong) UILabel *speedTextLabel;//顯示網(wǎng)速Label
@property (nonatomic, strong) VideoPlayerToolsView *vpToolsView;//工具條
@property (nonatomic, strong) id playbackObserver;
@property (nonatomic) BOOL buffered;//是否緩沖完畢
@end
@implementation VideoPlayerContainerView
//設(shè)置播放地址
-(void)setUrlVideo:(NSString *)urlVideo{
[self.player seekToTime:CMTimeMakeWithSeconds(0, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
[self.player play];//開始播放視頻
[self.speedMonitor startNetworkSpeedMonitor];//開始監(jiān)聽網(wǎng)速
[[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 - 網(wǎng)速監(jiān)聽器
- (NetworkSpeedMonitor *)speedMonitor {
if (!_speedMonitor) {
_speedMonitor = [[NetworkSpeedMonitor alloc] init];
}
return _speedMonitor;
}
#pragma mark - 顯示網(wǎng)速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;
// 每秒回調(diào)一次
self.playbackObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:NULL usingBlock:^(CMTime time) {
[weakSelf vpc_setTimeLabel];
NSTimeInterval totalTime = CMTimeGetSeconds(weakSelf.player.currentItem.duration);//總時(shí)長
NSTimeInterval currentTime = time.value / time.timescale;//當(dāng)前時(shí)間進(jìn)度
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);//總時(shí)長
NSTimeInterval currentTime = CMTimeGetSeconds(self.player.currentTime);//當(dāng)前時(shí)間進(jìn)度
// 切換視頻源時(shí)totalTime/currentTime的值會(huì)出現(xiàn)nan導(dǎo)致時(shí)間錯(cuò)亂
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 {
// 監(jiān)聽播放狀態(tài)
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
// 監(jiān)聽緩沖進(jìn)度
[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];//本次緩沖時(shí)間范圍
NSTimeInterval startSeconds = CMTimeGetSeconds(timeRange.start);//本次緩沖起始時(shí)間
NSTimeInterval durationSeconds = CMTimeGetSeconds(timeRange.duration);//緩沖時(shí)間
NSTimeInterval totalBuffer = startSeconds + durationSeconds;//緩沖總長度
float totalTime = CMTimeGetSeconds(self.player.currentItem.duration);//視頻總長度
float progress = totalBuffer/totalTime;//緩沖進(jìn)度
NSLog(@"progress = %lf",progress);
//如果緩沖完了,拖動(dòng)進(jìn)度條不需要重新顯示緩沖條
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、整合
最后全部封裝完了之后,調(diào)用的時(shí)候,只需要引入頭文件
#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";
