来自 电脑系统 2019-09-11 14:01 的文章
当前位置: 金沙澳门官网网址 > 电脑系统 > 正文

iOS上下滑动切换视频播放的一种思路,仿喵播直

在播放视频时,上下滑动来切换视频源是种很好的体验,就像下图中新浪的微博和新闻客户端,以及inke直播等APP的视频播放页面,很容易让用户产生看完一个再看一个的冲动。

Git:  

图片 1新浪微博视频图片 2inke直播界面

参考:

这种滑动切换播放内容的实现方式,新浪微博来看,可以采用基于TableView的方案,滑动表格,通过一定算法判断哪个Cell的内容应该被播放。inke直播的方案更像是通过scrollView滑动分页的效果。相对而言,TableView的方案当列表中视频数量较多,需要考虑性能上的问题,而scrollView的方案可以参考图片无限轮播的实现方式,只不过不用实现定时翻页,仅需实现滑动翻页。

总结: 

本文用的是scrollView方案,实现类似inke的视频播放界面,滑动切换主播功能。回想用scrollView实现无限图片轮播的方法,可以采用三个imageView,作为scrollView的left,middle,right子视图,滑动scrollView,在-scrollViewDidScroll:(UIScrollView *)scrollView的代理中去实现三个imageView的图片切换,从而达到看上去可以无限滚动的效果。这样的效果也适合用在这里。

弹幕使用Barrage、编码推流使用LFLive(自带美颜、摄像头切换)、拉流解码使用IJK、特效使用粒子动画 。以下内容建议伴随源码观看。

直接说说如何实现。我用的是ijkplayer实现对视频流的解码,参考ijkplayer的Demo,创建一个IJKFFMoviePlayerController对象player作为真正的播放界面,把player的View作为一个subView放在scrollView的中间,scrollView实际上仅仅负责滑动切换player播放的链接,这样整个播放界面只要一个player,就可以实现滑动切换主播的功能。为了方便,可以创建一个继承于scrollView的类PlayerScrollView, 需要在初始化时,指定所需要的frame大小,以及更新需要播放的主播列表和当前主播在列表中的index。PlayerScrollView头文件如下:

宏定义:

//// SamPlayerScrollView.h////#import <UIKit/UIKit.h>@class SamPlayerScrollView;@protocol SamPlayerScrollViewDelegate <NSObject>- playerScrollView:(SamPlayerScrollView *)playerScrollView currentPlayerIndex:(NSInteger)index;@end@interface SamPlayerScrollView : UIScrollView@property (nonatomic, assign) id<SamPlayerScrollViewDelegate> playerDelegate;@property (nonatomic, assign) NSInteger index;- (instancetype)initWithFrame:frame;- updateForLives:(NSMutableArray *)livesArray withCurrentIndex:(NSInteger) index;@end

先判断判断OC语言才执行定义的操作,#ifndef PresfixHeader_pch // 防止头文件被重复引用

其中还定义了一个协议,用于在滑动scrollView时通知外界滑动后的index,以便于player可以切换播放链接。

#define PrefixHeader_pch //引用头文件  NSLog的输出为DeBug时才输出

