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


  原文:http://www.jianshu.com/p/1b57c02cf9e0


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


服務器

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


nginx.conf

概述

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


文件目錄

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

//
//  HBVideoChatViewController.m
//  視頻聊天
@interface HBVideoChatViewController ()<LFLiveSessionDelegate>
//當前區域網所在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,也是依靠着個類來實現推流的,底層的實現則是對ffmpeg的封裝,有興趣的童鞋可以去研究研究,廢話少說上代碼~

首先,創建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都可以進行定製,配置的高低影響傳輸的速度和質量,具體的文檔都有中文註釋寫得很清楚

接着向系統請求設備授權

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

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

設置播放器播放通知監聽

- (void)initPlayerObserver{
    //監聽網絡狀態改變
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadStateDidChange:) name:IJKMPMoviePlayerLoadStateDidChangeNotification object:self.player];
    //監聽播放網絡狀態改變
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playStateDidChange:) name:IJKMPMoviePlayerPlaybackStateDidChangeNotification object:self.player];
}
//網絡狀態改變通知響應
- (void)loadStateDidChange:(NSNotification *)notification{
    IJKMPMovieLoadState loadState = self.player.loadState;
    if ((loadState & IJKMPMovieLoadStatePlaythroughOK) != 0) {
        NSLog(@"LoadStateDidChange: 可以開始播放的狀態: %d\\n",(int)loadState);
    }else if ((loadState & IJKMPMovieLoadStateStalled) != 0) {
        NSLog(@"loadStateDidChange: IJKMPMovieLoadStateStalled: %d\\n", (int)loadState);
    } else {
        NSLog(@"loadStateDidChange: ???: %d\\n", (int)loadState);
    }
}
//播放狀態改變通知響應
- (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];
}

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

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

#import <UIKit/UIKit.h>

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

輔助文件

在MainViewController中導入剛寫的文件並設置一個alertView來傳入參數

//
//  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 = @"請輸入區域網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"
        //取到文本數據傳值
        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

現在用終端推下桌面測試下,模擬器沒攝像頭所以就沒畫面了╮(╯_╰)╭
在終端輸入
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連接到所搭建服務器的區域網


視頻聊天.gif


測試了下大概有3~5秒的延遲,現在在同一區域網下輸入對方的房間號就可以實現視頻聊天啦~

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

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

最後最後

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



文/衆人皆歡我沉默(簡書作者)
原文鏈接:http://www.jianshu.com/p/1b57c02cf9e0
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。

發佈了17 篇原創文章 · 獲贊 2 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章