iOS開發 懸浮窗口播放器簡單實現 類似iPad畫中畫效果

場景

公司新項目是一個直播類型的項目,要求實現類似熊貓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文件所在目錄)運行即可

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,476評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,251評論 4 61
  • 做了一場夢,像站在時光罅隙宇宙流光里看盡了世事變遷,夢卻了無痕跡。當初佛祖菩提樹下一朝悟道明心見性,是不是也會有這...
    將離丶閱讀 235評論 0 3
  • 我看見 遠方新娘的頭紗掉落懸崖 迷蒙的雙眼 讓我從夢中驚醒 冰冷的室溫凍壞了心臟 靈魂漸漸抽離 只留下一架軀體 ...
    東鄰子閱讀 185評論 0 1
  • “我給你好的吃,好的穿,給你買玩具,帶你去游樂園,為什么你又惹我生氣?為什么把家里弄得亂糟糟?為什么?為什么?.....
    小眼豆豆米閱讀 760評論 0 0