用直播(推拉流)模擬實現(xiàn)視頻聊天功能(iOS)

demo已經寫好很久了,懶癌證復發(fā)一直沒上傳~開始進入正題
本文主要是用來練習如何實現(xiàn)直播功能,既推流+拉流,真正的視頻聊天并不是這么做的╮(╯╰)╭ 咱們的目的是學會如何實現(xiàn)直播功能


說下簡單的步驟:搭建本地服務器->推流->拉流->perfect <( ̄ ̄)> 哇哈哈…
實現(xiàn)原理:既向一個服務器同時進行推流和拉流,只不過對應的"房間號"不同而已,比如A和B住在同一棟樓(IP地址),A從B的房間拿東西(拉流)并且A向自己的房間放東西(推流);B向A的得房間拿東西(拉流)并且B向自己的房間放東西(推流);此時只要輸入正確的房間號就可以實現(xiàn)了


服務器

首先你要找到一個測試服務器或者創(chuàng)建本地Nginx服務器,搭建本地服務器請看JJAAIR的文章Mac搭建nginx+rtmp服務器
注意注意,搭建服務器配置nginx.conf文件時,application可以隨便寫,但要記住,后面會用到

nginx.conf

概述

現(xiàn)在開始創(chuàng)建xcode文件吧~推流端用的是LFLiveKit框架,拉流用IJKPlayer,先看下整個文件目錄


文件目錄

是的,沒有看錯,幾個文件就能完成整個推拉流的過程╮(╯╰)╭ 主要實現(xiàn)是HBVideoChatViewController文件

//
//  HBVideoChatViewController.m
//  視頻聊天
@interface HBVideoChatViewController ()<LFLiveSessionDelegate>
//當前區(qū)域網所在IP地址
@property (nonatomic,copy) NSString *ipAddress;
//我的房間號
@property (nonatomic,copy) NSString *myRoom;
//別人的房間號
@property (nonatomic,copy) NSString *othersRoom;
//ip后綴(如果用本地服務器,則為在nginx.conf文件中寫的rtmplive)
@property (nonatomic, copy) NSString *suffix;
//大視圖
@property (nonatomic,weak) UIView *bigView;
//小視圖
@property (nonatomic,weak) UIView *smallView;
//播放器
@property (nonatomic,strong) IJKFFMoviePlayerController *player;
//推流會話
@property (nonatomic,strong) LFLiveSession *session;
@end

推流

LFLiveKit這個推流框架的關鍵類是LFLiveSession,也是依靠著個類來實現(xiàn)推流的,底層的實現(xiàn)則是對ffmpeg的封裝,有興趣的童鞋可以去研究研究,廢話少說上代碼~

首先,創(chuàng)建session并進行一些配置

- (LFLiveSession *)session{
    if (_session == nil) {
        //初始化session要傳入音頻配置和視頻配置
        //音頻的默認配置為:采樣率44.1 雙聲道
        //視頻默認分辨率為360 * 640
        _session = [[LFLiveSession alloc] initWithAudioConfiguration:[LFLiveAudioConfiguration defaultConfiguration] videoConfiguration:[LFLiveVideoConfiguration defaultConfigurationForQuality:LFLiveVideoQuality_Low1] ];
        //設置返回的視頻顯示在指定的view上
        _session.preView = self.smallView;
        _session.delegate = self;
        //是否輸出調試信息
        _session.showDebugInfo = NO;
    }
    return _session;
}

LFLiveAudioConfiguration和LFLiveVideoConfiguration都可以進行定制,配置的高低影響傳輸?shù)乃俣群唾|量,具體的文檔都有中文注釋寫得很清楚

接著向系統(tǒng)請求設備授權

/**
 *  請求攝像頭資源
 */
- (void)requesetAccessForVideo{
    __weak typeof(self) weakSelf = self;
    //判斷授權狀態(tài)
    AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    switch (status) {
        case AVAuthorizationStatusNotDetermined:{
            //發(fā)起授權請求
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        //運行會話
                        [weakSelf.session setRunning:YES];
                    });
                }
            }];
            break;
        }
        case AVAuthorizationStatusAuthorized:{
            //已授權則繼續(xù)
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf.session setRunning:YES];
            });
            break;
        }
        default:
            break;
    }
}

/**
 *  請求音頻資源
 */
- (void)requesetAccessForMedio{
    __weak typeof(self) weakSelf = self;
    //判斷授權狀態(tài)
    AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    switch (status) {
        case AVAuthorizationStatusNotDetermined:{
            //發(fā)起授權請求
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
                if (granted) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        //運行會話
                        [weakSelf.session setRunning:YES];
                    });
                }
            }];
            break;
        }
        case AVAuthorizationStatusAuthorized:{
            //已授權則繼續(xù)
            dispatch_async(dispatch_get_main_queue(), ^{
                [weakSelf.session setRunning:YES];
            });
            break;
        }
        default:
            break;
    }
}

通過代理方法來處理連接異常

//連接錯誤回調
- (void)liveSession:(nullable LFLiveSession *)session errorCode:(LFLiveSocketErrorCode)errorCode{
//彈出警告
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Warning" message:@"連接錯誤,請檢查IP地址后重試" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *sure = [UIAlertAction actionWithTitle:@"sure" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [self.navigationController popViewControllerAnimated:YES];
    }];
    [alert addAction:sure];
    [self presentViewController:alert animated:YES completion:nil];
}

全部設置好就可以開始推流啦~

- (void)viewDidLoad{
    ...
//    推流端
    [self requesetAccessForVideo];
    [self requesetAccessForMedio];
    [self startLive];
    ...
}
- (void)startLive{
    //RTMP要設置推流地址
    LFLiveStreamInfo *streamInfo = [LFLiveStreamInfo new];
    streamInfo.url = [NSString stringWithFormat:@"rtmp://%@:1935/%@/%@",self.ipAddress,self.suffix,self.myRoom];
    [self.session startLive:streamInfo];
}

- (void)stopLive{
    [self.session stopLive];
}

拉流

用IJKPlayer進行拉流,具體的編譯和集成步驟可以看iOS中集成ijkplayer視頻直播框架,也可以直接將我編譯好的IJK拖到項目中即可,在文章最后會給出下載地址

對播放器進行初始化

-(IJKFFMoviePlayerController *)player{
    if (_player == nil) {
        IJKFFOptions *options = [IJKFFOptions optionsByDefault];
        _player = [[IJKFFMoviePlayerController alloc] initWithContentURLString:[NSString stringWithFormat:@"rtmp://%@:1935/%@/%@",self.ipAddress,self.suffix,self.othersRoom] withOptions:options];
        //設置填充模式
        _player.scalingMode = IJKMPMovieScalingModeAspectFill;
        //設置播放視圖
        _player.view.frame = self.bigView.bounds;
        [self.bigView addSubview:_player.view];
        //設置自動播放
        _player.shouldAutoplay = YES;
        
        [_player prepareToPlay];
    }
    return _player;
}

設置播放器播放通知監(jiān)聽

- (void)initPlayerObserver{
    //監(jiān)聽網絡狀態(tài)改變
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadStateDidChange:) name:IJKMPMoviePlayerLoadStateDidChangeNotification object:self.player];
    //監(jiān)聽播放網絡狀態(tài)改變
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playStateDidChange:) name:IJKMPMoviePlayerPlaybackStateDidChangeNotification object:self.player];
}
//網絡狀態(tài)改變通知響應
- (void)loadStateDidChange:(NSNotification *)notification{
    IJKMPMovieLoadState loadState = self.player.loadState;
    if ((loadState & IJKMPMovieLoadStatePlaythroughOK) != 0) {
        NSLog(@"LoadStateDidChange: 可以開始播放的狀態(tài): %d\\n",(int)loadState);
    }else if ((loadState & IJKMPMovieLoadStateStalled) != 0) {
        NSLog(@"loadStateDidChange: IJKMPMovieLoadStateStalled: %d\\n", (int)loadState);
    } else {
        NSLog(@"loadStateDidChange: ???: %d\\n", (int)loadState);
    }
}
//播放狀態(tài)改變通知響應
- (void)playStateDidChange:(NSNotification *)notification{
    switch (_player.playbackState) {
            
        case IJKMPMoviePlaybackStateStopped:
            NSLog(@"IJKMPMoviePlayBackStateDidChange %d: stoped", (int)_player.playbackState);
            break;
            
        case IJKMPMoviePlaybackStatePlaying:
            NSLog(@"IJKMPMoviePlayBackStateDidChange %d: playing", (int)_player.playbackState);
            break;
            
        case IJKMPMoviePlaybackStatePaused:
            NSLog(@"IJKMPMoviePlayBackStateDidChange %d: paused", (int)_player.playbackState);
            break;
            
        case IJKMPMoviePlaybackStateInterrupted:
            NSLog(@"IJKMPMoviePlayBackStateDidChange %d: interrupted", (int)_player.playbackState);
            break;
            
        case IJKMPMoviePlaybackStateSeekingForward:
        case IJKMPMoviePlaybackStateSeekingBackward: {
            NSLog(@"IJKMPMoviePlayBackStateDidChange %d: seeking", (int)_player.playbackState);
            break;
        }
            
        default: {
            NSLog(@"IJKMPMoviePlayBackStateDidChange %d: unknown", (int)_player.playbackState);
            break;
        }
    }
}

接著在viewDidLoad中調用方法并開始播放

- (void)viewDidLoad {
    [super viewDidLoad];
    
    ...
    //    播放端
    [self initPlayerObserver];
    [self.player play];
}

大功告成,現(xiàn)在只要傳入正確的參數(shù)就能實現(xiàn)視頻聊天啦╰( ̄ ̄)╮

//
//  HBVideoChatViewController.h
//  視頻聊天
//
//  Created by apple on 16/8/9.
//  Copyright ? 2016年 yhb. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface HBVideoChatViewController : UIViewController
/**
 *  創(chuàng)建視頻聊天播放器
 *
 *  @param IPAddress  兩個人共同所在的區(qū)域網
 *  @param myRoom     我的推流后綴地址(隨便寫,只要與別人的othersRoom相同即可)
 *  @param othersRoom 別人的推流地址
 *
 */
- (instancetype)initWithIPAddress:(NSString *)ipAddress MyRoom:(NSString *)myRoom othersRoom:(NSString *)othersRoom;
@end

輔助文件

在MainViewController中導入剛寫的文件并設置一個alertView來傳入參數(shù)

//
//  TestViewController.m
//  VideoChat
//
//  Created by apple on 16/8/10.
//  Copyright ? 2016年 yhb. All rights reserved.
//

#import "MainViewController.h"
#import "HBVideoChatViewController.h"
@interface MainViewController ()
@property (nonatomic,copy) NSString *ipAddress;
@property (nonatomic,copy) NSString *myRoom;
@property (nonatomic, copy) NSString *othersRoom;
@end

@implementation MainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    [self setButton];
}

- (void)setButton{
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"跳轉到視頻聊天界面" forState:UIControlStateNormal];
    button.frame = CGRectMake(0, 0, 200, 50);
    button.center = self.view.center;
    [button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)buttonClick{
    //彈出輸入框
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Info" message:@"請輸入詳細信息" preferredStyle:UIAlertControllerStyleAlert];
    [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.placeholder = @"請輸入區(qū)域網IP";
    }];
    [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.placeholder = @"請輸入你的房間號";
    }];
    [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.placeholder = @"請輸入對方的房間號";
    }];
    //點擊確定按鈕跳轉界面
    UIAlertAction *sure = [UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        //                        @"192.168.15.32"
        //取到文本數(shù)據傳值
        HBVideoChatViewController *viewController = [[HBVideoChatViewController alloc] initWithIPAddress:[alert.textFields[0] text] MyRoom:[alert.textFields[1] text] othersRoom:[alert.textFields[2] text]];
        [self.navigationController pushViewController:viewController animated:YES];
    }];
    //取消按鈕
    UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
    [alert addAction:sure];
    [alert addAction:cancel];
    [self presentViewController:alert animated:YES completion:nil];
    
}
@end


現(xiàn)在用終端推下桌面測試下,模擬器沒攝像頭所以就沒畫面了╮(╯_╰)╭
在終端輸入
ffmpeg -f avfoundation -i "1" -vcodec libx264 -preset ultrafast -acodec libfaac -f flv rtmp://localhost:1935/rtmplive/home
將自己的桌面推送到服務器上,然后運行模擬器,輸入對應IP地址,效果如下
rtmp://localhost:1935/rtmplive/home是我本地的服務器,對應的是自己的IP,我的是192.168.15.30,見下圖

注意:用真機測試時,要確保手機wifi連接到所搭建服務器的區(qū)域網

視頻聊天.gif

測試了下大概有35秒的延遲,現(xiàn)在在同一區(qū)域網下輸入對方的房間號就可以實現(xiàn)視頻聊天啦

完整項目:Github
網絡服務器(不一定可用):rtmp://60.174.36.89:1935/live/xxx
打包好的IJKPlayer:https://pan.baidu.com/s/1o7Frs06
下載解壓后直接拖進項目即可

一些關于直播原理和延遲卡頓優(yōu)化的文章:
http://blog.csdn.net/zhonggaorong/article/details/51483282
http://toutiao.com/i6278412629417394689/
http://blog.ucloud.cn/archives/694

最后最后

送上一個個人對直播的見解(一般面試會問的內容)


直播.png

文章對你有幫助的話請在Github幫我點顆星星~有什么疑問請直接留言

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末瞧省,一起剝皮案震驚了整個濱河市骑科,隨后出現(xiàn)的幾起案子隐圾,更是在濱河造成了極大的恐慌盐碱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晾咪,死亡現(xiàn)場離奇詭異,居然都是意外死亡圆存,警方通過查閱死者的電腦和手機怕轿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門谒出,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杀狡,“玉大人恭陡,你說我怎么就攤上這事〔簦” “怎么了毫炉?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵趾疚,是天一觀的道長魄缚。 經常有香客問我仆邓,道長鲜滩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任节值,我火速辦了婚禮徙硅,結果婚禮上,老公的妹妹穿的比我還像新娘搞疗。我一直安慰自己嗓蘑,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布匿乃。 她就那樣靜靜地躺著桩皿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪幢炸。 梳的紋絲不亂的頭發(fā)上泄隔,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音宛徊,去河邊找鬼佛嬉。 笑死,一個胖子當著我的面吹牛闸天,可吹牛的內容都是我干的暖呕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼苞氮,長吁一口氣:“原來是場噩夢啊……” “哼湾揽!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤库物,失蹤者是張志新(化名)和其女友劉穎霸旗,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戚揭,經...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡定硝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了毫目。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡诲侮,死狀恐怖镀虐,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情沟绪,我是刑警寧澤刮便,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站绽慈,受9級特大地震影響恨旱,放射性物質發(fā)生泄漏。R本人自食惡果不足惜坝疼,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一搜贤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钝凶,春花似錦仪芒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哟沫,卻和暖如春饺蔑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嗜诀。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工猾警, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裹虫。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓肿嘲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筑公。 傳聞我的和親對象是個殘疾皇子雳窟,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內容