WebRTC-ios 跨平台音视频通话

WebRTC的基础知识和实现原理可以参考这篇文章,ios下音视频通信的实现-基于WebRTC

上述文章里面也有介绍到WebRTC在ios下的环境搭建,这里介绍一个利用Pod导入的方法。WebRTC官网上有说到。

先附上本人自己写的demo下载地址,参照demo,会对这篇文章有更好的理解。WebRTCdemo地址

捷径:下面三步,现在可以一步就可以完成,就是直接在Podfile文件里面pod WebRTCHelper就可以了。这个WebRTCHelper是我传到pod上面的,只作参考

source 'https://github.com/CocoaPods/Specs.git'
target 'WebRTCHelper_pod' do
platform :ios, '9.0'
pod 'WebRTCHelper'
end

终端进入到项目文件夹中,输入:

pod install

运行结果如图:下面三个步骤要导入的工程,全部都pod进项目中了。


pod webRTCHelper

第一步:在你的Podfile文件里面加入下面这段代码

source 'https://github.com/CocoaPods/Specs.git'
target 'YOUR_APPLICATION_TARGET_NAME_HERE' do
  platform :ios, '9.0'
  pod 'GoogleWebRTC'
end

GoogleWebRTC就是WebRTC的ios版本frameWork。

第二步:导入需要用到的WebSocket库;在Podfile文件里面加入

pod 'SocketRocket'

第三步:创建WebRTC的管理类,命名为WebRTCHelper,继承NSObject,这个网上都有,我这里提供了一个WebRTCHelper

完成上面的步骤,基本的工作就做完了,接下来就是测试了。如果是局域网里面测试,我们可以用到前面推荐阅读的那篇文章快到底的地方有个关于说服务器端WebSocket搭建的,有直接给你写好的服务器,只需要简单的操作就可以完成测试服务的搭建。这里就不多阐述。

现在在网络上还有其他的集成方式主要是自己编译WebRTC的源码,然后拉入到自己的项目中,添加依赖库,这个方式比较繁琐,在编译的时候会出现各种问题。既然官方有提供这个pod的库,直接pod是最简单的。


第二部分

WebRTC环境搭建就是这样了!下面具体讲解下WebRTCHelper管理类里面的具体方法和代理:

WebRTCHelper.h文件
  • 1-1.声明一个单例
+(instancetype)shareInstance;
  • 1-2 声明四个public方法
/**
 * 与服务器建立连接
 * @param server 服务器地址
 * @param port 端口号
 * @param room 房间号
 */
-(void)connectServer:(NSString *)server port:(NSString *)port room:(NSString *)room;
/**
 * 切换摄像头
 */
-(void)swichCamera;
/**
 * 是否显示本地视频
 */
-(void)showLocaolCamera;
/**
 * 退出房间
 */
-(void)exitRoom;
  • 1-3 声明两个代理
/*注释*/
@property (nonatomic,weak) id<WebRTCHelperDelegate> delegate;
/*注释*/
@property (nonatomic,weak) id<WebRTCHelperFrindDelegate> friendDelegate;

WebRTCHelperDelegate代理方法:

/**
 * 获取到发送信令消息
 * @param webRTCHelper 本类
 * @param message 消息内容
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper receiveMessage:(NSString *)message;
/**
 * 获取本地的localVideoStream数据(旧版本返回localStream方法)
 * @param webRTCHelper 本类
 * @param steam 视频流
 * @param userId 用户标识
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper setLocalStream:(RTCMediaStream *)steam userId:(NSString *)userId;
/**
 * 获取远程的remoteVideoStream数据
 * @param webRTCHelper 本类
 * @param stream 视频流
 * @param userId 用户标识
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper addRemoteStream:(RTCMediaStream *)stream userId:(NSString *)userId;
/**
 * 某个用户退出后,关闭用户的连接
 * @param webRTCHelper 本类
 * @param userId 用户标识
 */
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper closeWithUserId:(NSString *)userId;

