場景
公司新項目是一個直播類型的項目,要求實現類似熊貓or斗魚那種退出直播詳情界面銜接一個懸浮(可隨意拖動)的播放器繼續播放.
考慮到無縫銜接的需求和重新加載延遲緩沖的問題,大體定下一個思路是用一個單例對象來實現這個功能,單例對象包含一個播放器對象和一些需要用的參數等.
效果
-w415
實現
播放器使用了網易直播提供的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(@"http:// 播放器媒體流初始化完成后觸發,收到該通知表示可以開始播放");
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(@"http:// 正常播放結束或播放過程中發生錯誤導致播放結束時觸發的通知");
[self showFaildViewWithType:2];
}
- (void)NELivePlayerFirstVideoDisplayed:(NSNotificationCenter*)not{
NSLog(@"http:// 第一幀視頻圖像顯示時觸發的通知");
[self loadingStatus:NO];
[self timeEnd];
}
- (void)NELivePlayerFirstAudioDisplayed:(NSNotificationCenter*)not{
NSLog(@"http:// 第一幀音頻播放時觸發的通知");
}
- (void)NELivePlayerReleaseSuccess:(NSNotificationCenter*)not{
NSLog(@"http:// 資源釋放成功后觸發的通知");
}
- (void)NELivePlayerVideoParseError:(NSNotificationCenter*)not{
NSLog(@"http:// 視頻碼流解析失敗時觸發的通知");
}
#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文件所在目錄)運行即可