webRTC连接建立过程----基于一个简易demo来讲解
Posted 长江很多号
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webRTC连接建立过程----基于一个简易demo来讲解相关的知识,希望对你有一定的参考价值。
前言
本文尝试梳理webRTC连接建立的过程,但是不会一上来就给你个高大上的图,而是基于一个简单的demo运行来展开。
webRTC的demo和服务器部署,我已经介绍了两套:
一个是google官方的:
webRTC Android源码拉取与编译与运行
一个是Janus:
webRTC服务器搭建(基于Janus)与Demo运行
这两种都是可以改一改,直接商业化的。
如果是纯粹学习用,那这里推荐个github找到的简单的demo:
RTCStartupDemo。
比起前两个,这个demo只适合学习用,因为非常非常简单。只适合部署在局域网,不需要搭建STUN/TURN服务器。
所以本文就用这demo,来梳理webRTC连接建立的过程。
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)步是一样的。
小结
1. 信令交换主要是交换SDP/ICE信息客户端A在发起通话后,收集了本地SDP和ICE信息,发送给客户端B。
客户端B也礼尚往来,收集了本地的SDP和ICE信息,发送给客户端A。
这就是信令交换!
交换完成后,信息都有了,RTC内部也将拉到远端的流数据,开始显示画面。
2. 信令服务器可以不关心信令内容,只负责转发简易的信令服务器,只需要负责转发就行了,如果要做的复杂一下,那才需要处理信令内容,做一些处理。
3. 本地局域网下,可以不设置STUN/TURN服务器参考
- RTCStartupDemo
- https://juejin.cn/post/6844904079102050311
以上是关于webRTC连接建立过程----基于一个简易demo来讲解的主要内容,如果未能解决你的问题,请参考以下文章