美文网首页音、視頻編解碼视频技术iOS音视频大全
基于AVPlayer封装视频播放器(具有边下边播、离线缓存、自定

基于AVPlayer封装视频播放器(具有边下边播、离线缓存、自定

作者: dev_liyang | 来源:发表于2016-11-18 00:00 被阅读1010次

    最近因公司需求,要做一个类似QQ空间视频的轮播效果,故封装了一个功能齐全的视频播放器来实现项目的需求。现在我将封装过程中遇到的难题写出来和大家分享,以下只对重点进行说明,源码里有非常详细的注释,有兴趣的小伙伴可以下载参考。如果有此相似需求的小伙伴可以直接使用,在项目中播放视频只需简单两步。

        self.videoPlayer = [[LYVideoPlayer alloc] init];
        [self.videoPlayer playWithUrl:self.videoUrl showView:self.view];
    

    本篇文章将从三个大的模块为大家介绍一个视频播放器的封装。

    • 第一:视频播放的实现;
    • 第二:离线缓存的实现;
    • 第三:自定义控制面板(自定义滑块可随意调整滑块大小和轨道高度、手势前进/后退、手势音量加减)。
      首先来看一下实现的效果:
    播放器播放效果.gif 自定义滑块2.png 自定义滑块3.png

    一、视频播放的实现

    1、要现实视频的播放,得先知道在AVFoundation框架下的三个类:AVPlayerItem、AVPlayer、AVPlayerLayer。AVPlayerItem是一个媒体资源管理类,负责数据的获取与分发;AVPlayer负责解码数据;AVPlayerLayer 是图层显示,用于数据的展示。

        //1.创建播放器
        self.currentPlayerItem = [AVPlayerItem playerItemWithURL:url];
        self.player = [AVPlayer playerWithPlayerItem:self.currentPlayerItem];
        self.currentPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    

    2、第一步已经创建好了播放器,接下来就是去播放了,那么问题就来了,咱们怎么知道什么时候可以开始播放了呢?这就需要去监听播放器的状态了,通过KVO监听AVPlayerItem的状态,获得状态后就可以去让AVPlayer执行播放的方法了。

    
        //1.通过KVO监听AVPlayerItem的状态
        [self.currentPlayerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
      
        //2.方法回调-根据状态做相应的逻辑处理
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
        
        AVPlayerItem *playerItem = (AVPlayerItem *)object;
        
        if ([keyPath isEqualToString:@"status"]) {
            AVPlayerItemStatus status = playerItem.status;
            switch (status) {
                case AVPlayerItemStatusUnknown:{
                    NSLog(@"======== 播放失败");
                }
                    break;
                    
                case AVPlayerItemStatusReadyToPlay:{
                    NSLog(@"========= 准备播放");
                    //去播放
                    [self play];
                    //图层显示
                    [self handleShowViewSublayers];
                }
                    break;
                    
                case AVPlayerItemStatusFailed:{
                    NSLog(@"======== 播放失败");
                }
                    break;
                    
                default:
                    break;
            }
        }
    }
    

    3、视频的播放很简单,但是只有播放功能还远远不能满足咱们的需求啊!好吧,继续来。其中比较伤脑筋的是菊花(此菊花非彼菊花~)的显示逻辑,有多伤脑筋我就不说太细了,反正我相信做过这个的小伙伴应该是明白菊花带来的伤痛的。首先菊花的显示第一次肯定是在加载数据的时候进行显示的,再者就是在播放到没有缓冲数据的时候进行显示,这个很容易实现。实现方法就是利用AVPlayerItem进行监听,代码如下

     //监听到当前没有缓冲数据   
     [self.currentPlayerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
        
        AVPlayerItem *playerItem = (AVPlayerItem *)object;
        if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
            self.isPlaying = NO;
            self.isBufferEmpty = YES;
            self.lastBufferValue = self.currentBufferValue;
            [self.videoPlayControl videoPlayerDidLoading];//显示菊花
            
            NSLog(@"====playbackBufferEmpty");
        }
    }
    

    好了,伤脑筋的终于来了,菊花显示了该什么时候去让它消失呢?有的小伙伴可能会说利用AVPlayer进行播放的监听啊,当视频播放的时候就让菊花消失就行了。当然这是肯定的,会在这里监听做事情,但是事情远不止这一点。比如当正在缓冲的时候被暂停了,那么监听视频的播放来让菊花消失肯定是不能满足需求的,还有一个非常蛋疼的问题就是当拖动滑块到没有缓冲的地方的时候,这时候明明正在缓冲数据没有播放,但是这个时候莫名其妙的就还会来到这个地方,所以我不得已在代码里做了一些不人性化的操作,就是获取当前的本地时间,然后在拖动滑块的时候记录下当前时间来和下次进入这个方法的时候作对比,如果是时间差大于1秒以上的一般就是真正的在播放了。

    
    - (void)addObserver {
        //监听播放进度
        __weak typeof(self) weakSelf = self;
        self.timeObserve = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            CGFloat current = CMTimeGetSeconds(time);
            CGFloat total = CMTimeGetSeconds(weakSelf.currentPlayerItem.duration);
            CGFloat progress = current / total;
            
            weakSelf.videoPlayControl.currentTime = current;
            weakSelf.videoPlayControl.playValue = progress;
            
            /***** 这里是比较蛋疼的,当拖动滑块到没有缓冲的地方并且没有开始播放时,也会走到这里 *************/
            if (weakSelf.isCanToGetLocalTime) {
                 weakSelf.localTime = [weakSelf getLocalTime];
            }
            NSInteger timeNow = [weakSelf getLocalTime];
            if (timeNow - weakSelf.localTime > 1.5) {
                [weakSelf.videoPlayControl videoPlayerDidBeginPlay];
                weakSelf.isCanToGetLocalTime = YES;
            }
        }];
    }
    

    二、离线缓存的实现

    实现数据的离线缓存,我的做法是,当在建立起数据请求的时候,根据url生成一个文件路径,让数据下载到一个临时的文件路径下。第一种情况:当请求发起时一直下载到下载成功,这时候就将该文件移动到缓存目录下缓存起来。第二种情况:当中断下载数据时,对该临时文件不做任何处理,然后再次播放该视频请求数据时,根据url生成的路径查找当前的临时路径下有无该文件,如果有说明该文件没有下载完成,则需要读到这个文件然后做断点续传操作,让该文件继续下载,而不是重头开始下载。我在这里是提供了一个离线缓存的思路,如想深入研究离线缓存和断点下载的小伙伴可以去这里看看【补充】NSURLSession 详解离线断点下载的实现

    
    - (void)fileJudge{
        //判断当前目录下有无已有下载的临时文件
        if ([_fileManager fileExistsAtPath:self.videoTempPath]) {
            //存在已下载数据的文件
            _fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:self.videoTempPath];
            _curruentLength = [_fileHandle seekToEndOfFile];
            
        }else{
            //不存在文件
            _curruentLength = 0;
            //创建文件
            [_fileManager createFileAtPath:self.videoTempPath contents:nil attributes:nil];
            _fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:self.videoTempPath];
        }
        //发起请求
        [self sendHttpRequst];
    }
    
    //网路请求方法
    - (void)sendHttpRequst
    {
        [_fileHandle seekToEndOfFile];
        NSURL *url = [NSURL URLWithString:_videoUrl];
        NSMutableURLRequest *requeset = [NSMutableURLRequest requestWithURL:url];
        
        //指定头信息  当前已下载的进度
        [requeset setValue:[NSString stringWithFormat:@"bytes=%ld-", _curruentLength] forHTTPHeaderField:@"Range"];
        
        //创建请求
        NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:requeset];
        self.dataTask = dataTask;
        
        //发起请求
        [self.dataTask resume];
    }
    
    -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
        
        if (error == nil) { //下载成功
            //当前下载文件的临时路径
            NSURL *tempPathURL = [NSURL fileURLWithPath:self.videoTempPath];
            //缓存路径
            NSURL *cachefileURL = [NSURL fileURLWithPath:self.videoCachePath];
    
            // 如果没有该文件夹,创建文件夹
            if (![self.fileManager fileExistsAtPath:self.videoCachePath]) {
                [self.fileManager createDirectoryAtPath:self.videoCachePath withIntermediateDirectories:YES attributes:nil error:nil];
            }
            
            // 如果该路径下文件已经存在,就要先将其移除,在移动文件
            if ([self.fileManager fileExistsAtPath:[cachefileURL path] isDirectory:NULL]) {
                [self.fileManager removeItemAtURL:cachefileURL error:NULL];
            }
            //移动文件至缓存目录
            [self.fileManager moveItemAtURL:tempPathURL toURL:cachefileURL error:NULL];
        }
    }
    

    三、自定义控制面板

    该控制面板具有一个视频播放器应该具备的基本功能:播放与暂停、滑块拖动播放、显示视频当前播放时间和总的时间。另外我增加了一些手势的操作:左右滑动实现前进和后退,上下滑动实现音量的加减,单击实现面板的显示与收起。
    因为系统的滑块UISlider滑块控件要说实用倒也是能用,但是要改成自己想要的UI那也是件蛋疼的事情,所以我专门对滑块又做了一次单独的封装,封装好的滑块控件LYSlider,想要改变其高度和大小只需要改变相应的两个属性(trackHeight、thumbVisibleSize)就行了,当然想到系统的滑块和进度条是两个分开的控件,在此我也将进度条一起封装进去了,也就是滑块里具有缓冲进度条的功能,只要你对bufferProgress这个属性传值,那么这个进度条就会显示出来了。这里就上LYSlider初始化时候的代码了,想要一谈究竟的小伙伴就去我的github(地址在最后)下载源码吧!码字不容易写代码更不容易!记得给我star哦~

    
    - (LYSlider *)videoSlider{
        if (!_videoSlider) {
            _videoSlider = [[LYSlider alloc] initWithFrame:CGRectMake(CGRectGetMaxX(self.currentLabel.frame) + 5, 0, _frame.size.width - CGRectGetMaxX(self.currentLabel.frame) - self.totalLabel.frame.size.width - 20 , BottomHeight)];
            
            //设置滑块图片样式
            // 1 通过颜色创建 Image
             UIImage *normalImage = [UIImage createImageWithColor:[UIColor redColor] radius:5.0];
            
            // 2 通过view 创建 Image
            UIView *highlightView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
            highlightView.layer.cornerRadius = 6;
            highlightView.layer.masksToBounds = YES;
            highlightView.backgroundColor = [UIColor redColor];
            UIImage *highlightImage = [UIImage creatImageWithView:highlightView];
    
            [_videoSlider setThumbImage:normalImage forState:UIControlStateNormal];
            [_videoSlider setThumbImage:highlightImage forState:UIControlStateHighlighted];
            
            _videoSlider.trackHeight = 1.5;    //设置轨道高度
            _videoSlider.thumbVisibleSize = 12;//设置滑块(可见的)大小
            
            [_videoSlider addTarget:self action:@selector(sliderValueChange:) forControlEvents:UIControlEventValueChanged];//正在拖动
            [_videoSlider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventEditingDidEnd];//拖动结束
            [self.bottomView addSubview:_videoSlider];
        }
        return _videoSlider;
    }
    
    最后的话

    本篇文章只是对播放器的简单的封装,如有不合理的地方还望指正!如果你看了这篇文章对你有些许的帮助,我也将感到非常荣幸!也请点击下方的喜欢或关注本人

    相关文章

      网友评论

      • 可惜你不是我的双子座:大神做的自定义滑块非常的帮,帮助很大,但是有个bug,就是有时滑动进度条的时候,会回到原来的位置然后快速到滑动的位置,只是有时候,怎么解决?急急急
        可惜你不是我的双子座:@一个安静的时候 是的 自己写的音频播放,只用了你写的滑块!
        可惜你不是我的双子座:@Dev_LiYang 我先试试 谢谢啦
        dev_liyang:@一个安静的时候 你是自己自定义的播放器?你这个情况我分析的话,你在滑动的时候视频还是在播放中的,所以播放的回调会一直走,当你滑动滑块结束时,回调会更新你的滑块进度,所以就回到了原来的位置。解决办法:当滑动滑块时指定一个BOOL变量值,播放的回调里判断这个值,做相应的操作,滑动结束后更新BOOL值,回调里就会走相应的操作了。你看看能解决不,不行的话咱们再聊细节。
      • 鹿零9:大神,iOS 8播放视频没有声音怎么回事啊? :fearful:
        鹿零9:@Dev_LiYang 对不起,大神,我错了,无意中碰到了静音按钮。。 :joy: :joy:
        dev_liyang:@鹿零 不会吧,你是iOS8.几的?我测试机是iOS8.4.1的,正常有声音。还有你是否设置了mute为YES呢?
      • hu9134:请教一个问题,现在有一个需求是实现视频慢速播放,2,4,8倍慢速播放,我是使用的AvPlayer,本来是设置的rate来控制播放速度,因为正常播放时rate为1,想设置慢速的话,就设置0--1之间的数字,但是发现设置0.5和0.1都是一个速度,请问这个怎么实现?非常感谢!
        dev_liyang:@hu9134 我试了下,设置0.1和0.5的播放速度还是有区别的,是不是其实有区别,只是你人为感觉不到变化
        hu9134:@Dev_LiYang 小于0.5的都是一个速度,和0.5的速度是一样的。好像是这个属性的值为0.5--2
        dev_liyang:@hu9134 你设置rate的值为0.1和0.5后,播放速度有变慢吗?你的是0.1和0.5下的速度都是一样的慢?还是不管值设为多少对播放速度都没有作用?
      • wh5865885:我现在就根据网上的一些资料 用AVAssetResourceLoader这个在写缓存 但是难点也是在 缓存一半的时候 取消掉 再当前文件继续缓存的时候 就出了好多问题...我用fileHandleForUpdatingAtPath 调用之前缓存一半的文件 就会把那个文件 弄成0kb 不知道为啥...请问下您有遇到过这个问题吗
        dev_liyang:@wh5865885 既然能读取到文件那就是没问题的,读取到文件后再写入数据的话一定要用seekToEndOfFile 这个方法将节点跳转至文件末尾,否则就会出现你那种情况,希望能帮到你
      • 老司机Wicky:兄弟,你这个边下边播其实是走了两份流量吧
        dev_liyang:@老司机Wicky 哈,得花时间去研究下了:sunglasses:
        老司机Wicky:嗯,我就是想说那个
        dev_liyang:@老司机Wicky 对,其中的离线缓存是走的另一份流量,如果单纯的播放而且不需要缓存的话就只会走一份流量了。然而这里缓存的实现需要另一份流量,这也是个比较大的问题,之前倒是看过一篇文章介绍说在AVAssetResourceLoader类的代理方法里做处理就可以拿到播放器下载的数据,据说这个知识点还是蛮难的,也因为时间问题就没有深入研究了,目前也没有好的实现方法,不知老司机你有没有什么好的解决办法呢 :smile:

      本文标题:基于AVPlayer封装视频播放器(具有边下边播、离线缓存、自定

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