//// SamPlayerScrollView.m////#import "SamPlayerScrollView.h"#import "SamLive.h"@interface SamPlayerScrollView () <UIScrollViewDelegate>@property (nonatomic, strong) NSMutableArray * lives;@property (nonatomic, strong) SamLive *live;@property (nonatomic, strong) UIImageView *upperImageView, *middleImageView, *downImageView;@property (nonatomic, strong) SamLive *upperLive, *middleLive, *downLive;@property (nonatomic, assign) NSInteger currentIndex;@end@implementation SamPlayerScrollView- (NSMutableArray *)lives{ if  { _lives = [NSMutableArray array]; } return _lives;}- (instancetype)initWithFrame:frame{ self = [super initWithFrame:frame]; if { self.contentSize = CGSizeMake(0, frame.size.height * 3); self.contentOffset = CGPointMake(0, frame.size.height); self.pagingEnabled = YES; self.opaque = YES; self.backgroundColor = [UIColor yellowColor]; self.showsHorizontalScrollIndicator = NO; self.showsVerticalScrollIndicator = NO; self.delegate = self; // image views // blur effect UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; // blur view UIVisualEffectView *visualEffectViewUpper = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; UIVisualEffectView *visualEffectViewMiddle = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; UIVisualEffectView *visualEffectViewDown = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; self.upperImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; self.middleImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, frame.size.height, frame.size.width, frame.size.height)]; self.downImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, frame.size.height*2, frame.size.width, frame.size.height)]; // add image views [self addSubview:self.upperImageView]; [self addSubview:self.middleImageView]; [self addSubview:self.downImageView]; visualEffectViewUpper.frame = self.upperImageView.frame; [self addSubview:visualEffectViewUpper]; visualEffectViewMiddle.frame = self.middleImageView.frame; [self addSubview:visualEffectViewMiddle]; visualEffectViewDown.frame = self.downImageView.frame; [self addSubview:visualEffectViewDown]; } return self;}- updateForLives:(NSMutableArray *)livesArray withCurrentIndex:(NSInteger)index{ if (livesArray.count && [livesArray firstObject]) { [self.lives removeAllObjects]; [self.lives addObjectsFromArray:livesArray]; self.currentIndex = index; _upperLive = [[SamLive alloc] init]; _middleLive = (SamLive *)_lives[_currentIndex]; _downLive = [[SamLive alloc] init]; if (_currentIndex == 0) { _upperLive = (SamLive *)[_lives lastObject]; } else { _upperLive = (SamLive *)_lives[_currentIndex - 1]; } if (_currentIndex == _lives.count - 1) { _downLive = (SamLive *)[_lives firstObject]; } else { _downLive = (SamLive *)_lives[_currentIndex + 1]; } [self prepareForImageView:self.upperImageView withLive:_upperLive]; [self prepareForImageView:self.middleImageView withLive:_middleLive]; [self prepareForImageView:self.downImageView withLive:_downLive]; }}-  prepareForImageView: (UIImageView *)imageView withLive:(SamLive *)live{ // download images for ImageView.}- switchPlayer:(UIScrollView*)scrollView{ CGFloat offset = scrollView.contentOffset.y; if (self.lives.count) { if (offset >= 2*self.frame.size.height) { // slides to the down player scrollView.contentOffset = CGPointMake(0, self.frame.size.height); _currentIndex++; self.upperImageView.image = self.middleImageView.image; self.middleImageView.image = self.downImageView.image; if (_currentIndex == self.lives.count - 1) { _downLive = [self.lives firstObject]; } else if (_currentIndex == self.lives.count) { _downLive = self.lives[1]; _currentIndex = 0; } else { _downLive = self.lives[_currentIndex+1]; } [self prepareForImageView:self.downImageView withLive:_downLive]; if ([self.playerDelegate respondsToSelector:@selector(playerScrollView:currentPlayerIndex:)]) { [self.playerDelegate playerScrollView:self currentPlayerIndex:_currentIndex]; } } else if (offset <= 0) { // slides to the upper player scrollView.contentOffset = CGPointMake(0, self.frame.size.height); _currentIndex--; self.downImageView.image = self.middleImageView.image; self.middleImageView.image = self.upperImageView.image; if (_currentIndex == 0) { _upperLive = [self.lives lastObject]; } else if (_currentIndex == -1) { _upperLive = self.lives[self.lives.count - 2]; _currentIndex = self.lives.count-1; } else { _upperLive = self.lives[_currentIndex - 1]; } [self prepareForImageView:self.upperImageView withLive:_upperLive]; if ([self.playerDelegate respondsToSelector:@selector(playerScrollView:currentPlayerIndex:)]) { [self.playerDelegate playerScrollView:self currentPlayerIndex:_currentIndex]; } } }}-scrollViewDidScroll:(UIScrollView *)scrollView{ [self switchPlayer:scrollView];}@end

showTime(直播)文件夹:

在实现文件中,- (instancetype)initWithFrame:frame初始化了scrollView的frame并添加了三个imageView和三个毛玻璃效果。- updateForLives:(NSMutableArray *)livesArray withCurrentIndex:(NSInteger)index用于更新主播列表和当前index,这样可以直接确定scrollView的middleImageView的image,上面的和下面的ImageView需要注意的是传入的index为边界时的赋值。

实现直播的文件夹,推流使用的LFLive框架,其自身集合了美颜、摄像头切换、转码推流到服务器

最关键的是- switchPlayer:(UIScrollView*)scrollView方法,切换的逻辑在这里,该方法在-scrollViewDidScroll:(UIScrollView *)scrollView中被调用。当向上滑动一个self.frame.size.height距离时,认为scrollView上滑并需要显示下方的downImageView,此时对于upperImageView和middleImageView非常好处理,仅需依次将下方的ImageView移动到上方。问题的关键在于downImageView需要加载的内容,如果当前的_currentIndex等于数据源列表数减1,那么需要把列表的firstObject赋给downImageView;如果当前的_currentIndex等于数据源列表数,说明之前的downImageView的内容已经为列表的firstObject,再向后移动一次为第二个元素,self.lives[1],并且这时候把_currentIndex置0,因为此时中间的middleImageView显示的是列表的firstObject;其他情况下,把列表的下一个元素赋值给downImageView即可。当一次上滑动作完成,调用委托方法,把当前的index反馈回控制器。类似的,当向下滑动一个self.frame.size.height距离时middleImageView需要显示原来在upperImageView中的内容,这时需要注意的边界是_currentIndex为0和-1。

