iOS 基於AVPlayer的簡易播放器

簡單介紹一下,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";
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章