短视频软件开发,仿抖音滑动播放视频的实现

下面要说的就是短视频软件开发重中之重,仿抖音滑动播放视频的实现。

当我们首次进入播放短视频页面时,会优先判断当前的视频列表videoList是否有值,如果没有值或当前的视频的index大于videoList.count - 3 时,就会重新请求服务端,获取新的一组短视频。

下面时核心代码

if (!_videoList || _videoList.count == 0) {
        _isHome = YES;
        _currentIndex = 0;
        _pages = 1;
        self.videoList = [NSMutableArray array];
        
        [self requestMoreVideo];//请求数据并加载页面
}
if (_currentIndex>=_videoList.count-3) {
        _pages += 1;
        [self requestMoreVideo];//请求数据并加载页面
    }
- (void)requestMoreVideo { 
    WeakSelf;
    [YBNetworking postWithUrl:url Dic:nil Suc:^(NSDictionary *data, NSString *code,  NSString *msg) {
        if ([code isEqual:@"0"]) {
            NSArray *info = [data valueForKey:@"info"];
            if (_pages==1) {
                [_videoList removeAllObjects];
            }
            [_videoList addObjectsFromArray:info];
            if (_isHome == YES) {
                _isHome = NO;
                _scrollViewOffsetYOnStartDrag = -100;
                [weakSelf scrollViewDidEndScrolling];//加载页面
            }
        }
    } Fail:^(id fail) {
        
    }];
}


结下来我们要介绍加载页面的三种情况,这里我们会用到三个UIImageView,为firstImageView、secondImageView,thirdImageView,对应三个展示UI的View,分别为firstFront、secondFront、thirdFront,对应三个数据源lastHostDic、hostdic、nextHostDic:

第一种是刚进来currentIndex == 0(currentIndex是指当前滚动到第几个视频),这时候我们要设置UIScrollView的ContentOffset为(0,0), currentPlayerIV(当前UIIMageView)为firstImageView,currentFront(当前呈现UI的View)为firstFront。并且要预加载secondImageView的数据,这里不用处理thirdImageView,因为只能向下滑,不需要预加载thirdImageView并且滚到第二个的时候自然给第三个赋值:

//第一个
        [self.backScrollView setContentOffset:CGPointMake(0, 0) animated:NO];
        _currentPlayerIV = _firstImageView;
        _currentFront = _firstFront;
        
        /**
         *  _currentIndex=0时,重新处理下_secondImageView的封面、
         *  不用处理_thirdImageView,因为滚到第二个的时候上面的判断自然给第三个赋值
         */
        [_firstImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_hostdic valueForKey:@"thumb"])]];
        [self setUserData:_hostdic withFront:_firstFront];
        [self setVideoData:_hostdic withFront:_firstFront];
        [_secondImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_nextHostDic valueForKey:@"thumb"])]];
        [self setUserData:_nextHostDic withFront:_secondFront];
        [self setVideoData:_nextHostDic withFront:_secondFront];


这里的setUerData和setVideoData是给页面加载数据的,详细实现为:

-(void)setUserData:(NSDictionary *)dataDic withFront:(FrontView*)front{
 
    NSDictionary *musicDic = [dataDic valueForKey:@"musicinfo"];
    id userinfo = [dataDic valueForKey:@"userinfo"];
    NSString *dataUid;
    NSString *dataIcon;
    NSString *dataUname;
    if ([userinfo isKindOfClass:[NSDictionary class]]) {
        dataUid = [NSString stringWithFormat:@"%@",[userinfo valueForKey:@"id"]];
        dataIcon = [NSString stringWithFormat:@"%@",[userinfo valueForKey:@"avatar"]];//右边最上面的带➕的头像图片
        dataUname = [NSString stringWithFormat:@"@%@",[userinfo valueForKey:@"user_nicename"]];//左下角第一行@的作者名
    }else{
        dataUid = @"0";
        dataIcon = @"";
        dataUname = @"";
    }
    
    NSString *musicID = [NSString stringWithFormat:@"%@",[musicDic valueForKey:@"id"]];
    NSString *musicCover = [NSString stringWithFormat:@"%@",[musicDic valueForKey:@"img_url"]];
    //musicIV右下角转动的唱片上覆盖的歌曲背景图片
    if ([musicID isEqual:@"0"]) {
        [front.musicIV sd_setImageWithURL:[NSURL URLWithString:_hosticon]];
    }else{
        [front.musicIV sd_setImageWithURL:[NSURL URLWithString:musicCover]];
    }
    [front setMusicName:[NSString stringWithFormat:@"%@",[musicDic valueForKey:@"music_format"]]];
    front.titleL.text = [NSString stringWithFormat:@"%@",[dataDic valueForKey:@"title"]];//左下角滚动的文字
    front.nameL.text = dataUname;
    [front.iconBtn sd_setBackgroundImageWithURL:[NSURL URLWithString:dataIcon] forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@"default_head.png"]];
    
    //广告
    NSString *is_ad_str = [NSString stringWithFormat:@"%@",[dataDic valueForKey:@"is_ad"]];
    NSString *ad_url_str = [NSString stringWithFormat:@"%@",[dataDic valueForKey:@"ad_url"]];
    CGFloat ad_img_w = 0;
    if (![PublicObj checkNull:ad_url_str]&&[is_ad_str isEqual:@"1"]&&![PublicObj checkNull:front.titleL.text]) {
        
        NSString *att_text = [NSString stringWithFormat:@"%@    ",front.titleL.text];
        UIImage *ad_link_img = [UIImage imageNamed:@"广告-详情"];
        NSMutableAttributedString *att_img = [NSMutableAttributedString yy_attachmentStringWithContent:ad_link_img contentMode:UIViewContentModeCenter attachmentSize:CGSizeMake(13, 13) alignToFont:SYS_Font(15) alignment:YYTextVerticalAlignmentCenter];
        NSMutableAttributedString *title_att = [[NSMutableAttributedString alloc]initWithString:att_text];
        //NSLog(@"-==-:%@==:%@==img:%@",att_text,title_att,att_img);
        [title_att appendAttributedString:att_img];
        NSRange click_range = [[title_att string] rangeOfString:[att_img string]];
        title_att.yy_font = SYS_Font(15);
        title_att.yy_color = [UIColor whiteColor];
        title_att.yy_lineBreakMode = NSLineBreakByTruncatingHead;
        title_att.yy_kern = [NSNumber numberWithFloat:0.2];
        [title_att addAttribute:NSBackgroundColorAttributeName value:[UIColor clearColor] range:click_range];
        [title_att yy_setTextHighlightRange:click_range color:[UIColor clearColor] backgroundColor:[UIColor clearColor] tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
            //[YBMsgPop showPop:@"1111111"];
            [self adJump:ad_url_str];
        }];
        front.titleL.preferredMaxLayoutWidth =_window_width*0.75;
        front.titleL.attributedText = title_att;
        
        ad_img_w = 30;
    }
    
    //计算名称长度 最长3行高度最大60
    CGSize titleSize = [front.titleL.text boundingRectWithSize:CGSizeMake(_window_width*0.75, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:SYS_Font(15)} context:nil].size;
    CGFloat title_h = titleSize.height>60?60:titleSize.height;
    CGFloat title_w = _window_width*0.75;//titleSize.width>=(_window_width*0.75)?titleSize.width:titleSize.width+ad_img_w;
    front.titleL.frame = CGRectMake(0, front.musicL.top-title_h, title_w, title_h);
    front.nameL.frame = CGRectMake(0, front.titleL.top-25, front.botView.width, 25);
    front.followBtn.frame = CGRectMake(front.iconBtn.left+12, front.iconBtn.bottom-13, 26, 26);
    //广告
    if ([is_ad_str isEqual:@"1"]) {
        front.adLabel.hidden = NO;
        front.adLabel.frame = CGRectMake(0, front.nameL.top-25, 45, 20);
    }else{
        front.adLabel.hidden = YES;
    }
    
}

 