与直播状态的获取等方法

定义好PlayerScrollView之后,就可以在播放界面使用它。在实现文件PlayerViewController.m中,惰性初始化playerScrollView:

本案例通过自己本地搭建的nginx + RTMP服务器与ffmpeg协议来接收推流,通过IJK框架

- (SamPlayerScrollView *)playerScrollView{ if (!_playerScrollView) { _playerScrollView = [[SamPlayerScrollView alloc] initWithFrame:self.view.frame]; _playerScrollView.playerDelegate = self; _playerScrollView.index = self.index; } return _playerScrollView;}

实现拉流解码的操作

调用- updateForLives:(NSMutableArray *)livesArray withCurrentIndex:(NSInteger) index更新数据,并将player的View作为子视图加载到scrollView上[self.playerScrollView addSubview:self.player.view];当有效滑动时,在委托方法中重载播放器:

仿写时发现作者用的LFLive有修改一些文件,而我怎么尝试都失败就只好老老实实pod,而区别仅仅在于直播时的session少了一个推流类型设置为rtmp(本地搭建的服务器,用来接收推流的内容)

- playerScrollView:(SamPlayerScrollView *)playerScrollView currentPlayerIndex:(NSInteger)index{ if (self.index == index) { return; } else { [self reloadPlayerWithLive:self.dataList[index]]; self.index = index; }}

网络状态监听:Reachability

这样就实现了上下滑动切换视频源的功能,总体滑动流畅,开销较小。最后,一个较完整的Demo在这里。

据我所知,很多框架都是使用Reachability来监听网络状态,如网络状态是否改变、有网没网、2G、3G、4G还是WIFI

Main - 主界面

主体控件使用tabbarController搭建,包含三个控制器。分为左侧观看直播,中间开启直播,右侧个人中心

开启直播时需判断摄像头可否使用,麦克风可否使用,当前机型是否支持(模拟器不支持直播)

手机型号通过UIDevice的utsname获得,因为系统提供的型号描述与通常我们自己的描述不太相同,所以通过延展类转换

判断是否有摄像头通过UIImagePickerController

判断摄像头权限通过AVAuthorizationStatus

判断是否有麦克风权限通过AVAudioSession

Home(文件夹)-Hot(子文件夹) 热播

使用TbleView来展示页面,顶端广告轮播图的展示使用XRCarouselView第三方,下拉刷新上拉加载使用的MJ

其中下拉刷新是自定义继承了MJ的RefreshGifHeader,通过继承后重写init完成初始化时带有自定义操作,在通过MJ的Block设置时Block内部会调用[[self alloc]init]实现通过类方法生成一个对象

对于下拉加载、上拉刷新时通过网络请求获取所需数据的参数设置使用CurrentPage,CurrentPage值初始化为1。

下拉刷新时CurrentPage值为1,并清空之前通过数组保存的直播数据,而后获取顶部广告与直播数据

上拉加载时CurrentPage值加1,并获取直播数组数据,停止上拉、下拉的刷新状态,新数据添加到当前数组,随后页数减1恢复到原来值

展示时tableViewCell的第一行为广告,其余为直播展示。

广告图片的点击是跳转到一个WebView,通过请求回来的广告信息中的URL跳转

直播视图的点击事件通过tableView的点击事件来响应跳转到另一个控制器展示直播(ADLiveCollectionViewController)

展示直播的控制器是CollectionViewController类型,以便滑动时切换不同主播

Home-Home 热播、最新、关注所在的View

顶部是UIView挂载三个按钮,通过按钮的点击事件来切换underLine的位置,详情可参考我的上一篇文章:按钮点击切换不同页面

滑动选中的按钮与按钮代表的Type值通过枚举值setSelectedType来保存

底部是ScrollView,其宽度是屏幕宽*3,通过将最新最热关心三个控制器加到自身,与将控制器的View加到ScrollView上达到显示的目的

若滚动切换界面,则滚动结束后需设置导航栏的underLine,若ScrollView滑动的当前页过一半即为下一页

皇冠的点击是跳转到一个WebView, WebView通过一个UIViewController展示,通过自定义初始化方法设置WebView的内容,并通过点语法调用创建WebView

Home-NEW 最新

本文由金沙澳门官网网址发布于电脑系统,转载请注明出处:iOS上下滑动切换视频播放的一种思路,仿喵播直

关键词: