webRTC RTCPeerConnection连接建立过程 ---- 基于一个简易demo来分析

前言

本文尝试梳理webRTC连接建立的过程,但是不会一上来就给你个高大上的图,而是基于一个简单的demo运行来展开。

webRTC的demo和服务器部署,我已经介绍了两套:
一个是google官方的:
webRTC Android源码拉取与编译与运行

一个是Janus:
webRTC服务器搭建(基于Janus)与Demo运行

这两种都是可以改一改,直接商业化的。

如果是纯粹学习用,那这里推荐个github找到的简单的demo:
RTCStartupDemo

比起前两个,这个demo只适合学习用,因为非常非常简单。只适合部署在局域网,不需要搭建STUN/TURN服务器。

所以本文就用这demo,来梳理webRTC连接建立的过程。

在开始之前,说个细节^^。就是,前两个demo,都是推荐用WebSocket和信令服务器连接,第三个demo,用socket.io和信令服务器连接。WebSocket和socket.io的优势,就是可以双工通信,即服务器和客户端可以互相收发消息,非常适合做信令交换。 http单向通信,就不是很友好。

1 demo下载与运行

1.1 下载源码与说明

git clone https://github.com/Jhuster/RTCStartupDemo.git

源码包括服务端的和客户端的。
服务端,即信令服务器,使用Golang编写,基于socket.io,因为socket.io可以默认支持房间管理和消息转发。所以不用几行代码,就可以让2个业务方进入同一个房间。

简单的说,信令服务器干的事及其简单,就是客户端发(信令)消息到服务器,服务器帮你转发到其他客户端,完成信令交换。而房间的作用,就是让在同一个房间的多个客户端,谁发消息了,在房间的人都可以收到(信令)消息。

所以,信令服务器,不需要知道信令的内容,只需要帮忙转发就可以了

1.2 为什么用socket.io?

因为socket.io自带房间管理,所以建一个信令服务器,代码量少到让你绝望!

房间要怎么理解呢?你可以认为就是QQ群,QQ群的特点是:发一个消息,其他人都可以看到!也就是消息可以 广播

socket.io接口也很简单,没几个,列举如下:

socket.connect(),客户端的接口,就是进入QQ群,服务端监听socket.on(“connection”),就知道谁来了。
socket.on(“xxx”),客户端和服务端都有的接口,就是监听某个消息,比如群里的"xxx"消息我很关心,我要收!
socket.emit(“xxx”),客户端的接口,就是我发个"xxx"群消息出去!有监听socket.on(“xxx”)的其他客户端可以收到。

1.3 服务编译与运行

服务端编译:

$ cd RTCSignalServer
$ source env.sh 
$ make

服务端运行:

$ cd bin/{platform}
$ ./app server.conf

运行结果:

Listen on: 192.168.1.110:8080
Handle /status
Handle /socket.io

这里打印了信令服务器的IP和访问端口。服务器就是你自己的电脑啦!

1.4 客户端运行

客户端目前支持web和android,我们就尝试让电脑和我们的手机做通话连接。记住,需要连接同一个局域网喔。

1.4.1 web端运行

首先,要把信令服务器的ip地址改一下:
打开RTCClientDemo/Web/one-to-one/js/main.js

var socket = io('http://rtc-signal.jhuster.com:8080/socket.io');

改成

var socket = io('http://localhost:8080/socket.io');

然后,打开
在这里插入图片描述
右键,选择浏览器运行:
在这里插入图片描述
打开后,弹出对话框,随便输入一个房间号,例如123456。点击OK,后面再弹出是否允许使用摄像头,选择允许。然后就会有画面出来。
在这里插入图片描述
ps: 如果你是台式机,不是笔记本电脑,大概率没有摄像头,那可能使用有问题。此时建议搞两个手机,用Android客户端来做通话测试与学习。

另外,上面的状态,还只是进入了房间,还没有开始RTC的连接喔。

1.4.2 Android客户端运行

先把默认的信令服务器地址,修改为本地部署的地址:

diff --git a/RTCClientDemo/Android/RTCDroidDemo/app/src/main/res/values/strings.xml 
-    <string name="default_server">http://rtc-signal.jhuster.com:8080/socket.io/</string>
+    <string name="default_server">http://192.168.1.110:8080/socket.io/</string>

diff --git a/RTCClientDemo/Android/RTCDroidDemo/app/src/main/res/xml/network_security_config.xml
-               <domain includeSubdomains="true">rtc-signal.jhuster.com</domain>
+               <domain includeSubdomains="true">192.168.1.110</domain>

patch如上,尤其是第二个地方,一定要改,否则连接房间时,会遇到这个错误而失败:
Cleartext HTTP traffic not permitted

当然了,我这里的IP是192.168.1.110,你需要按照你的实际情况来,具体见@1.3节。

