iOS短视频加直播项目: 仿抖音的短视频(推荐加热门页面)
之前做过一些短视频和直播项目,但是很多部分使用的是别的公司做好的SDK,由于不想像傻瓜一样不知道具体实现方式的调用来调用去,我决定自己做一个完全开源的,没有任何封装的SDK的短视频加直播项目,接下来的几篇文章我会一一介绍的。
在实现短视频的过程中,我根据市面流行的快手和抖音,实现了这两种都有的短视频实现方式。下面我会先介绍抖音的短视频实现方式。
推荐页面
抖音样式推荐页面整体实现的Gif效果:
首先我先说下底层UI的搭建,我们可以看到推荐,热门,附近这三个可以切换的导航按钮是与下面呈现短视频的UISCrollView联动的(其中推荐和热门我使用的是抖音界面,附近使用的是快手界面),这里我使用了完全开源的TYPagerController三方库,这个三方库的使用很广泛,在滑动切换页面卡顿这方面处理的很好,有兴趣的可以详细去看源码,我在这里简单介绍下实现原理。
我创建的类CustomPagerController继承于三方中的TYTabButtonPagerController,这个滑动导航控制器由两部分组成:
- 上方的TabBar(CollectionCell、UnderLineView构成)
- 下方的ScrollView(ViewController.View构成)
下面说一下滑动导航控制器的总体流程:
- 将ViewController.view加入到下方的ScrollView中
- 根据数据源Titles对上方TabBar中CollectionCell上的Label赋值
- 处理下方ScorllView与上方TabBar之间的协同问题
具体代码实现如下:
CustomPagerController.h
[code]#import "TYTabButtonPagerController.h" @interface CustomPagerController : TYTabButtonPagerController @end
CustomPagerController.m
[code]#import "CustomPagerController.h" #import "RecommendVideoVC.h" #import "HotVideoVC.h" #import "NearByVideoVC.h" //Alan change @interface CustomPagerController(){ } @property(nonatomic,strong)UIButton *search; @property(nonatomic,strong)NSArray *infoArrays; @end @implementation CustomPagerController -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; self.navigationController.navigationBar.hidden = YES; } -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; } -(void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; } - (void)viewDidLoad { [super viewDidLoad]; self.navigationController.navigationBarHidden = YES; [self.view addSubview:self.search]; self.adjustStatusBarHeight = YES; self.cellSpacing = 8; self.infoArrays = [NSArray arrayWithObjects:@"推荐",@"热门",@"附近",nil]; [self setBarStyle:TYPagerBarStyleProgressView]; [self setContentFrame]; } #pragma mark - TYPagerControllerDataSource - (NSInteger)numberOfControllersInPagerController { return self.infoArrays.count; } - (NSString *)pagerController:(TYPagerController *)pagerController titleForIndex:(NSInteger)index { return self.infoArrays[index]; } - (UIViewController *)pagerController:(TYPagerController *)pagerController controllerForIndex:(NSInteger)index { if(index == 0){ //推荐 RecommendVideoVC *videoVC = [[RecommendVideoVC alloc]init]; NSString *url = [purl stringByAppendingFormat:@"?service=Video.getRecommendVideos&uid=%@&type=0",[Config getOwnID]]; videoVC.requestUrl = url; return videoVC; } else if(index == 1) { //热门 HotVideoVC *videoVC= [[HotVideoVC alloc]init]; videoVC.ismyvideo = 0; NSString *url = [purl stringByAppendingFormat:@"?service=Video.getVideoList&uid=%@&type=0",[Config getOwnID]]; videoVC.url = url; return videoVC; }else if(index == 2) { //附近 NearByVideoVC *videoVC= [[NearByVideoVC alloc]init]; return videoVC; }else{ return nil; } } #pragma mark - override delegate - (void)pagerController:(TYTabPagerController *)pagerController configreCell:(TYTabTitleViewCell *)cell forItemTitle:(NSString *)title atIndexPath:(NSIndexPath *)indexPath { [super pagerController:pagerController configreCell:cell forItemTitle:title atIndexPath:indexPath]; } - (void)pagerController:(TYTabPagerController *)pagerController didSelectAtIndexPath:(NSIndexPath *)indexPath{ NSLog(@"wmplayer:===6.28===%ld",(long)indexPath.row); } - (void)pagerController:(TYTabPagerController *)pagerController didScrollToTabPageIndex:(NSInteger)index{ } #pragma mark - set/get -(UIButton *)search { if (!_search) { _search = [UIButton buttonWithType:0]; [_search setImage:[UIImage imageNamed:@"home_search"] forState:0]; _search.frame = CGRectMake(_window_width-50, 20+statusbarHeight, 40, 40); [_search addTarget:self action:@selector(doSearchBtn) forControlEvents:UIControlEventTouchUpInside]; } return _search; } @end
接下来就是重点了,对于抖音界面呈现样式的详细介绍。
整体UI静态图:
我先简单说下UI层级的搭建,它最下面是一个UIScrollView,这是实现上下滑动播放的基础,在这里我们要介绍一个很重要的属性,这是UIScrollView滚动自动换页的关键,就是pagingEnabled属性,一定要设置为YES。UIScrollView的上层是三个UIImageView,这三个UIIMageView是实现无限轮播的关键,播放器就铺在UIImageView上面。最上面是包括点赞,评论,滚动唱片和歌曲名字等的一个UIView,这个UIView会在每一个UIIMageView上放一个,它们都是会预先加载的,这样在滑动的时候就不会有卡顿和画面不流畅的问题。这里多说一句,UIIMageView只是会预先加载视频的第一帧,也就是一张图片,并不会预先加载要播放的视频,只有滑动到每个UIImageView的时候,才会开始加载当前的UIIMageView上的视频。
这是UI主要的代码:
[code]-(UIScrollView *)backScrollView{ if (!_backScrollView) { _backScrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, _window_width, _window_height)]; _backScrollView.contentSize = CGSizeMake(_window_width, _window_height*3); _backScrollView.userInteractionEnabled = YES; _backScrollView.pagingEnabled = YES;//设为YES当滚动的时候会自动跳页 _backScrollView.showsVerticalScrollIndicator = NO; _backScrollView.showsHorizontalScrollIndicator =NO; _backScrollView.delegate = self; _backScrollView.scrollsToTop = NO; _backScrollView.bounces = NO; _backScrollView.backgroundColor = [UIColor clearColor]; _firstImageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, _window_width, _window_height)]; _firstImageView.image = [UIImage imageNamed:@""]; _firstImageView.contentMode = UIViewContentModeScaleAspectFill; _firstImageView.clipsToBounds = YES; [_backScrollView addSubview:_firstImageView]; _firstImageView.jp_videoPlayerDelegate = self; _secondImageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, _window_height, _window_width, _window_height)]; _secondImageView.image = [UIImage imageNamed:@""]; _secondImageView.contentMode = UIViewContentModeScaleAspectFill; _secondImageView.clipsToBounds = YES; [_backScrollView addSubview:_secondImageView]; _secondImageView.jp_videoPlayerDelegate = self; _thirdImageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, _window_height*2, _window_width, _window_height)]; _thirdImageView.image = [UIImage imageNamed:@""]; _thirdImageView.contentMode = UIViewContentModeScaleAspectFill; _thirdImageView.clipsToBounds = YES; [_backScrollView addSubview:_thirdImageView]; _thirdImageView.jp_videoPlayerDelegate = self; WeakSelf; _firstFront = [[FrontView alloc]initWithFrame:_firstImageView.frame callBackEvent:^(NSString *type) { [weakSelf clickEvent:type]; }]; [_backScrollView addSubview:_firstFront]; _secondFront = [[FrontView alloc]initWith 3ff7 Frame:_secondImageView.frame callBackEvent:^(NSString *type) { [weakSelf clickEvent:type]; }]; [_backScrollView addSubview:_secondFront]; _thirdFront = [[FrontView alloc]initWithFrame:_thirdImageView.frame callBackEvent:^(NSString *type) { [weakSelf clickEvent:type]; }]; [_backScrollView addSubview:_thirdFront]; } return _backScrollView; }
基本的UI实现比如点赞按钮一类的我就不介绍了,我主要介绍下左下角滚动的音乐名字和右下角转动的唱片音符这两个动画实现方式。
这两类动画主要是通过核心动画框架QuartzCore来实现的,话不多说看代码:
跑马灯式的左下角音乐名字滚动,实现原理是使用UIView动画使两个紧挨的Label同时向左匀速变动位置。
[code] _label.text = _contentStr; _label2.text = _label.text; CGSize sizee = [PublicObj sizeWithString:_label.text andFont:SYS_Font(15)];//自适应大小 CGFloat withdd = MAX(self.frame.size.width,sizee.width)+20; _label.frame = CGRectMake(0.0, 0.0, withdd, self.frame.size.height); _label2.frame = CGRectMake(withdd, 0.0, withdd, self.frame.size.height); // 动画 [UIView beginAnimations:@"testAnimation" context:NULL]; [UIView setAnimationDuration:3.0f]; [UIView setAnimationCurve:UIViewAnimationCurveLinear]; [UIView setAnimationRepeatCount:999999]; CGRect frame = _label.frame; frame.origin.x = -withdd; _label.frame = frame; CGRect frame2 = _label2.frame; frame2.origin.x = 0.0; _label2.frame = frame2; [UIView commitAnimations];
音符动画的实现原理是通过CAAnimationGroup动画组同时实现移动,放大及渐变透明三个动画,代码中对核心部分进行了详细注释。
[code]#pragma mark - 音符动画组 +(CAAnimationGroup*)caGroup{ //动画组,用来保存一组动画对象 CAAnimationGroup *group = [[CAAnimationGroup alloc]init]; //路径 CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; pathAnimation.calculationMode = kCAAnimationPaced;//kCAAnimationPaced 使得动画均匀进行,而不是按keyTimes设置的或者按关键帧平分时间,此时keyTimes和timingFunctions无效; pathAnimation.fillMode = kCAFillModeForwards;//当动画结束后,layer会一直保持着动画最后的状态 pathAnimation.removedOnCompletion = YES;//默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态 CGMutablePathRef curvedPath = CGPathCreateMutable();//创建一个可变路径 //起点 CGPathMoveToPoint(curvedPath, NULL, 45, 350);//(45,350)起点 //辅助点和终点--- 父视图 85*350(唱片 50*50 ) CGPathAddQuadCurveToPoint(curvedPath, NULL, 8, 340, 16, 290);//(16,290)终点 pathAnimation.path = curvedPath; CGPathRelease(curvedPath); //缩放 CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; NSMutableArray *values = [NSMutableArray array]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1, 1, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.2, 1.2, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.3, 1.3, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.4, 1.4, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.5, 1.5, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.6, 1.6, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.7, 1.7, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.8, 1.8, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.9, 1.9, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 2.0, 1.0)]]; [values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(2.1, 2.1, 1.0)]]; animation.values = values; //透明 CAKeyframeAnimation *opacityAnimaton = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; opacityAnimaton.values = @[@1,@1,@1,@1,@1,@0.9,@0.8,@0.7,@0.6,@0.5,@0.4,@0.3]; group.animations = @[pathAnimation,animation,opacityAnimaton]; group.duration = 3.0; group.repeatCount = MAXFLOAT; group.fillMode = kCAFillModeForwards;//定义定时对象在其活动持续时间之外的行为。 return group; }
唱片旋转动画的原理是使用方法
animationWithKeyPath:对 CABasicAnimation进行实例化,并指定Layer的旋转属性作为关键路径进行注册,使其不断旋转。
[code]#pragma mark - 唱片旋转动画 +(CABasicAnimation*)rotationAnimation { CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; rotate.toValue = @(2 * M_PI);//结束时转动的角度 rotate.duration = 5;//动画时长 rotate.repeatCount = MAXFLOAT;//无限重复 return rotate; }
下面要说的就是重中之重的滑动播放视频了。
当我们首次进入抖音播放短视频页面时,会优先判断当前的视频列表videoList是否有值,如果没有值或当前的视频的index大于videoList.count - 3 时,就会重新请求服务端,获取新的一组短视频。
下面时核心代码
[code]if (!_videoList || _videoList.count == 0) { _isHome = YES; _currentIndex = 0; _pages = 1; self.videoList = [NSMutableArray array]; [self requestMoreVideo];//请求数据并加载页面 }
[code]if (_currentIndex>=_videoList.count-3) { _pages += 1; [self requestMoreVideo];//请求数据并加载页面 }
[code]- (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并且滚到第二个的时候自然给第三个赋值:
[code]//第一个 [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是给页面加载数据的,详细实现为:
[code]-(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; } }
[code]-(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(即既不是第一个也不是最后一个视频),这时候会优先触发代理方法:
[code]#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 *)scro 4000 llView { 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数据的。代码如下:
[code][_secondImageView sd_setImageWithURL:[NSURL URLWithString:minstr([_hostdic valueForKey:@"thumb"])]];//这里设置视频的第一帧,用于在整个页面显示 [self setUserData:_hostdic withFront:_secondFront]; [self setVideoData:_hostdic withFront:_secondFront];
[code][self.backScrollView setContentOffset:CGPointMake(0, _window_height) animated:NO]; _currentPlayerIV = _secondImageView; _currentFront = _secondFront;
[code] 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数据的。代码如下:
[code]//最后一个 [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播放器(完全开源的,有兴趣的可以自行下载研究原理),播放的主要代码如下:
[code] //切记一定要先把当前播放的上一个关闭 [_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)实现重复播放
[code]//return返回NO以防止重播视频。 返回YES则重复播放视频。如果未实施,则默认为YES - (BOOL)shouldAutoReplayForURL:(nonnull NSURL *)videoURL { return YES; }
2)播放状态改变时需要做的相应处理,主要是页面消失的时候停止播放
[code]//播放状态改变的时候触发 -(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; } }
热门页面
抖音样式热门页面整体实现的Gif效果:
这里我们可以看到热门页面只不过是多了一个由UICollectionView呈现多个视频图片的中间界面,点开后播放视频界面其实就是推荐界面,所以这里并没有什么核心难点,只是简单介绍下这个collectionView就好了。
[code]#import "HotVideoVC.h" #import <MJRefresh/MJRefresh.h> #import "RecommendVideoVC.h" #import "NearbyVideoModel.h" #import "VideoCollectionCell.h" #import "AFNetworking.h" @interface HotVideoVC ()<UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout> @property(nonatomic,strong)NSMutableArray *allArray; @property(nonatomic,strong)NSArray *modelrray; @property(nonatomic,strong)UICollectionView *collectionView; @end @implementation HotVideoVC { NSInteger _page; } - (void)viewDidLoad { [super viewDidLoad]; self.automaticallyAdjustsScrollViewInsets = NO; _modelrray = [NSArray array]; _page = 1; self.navigationController.interactivePopGestureRecognizer.delegate = (id) self; self.automaticallyAdjustsScrollViewInsets = NO; self.allArray = [NSMutableArray array]; UICollectionViewFlowLayout *flow = [[UICollectionViewFlowLayout alloc]init]; flow.scrollDirection = UICollectionViewScrollDirectionVertical; flow.itemSize = CGSizeMake(_window_width/2-1, (_window_width/2-1) * 1.4); flow.minimumLineSpacing = 2; flow.minimumInteritemSpacing = 2; self.collectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0,statusbarHeight, _window_width, _window_height-49-statusbarHeight-ShowDiff) collectionViewLayout:flow]; [self.collectionView registerNib:[UINib nibWithNibName:@"VideoCollectionCell" bundle:nil] forCellWithReuseIdentifier:@"VideoCollectionCell"]; self.collectionView.delegate =self; self.collectionView.dataSource = self; self.collectionView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{ _page ++; [self pullInternetforNew:_page]; }]; self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{ _page = 1; [self pullInternetforNew:_page]; }]; [self.view addSubview:self.collectionView]; self.view.backgroundColor = Black_Cor; self.collectionView.backgroundColor = [UIColor blackColor]; [self pullInternetforNew:1]; //因为列表不可以每次 都重新刷新,影响用户体验,也浪费流量 //在视频页面输出视频后返回 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getLiveList:) name:@"delete" object:nil]; //发布视频成功之后返回首页刷新列表 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pullInternetforNewDown) name:@"reloadlist" object:nil]; } //在视频页面删除视频回来后删除 -(void)getLiveList:(NSNotification *)nsnitofition{ NSString *videoid = [NSString stringWithFormat:@"%@",[[nsnitofition userInfo] valueForKey:@"videoid"]]; NSDictionary *deletedic = [NSDictionary dictionary]; for (NSDictionary *subdic in self.allArray) { NSString *videoids = [NSString stringWithFormat:@"%@",[subdic valueForKey:@"id"]]; if ([videoid isEqual:videoids]) { deletedic = subdic; break; } } if (deletedic) { [self.allArray removeObject:deletedic]; [self.collectionView reloadData]; } } -(void)refreshNear{ } //down -(void)pullInternetforNewDown{ self.allArray = [NSMutableArray array]; _page = 1; [self pullInternetforNew:_page]; } -(void)getDataByFooterup{ _page ++; [self pullInternetforNew:_page]; } -(void)pullInternetforNew:(NSInteger)pages{ self.collectionView.userInteractionEnabled = NO; NSString *url = [NSString stringWithFormat:@"%@&p=%ld",_url,(long)pages]; WeakSelf; [YBNetworking postWithUrl:url Dic:nil Suc:^(NSDictionary *data, NSString *code, NSString *msg) { [weakSelf.collectionView.mj_header endRefreshing]; [weakSelf.collectionView.mj_footer endRefreshing]; weakSelf.collectionView.userInteractionEnabled = YES; if ([code isEqual:@"0"]) { NSArray *info = [data valueForKey:@"info"]; if (_page == 1) { [self.allArray removeAllObjects]; } [self.allArray addObjectsFromArray:info]; //加载成功 停止刷新 [self.collectionView.mj_header endRefreshing]; [self.collectionView.mj_footer endRefreshing]; [self.collectionView reloadData]; if (self.allArray.count > 0) { [PublicView hiddenTextNoData:_collectionView]; }else{ [PublicView showTextNoData:_collectionView text1:@"" text2:@"暂无热门视频哦~"]; } if (info.count <= 0) { [self.collectionView.mj_footer endRefreshingWithNoMoreData]; } }else if ([code isEqual:@"700"]){ [PublicObj tokenExpired:minstr([data valueForKey:@"msg"])]; }else{ if (self.allArray) { [self.allArray removeAllObjects]; } [self.collectionView reloadData]; [PublicView showTextNoData:_collectionView text1:@"" text2:@"暂无热门视频哦~"]; } } Fail:^(id fail) { weakSelf.collectionView.userInteractionEnabled = YES; self.collectionView.userInteractionEnabled = YES; if (self.allArray) { [self.allArray removeAllObjects]; } [self.collectionView reloadData]; [PublicView showTextNoData:_collectionView text1:@"" text2:@"暂无热门视频哦~"]; [self.collectionView.mj_header endRefreshing]; [self.collectionView.mj_footer endRefreshing]; }]; } #pragma mark - Table view data source -(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{ return self.allArray.count; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section{ return 2; } -(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{ return 1; } -(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{ VideoCollectionCell *cell = (VideoCollectionCell *)[collectionView cellForItemAtIndexPath:indexPath]; RecommendVideoVC *video = [[RecommendVideoVC alloc]init]; video.fromWhere = @"myVideoV"; video.curentIndex = indexPath.row; video.videoList = _allArray; video.pages = _page; video.firstPlaceImage = cell.bgImageV.image; video.requestUrl = _url; video.block = ^(NSMutableArray *array, NSInteger page,NSInteger index) { _page = page; self.allArray = array; [self.collectionView reloadData]; [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0] atScrollPosition:UICollectionViewScrollPositionBottom animated:NO]; }; // video.hidesBottomBarWhenPushed = YES; [[TCBaseAppDelegate sharedAppDelegate] pushViewController:video animated:YES]; } -(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ VideoCollectionCell *cell = (VideoCollectionCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"VideoCollectionCell" forIndexPath:indexPath]; NSDictionary *subdic = _allArray[indexPath.row]; cell.isList = @"1"; cell.model = [[NearbyVideoModel alloc] initWithDic:subdic]; return cell; } @end
至此抖音样式的短视频已经大体实现了 下篇文章将向大家介绍快手样式的短视频
- iOS视频直播又一大神开源项目、RTMP 协议
- iOS视频直播又一大神开源项目、RTMP 协议
- iOS短视频加直播:仿快手的短视频(附近页面)
- iOS开源项目推荐
- iOS RTMP 视频直播开发笔记(1)----- 采集摄像头图像
- iOS学习——iOS视频和推荐网站
- 推荐一款基于Java的音视频处理开源项目--JAVE
- iOS 简单的视频直播功能开发(实时视音频流录制编码+RTMP传输+实时拉流解码播放)
- iOS开发之集成ijkplayer视频直播
- iOS RTMP上推直播视频
- iOS中 视频直播功能-流媒体的使用
- iOS开源项目推荐|动画
- Ios精品源码,tableview下载视频直播源播放器图片位置3D立体旋转相册屏风动画
- 推荐十个Swift的iOS开源项目
- 基于开源项目的在线网络视频直播项目---一个很好的电视直播开源项目Sopcast
- iOS视频直播初窥:高仿<喵播APP>
- 超强教程:如何搭建一个 iOS 系统的视频直播 App?
- iOS RTMP 视频直播开发笔记(2)----关于MP4及H.264
- iOS之一个超赞的视频直播、第三方库,直播看这个就够了,支持RTMP推流,美颜直播
- iOS直播-播放基于RTMP协议的视频