/**
 * 获取socket连接状态
 * @param webRTCHelper 本类
 * @param connectState 连接状态,分为
 WebSocketConnectSuccess 成功,
 WebSocketConnectField, 失败
 WebSocketConnectClosed 关闭
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper socketConnectState:(WebSocketConnectState)connectState;
/**
 * 获取本地视频流的AVCaptureSession(新版本实现本地视频展示代理)
 * @param webRTCHelper
 * @param captureSession AVCaptureSession类
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper capturerSession:(AVCaptureSession *)captureSession;

WebRTCHelperFrindDelegate代理方法,这个代理方法是做房间用户列表所用:

/**
 * 获取房间内所有的用户(除了自己)
 * @param friendList 用户列表
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper gotFriendList:(NSArray *)friendList;
/**
 * 获取新加入的用户信息
 * @param friendId 新用户的id
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper gotNewFriend:(NSString *)friendId;
/**
 * 获取离开房间用户的信息
 * @param friendId 离开用户的ID
 */
-(void)webRTCHelper:(WebRTCHelper *)webRTCHelper removeFriend:(NSString *)friendId;
WebRTCHelper.m文件

.m里面的方法实现就按照整体的实现流程来说明:

  • 1、先与服务器建立起socket连接,这里要借助SRWebSocket库,见方法
/**
 * 与服务器进行连接
 */
- (void)connectServer:(NSString *)server port:(NSString *)port room:(NSString *)room{
    _server = server;
    _room = room;
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%@",server,port]] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20];
    _socket = [[SRWebSocket alloc] initWithURLRequest:request];
    _socket.delegate = self;
    [_socket open];
}
  • 2、在与服务器建立连接之后,会调用SRWebSocket的代理,见代码
/**
 * webSocket连接成功
 */
- (void)webSocketDidOpen:(SRWebSocket *)webSocket{
    NSLog(@"socket连接成功");
    [self joinRoom:_room];
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([self->_delegate respondsToSelector:@selector(webRTCHelper:socketConnectState:)]) {
            [self->_delegate webRTCHelper:self socketConnectState:WebSocketConnectSuccess];
        }
    });
}
/**
 * webSocket连接失败,返回失败原因
 */
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error{
    NSLog(@"socket连接失败");
    dispatch_async(dispatch_get_main_queue(), ^{
        if ([self->_delegate respondsToSelector:@selector(webRTCHelper:socketConnectState:)]) {
            [self->_delegate webRTCHelper:self socketConnectState:WebSocketConnectSuccess];
        }
    });
}
/**
 * webSocket关闭连接,返回关闭的原因reason
 */
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean{
    NSLog(@"socket关闭。code = %ld,reason = %@",code,reason);
}
  • 3、在SRWebSocket的连接成功的代理里做加入房间操作,2步骤处有调用,加入房间就是socket发送一个__join的信令,现在看下加入房间里面的代码实现:
/**
 *  加入房间
 *
 *  @param room 房间号
 */
- (void)joinRoom:(NSString *)room
{
    //如果socket是打开状态
    if (_socket.readyState == SR_OPEN)
    {
        //初始化加入房间的类型参数 room房间号
        NSDictionary *dic = @{@"eventName": @"__join", @"data": @{@"room": room}};
        
        //得到json的data
        NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:nil];
        //发送加入房间的数据
        [_socket send:data];
    }
}
  • 4、调用加入房间joinRoom:时,会发送__join信令到服务器,此时如果房间内有其他的用户,就会返回信令__peers,告诉你房间内有用户,需要建立连接,这个接收__Peers信令方法就是SRWebSocket的接收消息代理方法webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
  • 5、在方法webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message里面的eventName==@"__peers"条件下,做以下操作:
    -- 5-1 对房间内其他的所有用户建立RTCPeerConnction连接[self createPeerConnections];
    -- 5-2 对建立的所有连接添加本地视频流[self addStreams];
    -- 5-3 发送本地的offer给到房间内所有的用户[self createOffers];
    这些操作完成后就可以和房间内其他人建立起视频聊天了;如果房间内没有其他用户的话,就不需要做多余的操作。
  • 6、针对房间内其他用户的加入,会在webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message方法里面收到_new_peer信令,这个时候要针对这个用户创建一个RTCPeerConnection连接,然后给这个链接添加本地视频流,然后设置代理回调;
    -- 6-1、新加入的用户,会发送ice候选地址,就是通过ICEService服务器获取的地址,在webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message方法里面的eventName_ice_candidate条件下有代码和说明;
    -- 6-2、先加入的用户还会发送本身的offer,也就是remote的sdp描述。在接收到新加入用户发的offer的时候,会获取到这个先加入用户的连接peerConnection,然后发送sdp描述对象;这个代码是在webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message方法里面的eventName_offer的条件下。

注意的地方:

这这里需要注意一个方面就是针对最新库和比较旧的库之间在创建本地视频流创建视频约束的时候不一样的地方;

  • 创建本地视频流,在WebRTCHelper.m文件的createLocalStream方法里面的if(device){}条件下
    -- 最新版的库创建本地视频流的方法
//创建RTCVideoSource
RTCVideoSource *videoSource = [_factory videoSource];
//创建RTCCameraVideoCapturer
RTCCameraVideoCapturer * capture = [[RTCCameraVideoCapturer alloc] initWithDelegate:videoSource];
//创建AVCaptureDeviceFormat
AVCaptureDeviceFormat * format = [[RTCCameraVideoCapturer supportedFormatsForDevice:device] lastObject];   
//获取设备的fbs
CGFloat fps = [[format videoSupportedFrameRateRanges] firstObject].maxFrameRate;
//创建RTCVideoTrack
RTCVideoTrack *videoTrack = [_factory videoTrackWithSource:videoSource trackId:@"ARDAMSv0"];
__weak RTCCameraVideoCapturer *weakCapture = capture;
__weak RTCMediaStream * weakStream = _localStream;
__weak NSString * weakMyId = _myId;
//捕获摄像头的视频数据
[weakCapture startCaptureWithDevice:device format:format fps:fps completionHandler:^(NSError * error) {
      NSLog(@"11111111");
      [weakStream addVideoTrack:videoTrack];
      if ([self->_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)])
     {
  //在实现代理中,将weakCapture.captureSession设置到RTCCameraPreviewView的captureSession属性,视频才能显示
         [self->_delegate webRTCHelper:self capturerSession:weakCapture.captureSession];
       }
}];
  • 旧版库创建本视频流的方法
 /*旧版本创建videoSource是RTCAVFoundationVideoSource类,新版是RTCVideoSource,且创建的方法也不一样*/
RTCAVFoundationVideoSource *videoSource = [_factory avFoundationVideoSourceWithConstraints:[self localVideoConstraints]];
//创建RTCVideoTrack,这个方法没有什么变化
RTCVideoTrack *videoTrack = [_factory videoTrackWithSource:videoSource trackId:@"ARDAMSv0"];
//本地流添加RTCVideoTrack
[_localStream addVideoTrack:videoTrack];
//代理回调
 if ([self->_delegate respondsToSelector:@selector(webRTCHelper:setLocalStream:userId:)])
     {
  //将localStream中的videoTracks数据绑定到RTCEAGLVideoView类型localVideoView上,本地视频才会显示
         [self->_delegate webRTCHelper:self setLocalStream:weakStream userId:weakMyId];  
     }

WebRTCHelper.m文件里面就这么多主要的实现;然后看具体怎么去调用这些功能实现音视频通话。

第一、先定义以下的属性

/*保存远端视频流*/
@property (nonatomic,strong) NSMutableDictionary *videoTracks;
/*房间内其他用户*/
@property (nonatomic,strong) NSMutableArray *members;
//显示本地视频的view
@property (weak, nonatomic) IBOutlet RTCCameraPreviewView *localVideoView;

