iOS 基于AVPlayer的简易播放器

作者: 邓布利多教授 | 来源:发表于2019-07-29 15:24 被阅读0次

    简单介绍一下,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";
    
    效果图.gif

    5、全剧终

    相关文章

      网友评论

        本文标题:iOS 基于AVPlayer的简易播放器

        本文链接:https://www.haomeiwen.com/subject/mndhrctx.html