场景
公司新项目是一个直播类型的项目,要求实现类似熊猫or斗鱼那种退出直播详情界面衔接一个悬浮(可随意拖动)的播放器继续播放.
考虑到无缝衔接的需求和重新加载延迟缓冲的问题,大体定下一个思路是用一个单例对象来实现这个功能,单例对象包含一个播放器对象和一些需要用的参数等.
效果
实现
播放器使用了网易直播提供的NELivePlayer,集成该播放器可以参考网易官网的集成文档:http://vcloud.163.com/docs/live/player.html 播放器底层使用的是bilibili开源的ijkplaer
注:只能真机调试,模拟器播放器会创建失败.
流程:
1.创建PlayerShowView对象传入直播url
2.PlayerShowView内部获取PlayObj单利对象,传入直播url,获得播放器视图,添加到自身
3.PlayObj获取到直播url,判断是否已经有创建的播放器对象 || 是否是正在播放的直播等做不同操作
4.退出播放详情页-调用delloc时发送一个通知,在根视图控制器接受消息创建悬浮播放器
PlayObj.h
#import <Foundation/Foundation.h>
#import <NELivePlayer/NELivePlayer.h>
#import <NELivePlayer/NELivePlayerController.h>
#import "Masonry.h"
#import <YYKit.h>
#import "UIDevice+XJDevice.h"
#import "MLRefreshView.h"
@protocol PlayObjDelegate <NSObject>
- (void)PlayObjFull;
- (void)PlayObjclose;
- (void)PlayObjRestConnect;
- (void)PlayObjBack;
@end
@interface PlayObj : NSObject
/**
直播播放器
*/
@property(nonatomic, strong) id<NELivePlayer> liveplayer;
/**
播放url
*/
@property (nonatomic, copy) NSString* liveUrl;
/**
是否悬浮窗口播放
*/
@property (nonatomic, assign) BOOL isSuspend;
/**
是否全屏
*/
@property (nonatomic, assign) BOOL isFull;
@property (nonatomic, weak) id<PlayObjDelegate>delagete;
+ (PlayObj*)getInstance;
- (void)shutDown;
@end
PlayObj.m
//
// PlayObj.m
// ijkplayerDemo
//
// Created by sands on 2017/3/5.
// Copyright © 2017年 wanglei. All rights reserved.
//
#import "PlayObj.h"
@interface PlayObj()
/**
返回按钮
*/
@property (nonatomic, weak) UIButton *backButton;
/**
屏幕切换按钮
*/
@property (nonatomic, weak) UIButton *orientationButton;
/**
关闭按钮
*/
@property (nonatomic, weak) UIButton *closeButton;
/**
loadingView
*/
@property (nonatomic, weak) MLRefreshView *indicator;
/**
加载提示
*/
@property (nonatomic, weak) UILabel* lodingTextLabel;
/**
加载失败提示视图
*/
@property (nonatomic, weak) UIView* faildView;
/**
定时器-判断加载超时
*/
@property (nonatomic, weak) NSTimer* inOutTimer;
@property (nonatomic, assign) NSInteger inOutNumber;
@end
static PlayObj *playObj = nil;
#define MAX_LODING_TIME 30 //最大加载时间 超过这个时间显示连接失败提示
@implementation PlayObj
+ (PlayObj*)getInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
playObj = [[PlayObj alloc]init];
});
return playObj;
}
- (instancetype)init
{
self = [super init];
if (self) {
self.inOutTimer = 0;
}
return self;
}
#pragma mark =================defaultUI==================
- (void)defaultWithPlaye{
self.liveplayer = [[NELivePlayerController alloc]
initWithContentURL:[NSURL URLWithString:self.liveUrl]];
if (self.liveplayer == nil) {
NSLog(@"failed to initialize!");
}
self.liveplayer.view.frame = CGRectMake(0, 64, CGRectGetWidth([UIScreen mainScreen].bounds), 210);
self.liveplayer.view.backgroundColor = [UIColor blackColor];
//设置播放缓冲策略,直播采用低延时模式或流畅模式,点播采用抗抖动模式,具体可参见API文档
[self.liveplayer setBufferStrategy:NELPLowDelay];
//设置画面显示模式,默认按原始大小进行播放,具体可参见API文档
[self.liveplayer setScalingMode:NELPMovieScalingModeNone];
//设置视频文件初始化完成后是否自动播放,默认自动播放
[self.liveplayer setShouldAutoplay:YES];
//设置是否开启硬件解码,IOS 8.0以上支持硬件解码,默认为软件解码
[self.liveplayer setHardwareDecoder:YES];
//设置播放器切入后台后时暂停还是继续播放,默认暂停
[self.liveplayer setPauseInBackground:NO];
[self.liveplayer prepareToPlay];
[self defaultOtherUI];
[self initNotification];
}
- (void)defaultOtherUI{
if (_backButton != nil) {
return;
}
@weakify(self);
[self.backButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.top.mas_equalTo(@16);
make.size.mas_equalTo(CGSizeMake(30.f,30.f));
}];
[self.orientationButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.right.bottom.mas_equalTo(@(-16));
make.size.mas_equalTo(CGSizeMake(30.f, 30.f));
}];
[self.indicator mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.mas_equalTo(weak_self.liveplayer.view.mas_centerY);
make.centerX.mas_equalTo(weak_self.liveplayer.view.mas_centerX);
make.size.mas_equalTo(CGSizeMake(20.f,20.f));
}];
[self.lodingTextLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(weak_self.liveplayer.view.mas_centerX);
make.top.equalTo(weak_self.indicator.mas_bottom).with.offset(5);
}];
[self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(@10);
make.right.mas_equalTo(@-10);
make.size.mas_equalTo(CGSizeMake(15.f, 15.f));
}];
[self.faildView mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(weak_self.liveplayer.view);
make.center.mas_equalTo(weak_self.liveplayer.view);
}];
}
#pragma mark sett
/**
传入url初始化播发器
@param liveUrl 直播地址
*/
- (void)setLiveUrl:(NSString *)liveUrl{
_liveUrl = liveUrl;
[self defaultWithPlaye];
}
/**
根据isSuspend展示不同的OtherUI
@param isSuspend 是否悬浮窗口
*/
- (void)setIsSuspend:(BOOL)isSuspend{
_isSuspend = isSuspend;
if (isSuspend) {
self.backButton.hidden = true;
self.orientationButton.hidden = true;
self.closeButton.hidden = false;
}
}
/**
详情页内非全屏不显示返回按钮
@param isFull 是否全屏
*/
- (void)setIsFull:(BOOL)isFull{
_isFull = isFull;
self.backButton.hidden = !_isFull;
self.orientationButton.hidden = false;
self.closeButton.hidden = true;
}
#pragma mark OtherUI (返回 放大 loding 关闭 加载失败)
- (UIButton*)backButton
{
@weakify(self);
if (!_backButton) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:[UIImage imageNamed:@"player_backButton_icon_30x30_"] forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:@"player_backButton_pressIcon_30x30_"] forState:UIControlStateHighlighted];
[button addBlockForControlEvents:UIControlEventTouchUpInside block:^(id _Nonnull sender) {
[weak_self.delagete PlayObjBack];
}];
[self.liveplayer.view addSubview:button];
_backButton = button;
_backButton.hidden = !_isFull;
}
return _backButton;
}
- (UIButton*)orientationButton
{
if (!_orientationButton) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:[UIImage imageNamed:@"player_fullScreen_icon_30x30_"] forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:@"player_fullScreen_pressIcon_30x30_"] forState:UIControlStateHighlighted];
[button addTarget:self action:@selector(scaleFull) forControlEvents:UIControlEventTouchUpInside];
[self.liveplayer.view addSubview:button];
_orientationButton = button;
}
return _orientationButton;
}
- (MLRefreshView*)indicator{
if (!_indicator) {
MLRefreshView* indicator = [MLRefreshView refreshViewWithFrame:CGRectMake(0, 0, 20, 20) logoStyle:RefreshLogoNone];
[self.liveplayer.view addSubview:indicator];
_indicator = indicator;
[self loadingStatus:YES];
}
return _indicator;
}
- (UILabel*)lodingTextLabel{
if (!_lodingTextLabel) {
UILabel* label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, 100, 20)];
label.text = @"走心加载中";
label.backgroundColor = [UIColor clearColor];
label.textColor = [UIColor whiteColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont systemFontOfSize:11];
_lodingTextLabel = label;
[self.liveplayer.view addSubview:label];
}
return _lodingTextLabel;
}
- (UIButton*)closeButton{
@weakify(self);
if (!_closeButton) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:@"X" forState:UIControlStateNormal];
[button setBackgroundColor:[UIColor redColor]];
[button addBlockForControlEvents:UIControlEventTouchUpInside block:^(id _Nonnull sender) {
[weak_self.delagete PlayObjclose];
}];
[self.liveplayer.view addSubview:button];
_closeButton = button;
_closeButton.hidden = !_isSuspend;
}
return _closeButton;
}
- (UIView*)faildView{
@weakify(self);
if (!_faildView) {
UIView* view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)];
UIImageView* image = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"failure"]];
image.tag = 101;
image.frame = CGRectMake(0, 0, 100, 75);
[view addSubview:image];
[image mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(view);
}];
UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc]initWithActionBlock:^(id _Nonnull sender) {
NSLog(@"faild View tap");
[weak_self.delagete PlayObjRestConnect];
_faildView.hidden = true;
}];
[view addGestureRecognizer:tap];
[self.liveplayer.view addSubview:view];
_faildView = view;
_faildView.hidden = true;
}
return _faildView;
}
#pragma mark notify method
- (void)initNotification{
// 播放器媒体流初始化完成后触发,收到该通知表示可以开始播放
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(NELivePlayerDidPreparedToPlay:)
name:NELivePlayerDidPreparedToPlayNotification
object:_liveplayer];
// 播放器加载状态发生变化时触发,如开始缓冲,缓冲结束
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(NeLivePlayerloadStateChanged:)
name:NELivePlayerLoadStateChangedNotification
object:_liveplayer];
// 正常播放结束或播放过程中发生错误导致播放结束时触发的通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(NELivePlayerPlayBackFinished:)
name:NELivePlayerPlaybackFinishedNotification
object:_liveplayer];
// 第一帧视频图像显示时触发的通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(NELivePlayerFirstVideoDisplayed:)
name:NELivePlayerFirstVideoDisplayedNotification
object:_liveplayer];
// 第一帧音频播放时触发的通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(NELivePlayerFirstAudioDisplayed:)
name:NELivePlayerFirstAudioDisplayedNotification
object:_liveplayer];
// 资源释放成功后触发的通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(NELivePlayerReleaseSuccess:)
name:NELivePlayerReleaseSueecssNotification
object:_liveplayer];
// 视频码流解析失败时触发的通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(NELivePlayerVideoParseError:)
name:NELivePlayerVideoParseErrorNotification
object:_liveplayer];
}
#pragma 通知
- (void)NELivePlayerDidPreparedToPlay:(NSNotificationCenter*)not{
NSLog(@"// 播放器媒体流初始化完成后触发,收到该通知表示可以开始播放");
NSLog(@"_liveplayer = %@",_liveplayer);
}
- (void)NeLivePlayerloadStateChanged:(NSNotification*)not{
switch (self.liveplayer.loadState) {
case NELPMovieLoadStatePlayable:
NSLog(@"NELPMovieLoadStatePlayable 播放器初始化完成,可以播放");
break;
case NELPMovieLoadStatePlaythroughOK:{
NSLog(@"NELPMovieLoadStatePlaythroughOK 缓冲完成");
[self loadingStatus:NO];
[self.inOutTimer invalidate];
_inOutNumber = 0;
}
break;
case NELPMovieLoadStateStalled:
NSLog(@"NELPMovieLoadStateStalled 缓冲 展示loding..");
[self loadingStatus:YES];
break;
default:
break;
}
}
- (void)NELivePlayerPlayBackFinished:(NSNotification*)not{
NSLog(@"// 正常播放结束或播放过程中发生错误导致播放结束时触发的通知");
[self showFaildViewWithType:2];
}
- (void)NELivePlayerFirstVideoDisplayed:(NSNotificationCenter*)not{
NSLog(@"// 第一帧视频图像显示时触发的通知");
[self loadingStatus:NO];
[self timeEnd];
}
- (void)NELivePlayerFirstAudioDisplayed:(NSNotificationCenter*)not{
NSLog(@"// 第一帧音频播放时触发的通知");
}
- (void)NELivePlayerReleaseSuccess:(NSNotificationCenter*)not{
NSLog(@"// 资源释放成功后触发的通知");
}
- (void)NELivePlayerVideoParseError:(NSNotificationCenter*)not{
NSLog(@"// 视频码流解析失败时触发的通知");
}
#pragma mark Other Method
- (void)shutDown{
[self.liveplayer shutdown];
[self.liveplayer.view removeFromSuperview];
self.liveplayer = nil;
_liveUrl = @"";
[self removePlaySub];
[self timeEnd];
}
- (void)removePlaySub{
_faildView = nil;
_orientationButton = nil;
_closeButton = nil;
_indicator = nil;
_lodingTextLabel = nil;
_backButton = nil;
}
/**
全屏
*/
- (void)scaleFull{
[self.delagete PlayObjFull];
}
- (void)loadingStatus:(BOOL)status{
_indicator.hidden = !status;
_lodingTextLabel.hidden = _indicator.hidden;
if (status) {
[_indicator startAnimation];
_inOutTimer = 0;
self.inOutTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(checkLiveTimerOut:) userInfo:nil repeats:YES];
}else{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_indicator stopAnimation];
});
}
}
- (void)checkLiveTimerOut:(NSTimer*)timer{
_inOutNumber++;
NSLog(@"checkLiveTimerOut %ld",(long)_inOutNumber);
if (_inOutNumber>=20) {
_indicator.hidden = true;
_lodingTextLabel.hidden = true;
[self.liveplayer stop];
[self showFaildViewWithType:1];
}
}
- (void)timeEnd{
[_inOutTimer invalidate];
_inOutNumber = 0;
_inOutTimer = nil;
}
/**
展示错误提示View
@param type 1:点击重连 2:主播下播
*/
- (void)showFaildViewWithType:(NSInteger)type{
if (type == 1) {
_faildView.hidden = false;
_faildView.userInteractionEnabled = true;
}else if(type == 2){
UIImageView* imageView = [_faildView viewWithTag:101];
if (imageView) {
imageView.image = [UIImage imageNamed:@"live_icon_absent"];
}
_faildView.hidden = false;
_faildView.userInteractionEnabled = false;
[self loadingStatus:false];
}
[self timeEnd];
}
@end
PlayerShowView.h
//
// PlayerShowView.h
// NEPlyaer
//
// Created by fhzx_mac on 2017/3/9.
// Copyright © 2017年 sandsyu. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "Masonry.h"
#import <NELivePlayer/NELivePlayer.h>
#import <NELivePlayer/NELivePlayerController.h>
@interface PlayerShowView : UIView
- (instancetype)initWithFrame:(CGRect)frame connectWithUrl:(NSString*)url;
@property (nonatomic, strong) id<NELivePlayer> liveplayer;
@property (nonatomic, copy) NSString* url;
@property (nonatomic, assign) BOOL isFull;
@property (nonatomic, assign) BOOL isSuspend;
@property (nonatomic, assign) CGRect oldFrame;
@end
PlayerShowView.m
//
// PlayerShowView.m
// NEPlyaer
//
// Created by fhzx_mac on 2017/3/9.
// Copyright © 2017年 sandsyu. All rights reserved.
//
#import "PlayerShowView.h"
#import "PlayObj.h"
@interface PlayerShowView()<PlayObjDelegate>
@end
@implementation PlayerShowView
- (instancetype)initWithFrame:(CGRect)frame connectWithUrl:(NSString*)url
{
self = [super initWithFrame:frame];
if (self) {
self.url = url;
[self defaultUI];
}
return self;
}
- (void)defaultUI{
@weakify(self);
if ([PlayObj getInstance].liveUrl.length<=0) {
[PlayObj getInstance].liveUrl = self.url;
}else{
self.url = [PlayObj getInstance].liveUrl;
}
[PlayObj getInstance].delagete = self;
[self addSubview:[PlayObj getInstance].liveplayer.view];
[self sendSubviewToBack:self.liveplayer.view];
[[PlayObj getInstance].liveplayer.view mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(weak_self);
make.center.mas_equalTo(weak_self);
}];
[PlayObj getInstance].isSuspend = self.isSuspend;
}
- (void)setIsSuspend:(BOOL)isSuspend{
_isSuspend = isSuspend;
[PlayObj getInstance].isSuspend = _isSuspend;
}
- (void)setIsFull:(BOOL)isFull{
_isFull = isFull;
[PlayObj getInstance].isFull = isFull;
}
-(void)PlayObjFull{
@weakify(self);
if (!_isFull) {
weak_self.oldFrame = weak_self.frame;
weak_self.viewController.navigationController.navigationBar.hidden = true;
[UIDevice setOrientation:UIInterfaceOrientationLandscapeRight];
weak_self.frame = weak_self.window.bounds;
weak_self.isFull = true;
}else{
weak_self.viewController.navigationController.navigationBar.hidden = false;
[UIDevice setOrientation:UIInterfaceOrientationPortrait];
weak_self.frame = weak_self.oldFrame;
weak_self.isFull = false;
}
}
- (void)PlayObjclose{
[[PlayObj getInstance]shutDown];
[self removeFromSuperview];
}
- (void)PlayObjRestConnect{
[[PlayObj getInstance]shutDown];
[self defaultUI];
}
- (void)PlayObjBack{
[self PlayObjFull];
}
@end
使用:
PlayerShowView* View = [[PlayerShowView alloc]initWithFrame:CGRectMake(0, 100, self.view.width, self.view.width*0.6)
connectWithUrl:self.liveUrl];
View.isFull = false;
View.isSuspend = false;
[self.view addSubview:View];
具体实现可以参考github上的代码:
https://github.com/yushengchu/NEPlyaer
DEMO使用:
播放器静态库文件过大上传到百度云
https://pan.baidu.com/s/1i4FDtm1
下载解压,放入项目根目录(xcodeproj文件所在目录)运行即可