-(void)setVideoData:(NSDictionary *)videoDic withFront:(FrontView*)front{
    _shares =[NSString stringWithFormat:@"%@",[videoDic valueForKey:@"shares"]];
    _likes = [NSString stringWithFormat:@"%@",[videoDic valueForKey:@"likes"]];
    _islike = [NSString stringWithFormat:@"%@",[videoDic valueForKey:@"islike"]];
    _comments = [NSString stringWithFormat:@"%@",[videoDic valueForKey:@"comments"]];
    NSString *isattent = [NSString stringWithFormat:@"%@",[NSString stringWithFormat:@"%@",[videoDic valueForKey:@"isattent"]]];
    //_steps = [NSString stringWithFormat:@"%@",[info valueForKey:@"steps"]];
    WeakSelf;
    //dispatch_async(dispatch_get_main_queue(), ^{
    //点赞数 评论数 分享数
    if ([weakSelf.islike isEqual:@"1"]) {
        [front.likebtn setImage:[UIImage imageNamed:@"home_zan_sel"] forState:0];
        //weakSelf.likebtn.userInteractionEnabled = NO;
    } else{
        [front.likebtn setImage:[UIImage imageNamed:@"home_zan"] forState:0];
        //weakSelf.likebtn.userInteractionEnabled = YES;
    }
    [front.likebtn setTitle:[NSString stringWithFormat:@"%@",_likes] forState:0];
    front.likebtn = [PublicObj setUpImgDownText:front.likebtn];
    [front.enjoyBtn setTitle:[NSString stringWithFormat:@"%@",_shares] forState:0];
    front.enjoyBtn = [PublicObj setUpImgDownText:front.enjoyBtn];
    [front.commentBtn setTitle:[NSString stringWithFormat:@"%@",_comments] forState:0];
    front.commentBtn = [PublicObj setUpImgDownText:front.commentBtn];
    
    if ([[Config getOwnID] isEqual:weakSelf.hostid] || [isattent isEqual:@"1"]) {
        front.followBtn.hidden = YES;
    }else{
        [front.followBtn setImage:[UIImage imageNamed:@"home_follow"] forState:0];
        front.followBtn.hidden = NO;
        [front.followBtn.layer addAnimation:[PublicObj followShowTransition] forKey:nil];
    }
    //});
}


第二种是当你用手滑动的时候,currentIndex > 0 并且小于videoList.count - 1(即既不是第一个也不是最后一个视频),这时候会优先触发代理方法:

#pragma mark - scrollView delegate
//开始拖拽
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    lastContenOffset = scrollView.contentOffset.y;
    //NSLog(@"111=====%f",scrollView.contentOffset.y);
    _currentPlayerIV.jp_progressView.hidden = YES;//当前播放进度隐藏
    self.scrollViewOffsetYOnStartDrag = scrollView.contentOffset.y;//记录开始拖拽的contentoffset
}
//结束拖拽
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
                  willDecelerate:(BOOL)decelerate {
    endDraggingOffset = scrollView.contentOffset.y;//记录结束拖拽的位置
    //NSLog(@"222=====%f",scrollView.contentOffset.y);
    if (decelerate == NO) {
        [self scrollViewDidEndScrolling];
    }
}
 
//开始减速
-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
    scrollView.scrollEnabled = NO;
    //NSLog(@"333=====%f",scrollView.contentOffset.y);
    
}
 
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    
    //NSLog(@"currentIndex=====%.2f",scrollView.contentSize.height);
}
 
//结束减速
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    scrollView.scrollEnabled = YES;
    //NSLog(@"444=====%f",scrollView.contentOffset.y);
    
    if (lastContenOffset < scrollView.contentOffset.y && (scrollView.contentOffset.y-lastContenOffset)>=_window_height) {
        NSLog(@"=====向上滚动=====");
        
        _currentIndex++;
        if (_currentIndex>_videoList.count-1) {
            _currentIndex =_videoList.count-1;
        }
    }else if(lastContenOffset > scrollView.contentOffset.y && (lastContenOffset-scrollView.contentOffset.y)>=_window_height){
        
        NSLog(@"=====向下滚动=====");
        _currentIndex--;
        if (_currentIndex<0) {
            _currentIndex=0;
        }
    }else{
        NSLog(@"=======本页拖动未改变数据=======");
        if (scrollView.contentOffset.y == 0 && _currentIndex==0) {
            [YBMsgPop showPop:@"已经到顶了哦^_^"];
        }else if (scrollView.contentOffset.y == _window_height*2 && _currentIndex==_videoList.count-1){
            [YBMsgPop showPop:@"没有更多了哦^_^"];
        }
    }
    
    _currentPlayerIV.jp_progressView.hidden = NO;
    [self scrollViewDidEndScrolling];
    
    if (_requestUrl) {
        if (_currentIndex>=_videoList.count-3) {
            _pages += 1;
            [self requestMoreVideo];
        }
    }
    
}
 
#pragma mark - Private
 
- (void)scrollViewDidEndScrolling {
    
    if((self.scrollViewOffsetYOnStartDrag == self.backScrollView.contentOffset.y) && (endDraggingOffset!= _scrollViewOffsetYOnStartDrag)){
        
        return;
    }
    //NSLog(@"7-8==%f====%f",self.scrollViewOffsetYOnStartDrag,self.backScrollView.contentOffset.y);
    [self changeRoom];
    
}


 这时当scrollview 滑动自动触发翻页时,则让UIScrollView迅速复位,这时候我们要设置UIScrollView的ContentOffset为(0,_window_height), _window_height为当前屏幕大小,currentPlayerIV(当前UIIMageView)为secondImageView,currentFront(当前呈现UI的View)为secondFront。并且要预加载firstImageView,firstFront和thirdImageView,thirdFront数据的。代码如下:

[_secondImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_hostdic valueForKey:@"thumb"])]];//这里设置视频的第一帧,用于在整个页面显示
    [self setUserData:_hostdic withFront:_secondFront];
    [self setVideoData:_hostdic withFront:_secondFront];

 

[self.backScrollView setContentOffset:CGPointMake(0, _window_height) animated:NO];
        _currentPlayerIV = _secondImageView;
        _currentFront = _secondFront;


 

 if (_curentIndex>0) {
        _lastHostDic = _videoList[_curentIndex-1];
        [_firstImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_lastHostDic valueForKey:@"thumb"])]];
        [self setUserData:_lastHostDic withFront:_firstFront];
        [self setVideoData:_lastHostDic withFront:_firstFront];
    }
    if (_curentIndex < _videoList.count-1) {
        _nextHostDic = _videoList[_curentIndex+1];
        [_thirdImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_nextHostDic valueForKey:@"thumb"])]];
        [self setUserData:_nextHostDic withFront:_thirdFront];
        [self setVideoData:_nextHostDic withFront:_thirdFront];
        
    }


 第三种情况是滚动到最后一个,这时候我们要设置UIScrollView的ContentOffset为(0,_window_height*2), _window_height为当前屏幕大小,currentPlayerIV(当前UIIMageView)为thirdImageView,currentFront(当前呈现UI的View)为thirdFront。并且要预加载secondImageView,secondFront数据的。代码如下:

//最后一个
        [self.backScrollView setContentOffset:CGPointMake(0, _window_height*2) animated:NO];
        _currentPlayerIV = _thirdImageView;
        _currentFront = _thirdFront;
        /**
         *  _currentIndex=_videoList.count-1时,重新处理下_secondImageView的封面、
         *  这个时候只能上滑 _secondImageView 给 _lastHostDic的值
         */
        [_secondImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_lastHostDic valueForKey:@"thumb"])]];
        [self setUserData:_lastHostDic withFront:_secondFront];
        [self setVideoData:_lastHostDic withFront:_secondFront];
        [_thirdImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_hostdic valueForKey:@"thumb"])]];
        [self setUserData:_hostdic withFront:_thirdFront];
        [self setVideoData:_hostdic withFront:_thirdFront];


当三种情况都介绍完后就涉及到短视频软件开发最终的播放了,这里我使用的是JPVideoPlayer播放器(完全开源的,有兴趣的可以自行下载研究原理),播放的主要代码如下:

 

  //切记一定要先把当前播放的上一个关闭
    [_currentPlayerIV jp_stopPlay];
    
    //开始播放
    [_currentPlayerIV jp_playVideoMuteWithURL:[NSURL URLWithString:_playUrl]
                           bufferingIndicator:[JPBufferView new]
                                 progressView:[JPLookProgressView new]
                                configuration:^(UIView *view, JPVideoPlayerModel *playerModel) {
                                    view.jp_muted = NO;//播放器的音频输出是否静音
                                    _firstWatch = YES;
                                    if (_currentPlayerIV.image.size.width>0 && (_currentPlayerIV.image.size.width >= _currentPlayerIV.image.size.height)) {
                                        playerModel.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
                                    }else{
                                        playerModel.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
                                    }
                                }];


下面有几个需要设置的重要代理

1)实现重复播放

//return返回NO以防止重播视频。 返回YES则重复播放视频。如果未实施,则默认为YES
- (BOOL)shouldAutoReplayForURL:(nonnull NSURL *)videoURL
{
   return YES;
}


2)播放状态改变时需要做的相应处理,主要是页面消失的时候停止播放 

//播放状态改变的时候触发
-(void)playerStatusDidChanged:(JPVideoPlayerStatus)playerStatus {
    NSLog(@"=====7-8====%lu",(unsigned long)playerStatus);
    
    if (_stopPlay == YES) {
        NSLog(@"8-4:play-停止了");
        _stopPlay = NO;
        _firstWatch = NO;
        //页面已经消失了,就不要播放了
        [_currentPlayerIV jp_stopPlay];
    }
    
    if (playerStatus == JPVideoPlayerStatusPlaying) {
        if (_bufferIV) {
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [_bufferIV removeFromSuperview];
            });
        }
    }
    if (playerStatus == JPVideoPlayerStatusReadyToPlay && _firstWatch==YES) {
        //addview
        
    }
    if (playerStatus == JPVideoPlayerStatusStop && _firstWatch == YES) {
        //finish
        _firstWatch = NO;
        
    }
    
}