改好以后,编译,就能跑起来。

运行后,在界面上输入房间号,理论上就进入了房间。

和上面一样,刚打开,只是加入房间,还没有建立RTC的连接喔。

界面下,左上角显示状态,即已经加入房间,或者说,和信令服务器已经建立连接。
在这里插入图片描述

此时点击绿色的通话按钮,就要开始RTC的连接建立过程了!!!
一切顺利的话,将建立成功
在这里插入图片描述

从上可知,一次通话过程,主要包含两个大步骤:

(a) 和信令服务器建立连接,即加入房间
(b) 和其他客户端交换信令,最终连接成功,显示双方界面

下面就按照这2个步骤,来分析代码,梳理连接建立过程。

2 代码分析

2.1 加入房间的代码分析

2.1.1 Android客户端

文件: RTCSignalClient.java

 public void joinRoom(String url, String userId, String roomName) {
        Log.i(TAG, "joinRoom: " + url + ", " + userId + ", " + roomName);
        try {
            mSocket = IO.socket(url);//url就是http://192.168.1.110:8080/socket.io/
            mSocket.connect();
        } catch (URISyntaxException e) {
            e.printStackTrace();
            return;
        }
        mUserId = userId;
        mRoomName = roomName;
        listenSignalEvents();
        try {
            JSONObject args = new JSONObject();
            args.put("userId", userId);
            args.put("roomName", roomName);
            mSocket.emit("join-room", args.toString());//发送"join-room"事件
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

上面的代码简单,一个是连接,一个是连接成功后,向服务端发送消息,亮明身份和想要进入的房间。

2.1.2 服务端

文件: RTCClientDemo/Web/one-to-one/js/main.js

    server.On("connection", func(so socketio.Socket) {//监听连接,每个客户端连接都会走一次回调
        fmt.Printf("new connection, connection id: %s\n", so.Id())//有客户端连接成功,记录客户端的socket id
        so.On("join-room", func(args string) {//监听这个客户端的"join-room"消息
            var room RoomArgs
            err := json.Unmarshal([]byte(args), &room)
            if err != nil {
                fmt.Println(err)
                return
            }
            fmt.Printf("join-room, user: %s, room: %s\n", room.UserId, room.RoomName)
            so.Join(room.RoomName)
            broadcastTo(server, so.Rooms(), "user-joined", room.UserId)
        })
        so.On("leave-room", func(args string) {//监听这个客户端的"leave-room"消息
            var room RoomArgs
            err := json.Unmarshal([]byte(args), &room)
            if err != nil {
                fmt.Println(err)
                return
            }
            fmt.Printf("leave-room, user: %s, room: %s\n", room.UserId, room.RoomName)
            broadcastTo(server, so.Rooms(), "user-left", room.UserId)
            so.Leave(room.RoomName)
        })
        so.On("broadcast", func(msg interface{}) {//监听这个客户端的"broadcast"消息
            broadcastTo(server, so.Rooms(), "broadcast", msg)
        })
        so.On("disconnection", func() {//监听这个客户端断开连接的消息
            fmt.Printf("disconnection, connection id: %s \n", so.Id())
        })
    })

服务端收到connection的消息后,将监听这个客户端的各类子消息,包括加入房间,离开房间,广播等。

可见,代码已经封装好,所谓的房间,并不神秘。

2.2 信令交换

我们以两个Android客户端的通话过程,从代码来说明:
(a) 客户端A点击通话,创建PeerConnection对象,添加音视频轨道,然后收集本地SDP信息,收集成功,发消息给客户端B。精简的伪代码如下

//1.创建PeerConnection对象	
PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(new ArrayList<>());
PeerConnection mPeerConnection = mPeerConnectionFactory.createPeerConnection(configuration, mPeerConnectionObserver);
//2. 添加本地音视频轨道
mPeerConnection.addTrack(mVideoTrack);
mPeerConnection.addTrack(mAudioTrack);
//3. 收集本地sdp信息
mPeerConnection.createOffer(new SimpleSdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
				//设置本地sdp,这个函数将触发底层自动收集ICE信息,然后通过回调告诉上层
                mPeerConnection.setLocalDescription(new SimpleSdpObserver(), sessionDescription);//触发ICE收集
                JSONObject message = new JSONObject();
                try {
                    message.put("userId", RTCSignalClient.getInstance().getUserId());
                    message.put("msgType", RTCSignalClient.MESSAGE_TYPE_OFFER);
                    message.put("sdp", sessionDescription.description);
                    //发送sdp信息,其他客户端会收到
                    RTCSignalClient.getInstance().sendMessage(message);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }, mediaConstraints);

(b) RTCSignalClient发送消息,用socket.io的emit接口

    public void sendMessage(JSONObject message) {
        mSocket.emit("broadcast", message);
    }

© 信令服务器收到消息:

 so.On("broadcast", func(msg interface{}) {
     broadcastTo(server, so.Rooms(), "broadcast", msg)
 })
 
 func broadcastTo(server *Server, rooms []string, event string, msg interface{}) {
    for _, room := range rooms {
        server.BroadcastTo(room, event, msg)
    }
}

从代码看,信令服务器真的非常轻松,都不需要管消息内容,发一个BroadcastTo,通知所有在房间的已连接的客户端就行了!

(d) 客户端B收到了广播的sdp消息,则记录。然后生成本地SDP,反馈给客户端A

//收到其他客户端发来的SDP
private void onRemoteOfferReceived(String userId, JSONObject message) {
    if (mPeerConnection == null) {
        mPeerConnection = createPeerConnection();
    }

    String description = message.getString("sdp");
    mPeerConnection.setRemoteDescription(new SimpleSdpObserver(), new SessionDescription(SessionDescription.Type.OFFER, description));
    doAnswerCall();
}

public void doAnswerCall() {

    MediaConstraints sdpMediaConstraints = new MediaConstraints();
    //收集本地SDP
    mPeerConnection.createAnswer(new SimpleSdpObserver() {
        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {

        mPeerConnection.setLocalDescription(new SimpleSdpObserver(), sessionDescription);//RTCPeerConnection会抛出icecandidate事件
        JSONObject message = new JSONObject();

        message.put("userId", RTCSignalClient.getInstance().getUserId());
        message.put("msgType", RTCSignalClient.MESSAGE_TYPE_ANSWER);
        message.put("sdp", sessionDescription.description);
        //发送本地SDP给客户端A
        RTCSignalClient.getInstance().sendMessage(message);

    }, sdpMediaConstraints);
}

(e) 客户端A收到远端的SDP,记录。

private void onRemoteAnswerReceived(String userId, JSONObject message) {

    String description = message.getString("sdp");
    mPeerConnection.setRemoteDescription(new SimpleSdpObserver(), new SessionDescription(SessionDescription.Type.ANSWER, description));
}

(f) 客户端A拥有远端SDP后,RTC内部将创建一个remote track,某一时刻回调上层

@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
    MediaStreamTrack track = rtpReceiver.track();
    if (track instanceof VideoTrack) {
        Log.i(TAG, "onAddVideoTrack");
        VideoTrack remoteVideoTrack = (VideoTrack) track;
        remoteVideoTrack.setEnabled(true);
        ProxyVideoSink videoSink = new ProxyVideoSink();
        videoSink.setTarget(mRemoteSurfaceView);
        remoteVideoTrack.addSink(videoSink);
    }
}

remoteTrack代表客户端B的流。但注意,这时候只是初始化,还没有真正拉到流,还需要ICE也交换成功!

(g) 客户端A收到ICE回调
在第一步(a),调用了setLocalDescription后,底层会自动收集ICE信息,通过回调返回上层。注意,这个时间可能比(e)还早。什么时候收集好了,就什么时候回调。
收集好了,就发送给客户端B。

@Override
public void onIceCandidate(IceCandidate iceCandidate) {

	JSONObject message = new JSONObject();
	...
	message.put("candidate", iceCandidate.sdp);
	//发送ICE消息
	RTCSignalClient.getInstance().sendMessage(message);
}

信令服务器只负责转发,B将收到消息。

(h) 客户端B收到客户端A的ICE消息,记录

private void onRemoteCandidateReceived(String userId, JSONObject message) {
    IceCandidate remoteIceCandidate = new IceCandidate(message.getString("id"), message.getInt("label"), message.getString("candidate"));
    mPeerConnection.addIceCandidate(remoteIceCandidate);
}

(i) 客户端B收集好ICE消息,也发送给客户端A,A也将收到ICE消息并记录
在(d)步,B将触发收集ICE消息,然后某一个时刻回调到上层,和(g)步是一样的。

2.3 时序图

最后,终于可以上图了!把前面的代码分析内容,画成一张时序图,如下
在这里插入图片描述

小结

1. 信令交换主要是交换SDP/ICE信息

客户端A在发起通话后,收集了本地SDP和ICE信息,发送给客户端B。
客户端B也礼尚往来,收集了本地的SDP和ICE信息,发送给客户端A。
这就是信令交换!

交换完成后,信息都有了,RTC内部也将拉到远端的流数据,开始显示画面。

2. 信令服务器可以不关心信令内容,只负责转发

简易的信令服务器,只需要负责转发就行了,如果要做的复杂一下,那才需要处理信令内容,做一些处理。

3. 本地局域网下,可以不设置STUN/TURN服务器

参考

  1. RTCStartupDemo
  2. https://juejin.cn/post/6844904079102050311
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

newchenxf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值