localVideView在最新库下,是RTCCameraPreviewView类型,如果是旧版本的话,是RTCEAGLVideoView类型

然后在viewDidLoad设置WebRTCHelper的代理为self

[WebRTCHelper shareInstance].delegate = self;
 [WebRTCHelper shareInstance].friendDelegate = self;

创建连接,连接到socket服务器上面
viewDidLoad

[self connect];
/**
 * 连接服务器
 */
-(void)connect{
//    [[WebRTCHelper shareInstance] connectServer:@"192.168.30.186" port:@"3000" room:@"100"];
    [[WebRTCHelper shareInstance] connectServer:@"115.236.101.203" port:@"18080" room:@"100"];
}

连接服务成功后,会在WebRTCHelper里面的socket代理里面调用joinRoom方法,然后会在WebRTCHelperDelegateWebRTCHelperFrindDelegate代理里面去获取数据,这里具体说下获取视频流的两个代理方法。

第一个代理方法是获取本地视频流的方法:

  • 旧版库本地代理获取方法:
/**
 * 旧版本获取本地视频流的代理,在这个代理里面会获取到RTCVideoTrack类,然后添加到RTCEAGLVideoView类型的localVideoView上面
 */
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper setLocalStream:(RTCMediaStream *)steam userId:(NSString *)userId{
    if (steam) {
        _localSteam = steam;
        RTCVideoTrack * track = [_localSteam.videoTracks lastObject];
        [track addRenderer:self.localVideoView];
    }
    
}

上面这个获取本地视频流的代理方法只针对旧版本才可以,注意当中的localVideoViewRTCEAGLVideoView类型的

  • 新版库获取本地视频流的方法
    -- 这个方式主要是获取到RTCCameraPreviewViewAVCaptureSession类型的属性数据
/**
 * 新版获取本地视频流的方法
 * @param captureSession RTCCameraPreviewView类的参数,通过设置这个,就可以达到显示本地视频的功能
 */
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper capturerSession:(AVCaptureSession *)captureSession{
    self.localVideoView.captureSession = captureSession;
}

第二就是获取远端视频流的方法:

/**
 * 获取远端视频流的方法,主要是获取到RTCVideoTrack类型的数据,然后保存起来,在刷新列表的时候,添加到对应item里面的RTCEAGLVideoView类型的view上面
 */
- (void)webRTCHelper:(WebRTCHelper *)webRTCHelper addRemoteStream:(RTCMediaStream *)stream userId:(NSString *)userId{
    RTCVideoTrack * track = [stream.videoTracks lastObject];
    if (track != nil) {
        [self.videoTracks setObject:track forKey:userId];
    }
        [self.collectionView reloadData];
}


主要的实现都在这里了,肯定是有不到位的地方,还希望浏览有心人士帮忙指出,共同进步。



常见问题

1、远端视频流显示黑屏或者无法显示问题(只针对客户端,demo中的问题)

针对网友说的出现远端视频看不到或者显示黑屏,这里给出一个解决方法,自测是好的:
修改ViewController里面connect里面的代码,如图:

answer-1.png

注释代码是配置自己公司视频服务器的,对于网友来说是不适用的,所以需要修改成没有注释的代码;
红框里面的服务器地址是针对你电脑的ip地址,查看路劲:偏好设置->网络->以太网的ip
这里修改完之后,还需要进到ChatViewController里面修改connect方法,ip地址和上述一致就可以。
在这之前需要启动视频服务器,这个服务器不是本人搭的,目前有点问题,显示不了视频,但是不影响demo的测试。服务器搭建流程:
下载demo->终端进入到SkyRTC-demo-master->查看REMIND.md文件,跟着REMIND.md文件操作就可以了。服务器要在运行app之前搭建。

注意

注意:本贴中说到的项目运行会存在视频黑屏的问题,目前还没解决,希望各位大牛大咖解决或者有思路的能给我留言。先行谢谢了。
邮箱:wonderfulhzh@163.com
qq:1061023083

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349