WebRTC学习(八)1V1音视频实时互动直播系统(2)
接上:WebRTC学习(八)1V1音视频实时互动直播系统
一:客户端状态机
首先我们来看一下直播客户端的一个状态机,客户端与服务器直接通过信令的一个交互之后自然而然的形成一个状态机:
1.最开始的时候状态机是处于初始化状态的,当用户发送了一个join到服务端之后服务端会给它回一个joined消息,客户端在收到joined消息之后就变成了加入状态(joined),这个时候用户是可以离开房间的。当客户端离开的时候会回到初始化或者是离开状态;
2.当一个用户A处于joined的状态的时候,另外一个用户B又进来了,这时用户A就会变成joined_conn状态,就是加入并且可以与对方进行通话的状态。其中用户A的变化就是通过otherjoin消息通知的,当他收到otherjoin的时候它就改变状态从joined到joined_conn,对于后加入的用户B,它还是处于joined这个状态,因为它自己并没有收到otherjoin这个消息,所以房间这两个人其实是两个不同的状态,用户A是先加入房间的,是joiner_conn状态 ,对于后加入的用户B是joined状态。对于join_conn这个状态的用户它也可离开,当他离开的时候它也处于初始化或者离开状态,这同joined状态的情况是一样的。
3.如果用户A需要离开房间,离开的时候它会发送一个bye消息给这个现在还在房间中用户B,用户B收到bye这个消息的时候状态变成joined_unbind,它虽然已经在这个房间内,但是由于另外一个用户已经走了,所以他们直接进行通讯的这个连接已经不需要了,那这个时候需要释放这个连接,所以要将peerconnection这个连接中的相应通道全部进行解绑,所以它就处于joined_unbind状态。
4.如果用户B这个时候处于joined_unbind状态,这时候又有一个新的用户C进入,那么用户B会变成joined_conn状态,而刚进来用户C变成joined的状态。用户B处于join_conn状态的时候,就是说这两个用户直接可以进行通讯了,那么这时候如果用户C在加入后离开了,用户B就回到这个joined_unbind状态。
5.那处于joined_unbind状态那它也可以离开,那这个时候是房间里一个人都没有了,那都处于初始化和离开状态。
经过这样一个状态机之后呢,客户端与服务器之间就可以进行交互了,以上就是整个房间中各个用户状态的变化的一个状态机。
二:客户端流程图
(一)客户端加入流程图(注意结合后面web服务端代码)
1.首先获取这个音视频数据(本地),但如果不能获取到,那就直接失败了,本次通讯肯定不能完成了。
2.但是如果能获取到音视频数据(本地)的时候,就要拿到数据之后,再与这个信令服务器进行连接,然后注册相关的信令处理函数,也就是本机可以处理接收到的服务端信息,比如joined加入成功信息。
3.发送加入房间消息后的分支:
-
无论房间中是否有其他用户(或者房间已经满了),都会走这一步:如果有一个用户A加入的房间,那么他首先要设置状态,说我现在已经处于加入的状态,这时候它要创建并绑定媒体流PeerConnection,创建一个我们最终通讯的通道,并且将媒体流绑定,这是用户加入的情况。
-
如果房间中原本有另外一个用户B,那B要判断现在的状态,如果他现在是一个未绑定状态,他要创建PeerConnection,可能就要跟对方A进行通讯,并且绑定这个他本地的媒体流到这个PeerConnection后面才能进入相应通讯。如果不是这种状态,B是一个joined状态,那这时B就变成了这个join_conn,说明在这之前它已经创建过PeerConnection,并绑定过本地音视频媒体流了,之后所以就不需要绑定,这时候他只要改变状态,变成了join_conn状态,然后变成join_conn状态之后就开始媒体协商,然后收集这个candidate进行连接性检测,最终传输;
-
最后一个分支,就是当这个房间满了,用户C来了(经过分支一)发现房间满了,这时候将C设成离开的状态,并关闭与服务端的连接。注意:因为如果返回full房间满状态,是不会去走分支一的,所以不会去创建peerConnection连接和localStream数据,所以不需要去释放!!
(二)客户端离开流程图
另外还有leave消息,当离开的时候把它分成两种:
1.本地自己发送离开消息:先走中间的分支,关闭掉自己的peerconnection,关闭音视频设备(媒体流),然后等待收到服务器端确认已离开的消息,那这时就变成了leaved初始状态,这时候关闭连接就好了。
2.对端离开后,对方发送bye消息到本地的时候,本地状态就变成了join_unbind状态,同样也要关闭对应的peerconnection。关闭之后其他用户再加入的时候,当本机收到otherjoin消息的时候又会重新创建PeerConnection并进行绑定。
三:端到端连接的基本流程
回顾WebRTC学习(六)端对端传输
四:回顾知识
WebRTC学习(三)WebRTC设备管理
WebRTC学习(四)WebRTC音视频录制
WebRTC学习(五)WebRTC信令服务器
WebRTC学习(六)端对端传输(重点)
区别之处在于:
在其中实战音视频中,是在本机模拟了两端通信,所以在js中生成了pc1、pc2。
在本文中,通过服务器实战音视频通信,不需要再模拟两个peerconnection,只需要实现一个即可!!!
注意:在进行媒体协商之前,我们需要先将流(本地采集的数据)添加到peerConnection连接中去。这样在媒体协商之前,我们才知道有哪些媒体数据。如果先做媒体协商的话,知道这是连接中没有数据媒体流,就不会设置相关底层的接收器、发送器,即使后面设置了媒体流,传递给了peerConnection,他也不会进行媒体传输,所以我们要先添加流
WebRTC学习(七)SDP规范(重点)
注意:candidate存放在SDP的属性中!!
WebRTC学习(八)1V1音视频实时互动直播系统(重点)
五:实现1V1音视频实时互动直播系统
从腾讯云中白嫖一个15天的服务器用于实验:https://cloud.tencent.com/act/free
(一)web服务代码
'use strict' var http = require("http"); var https = require("https"); var fs = require("fs"); var express = require("express"); var serveIndex = require("serve-index"); var socketIo = require("socket.io"); //引入socket.io var USERCOUNT = 3; // var log4js = require('log4js'); //开启日志 var logger = log4js.getLogger(); logger.level = 'info'; var app = express(); //实例化express app.use(serveIndex("./")); //设置首路径,url会直接去访问该目录下的文件 app.use(express.static("./")); //可以访问目录下的所有文件 //https server var options = { key : fs.readFileSync("./ca/learn.webrtc.cn-key.pem"), //同步读取文件key cert: fs.readFileSync("./ca/learn.webrtc.cn.pem"), //同步读取文件证书 }; var https_server = https.createServer(options,app); //绑定socket.io与https服务端 var io = socketIo.listen(https_server); //io是一个节点(站点),内部有多个房间 https_server.listen(443,"0.0.0.0"); //---------实现了两个服务,socket.io与https server;都是绑定在443,复用端口 //-----处理事件 io.sockets.on("connection",(socket)=>{ //处理客户端到达的socket //监听客户端加入、离开房间消息 socket.on("join",(room)=>{ socket.join(room); //客户端加入房间 //io.sockets指io下面的所有客户端 //如果是第一个客户端加入房间(原本房间不存在),则会创建一个新的房间 var myRoom = io.sockets.adapter.rooms[room]; //从socket.io中获取房间 var users = Object.keys(myRoom.sockets).length; //获取所有用户数量 logger.info("the number of user in room is:"+users); //开始回复消息,包含两个数据房间和socket.id信息 if(users < USERCOUNT){ socket.emit("joined",room,socket.id); //给本人 //如果房间有其他人,发送otherjoin消息给他们 if(users > 1){ socket.to(room).emit("otherjoin",room,socket.id); } }else{ //告知人满,别来 socket.leave(room); socket.emit("full",room,socket.id); } }); socket.on("leave",(room)=>{ var myRoom = io.sockets.adapter.rooms[room]; //从socket.io中获取房间 var users = (myRoom)?Object.keys(myRoom.sockets).length:0; //获取所有用户数量 socket.leave(room); //离开房间 logger.info("the number of user in room is:"+(users-1)); socket.emit("leaved",room,socket.id); //给自己发送leaved socket.to(room).emit("bye",room,socket.id); //给其他人发送bye }); socket.on("message",(room,msg)=>{ socket.to(room).emit("message",room,msg); }); });
使用sudo node server.js开启
(二)首页展示页面
/* * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ button { margin: 0 20px 25px 0; vertical-align: top; width: 134px; } textarea { color: #444; font-size: 0.9em; font-weight: 300; height: 20.0em; padding: 5px; width: calc(100% - 10px); } div#getUserMedia { padding: 0 0 8px 0; } div.input { display: inline-block; margin: 0 4px 0 0; vertical-align: top; width: 310px; } div.input > div { margin: 0 0 20px 0; vertical-align: top; } div.output { background-color: #eee; display: inline-block; font-family: 'Inconsolata', 'Courier New', monospace; font-size: 0.9em; padding: 10px 10px 10px 25px; position: relative; top: 10px; white-space: pre; width: 270px; } div#preview { border-bottom: 1px solid #eee; margin: 0 0 1em 0; padding: 0 0 0.5em 0; } div#preview > div { display: inline-block; vertical-align: top; width: calc(50% - 12px); } section#statistics div { display: inline-block; font-family: 'Inconsolata', 'Courier New', monospace; vertical-align: top; width: 308px; } section#statistics div#senderStats { margin: 0 20px 0 0; } section#constraints > div { margin: 0 0 20px 0; } h2 { margin: 0 0 1em 0; } section#constraints label { display: inline-block; width: 156px; } section { margin: 0 0 20px 0; padding: 0 0 15px 0; } video { background: #222; margin: 0 0 0 0; --width: 100%; width: var(--width); height: 225px; } @media screen and (max-width: 720px) { button { font-weight: 500; height: 56px; line-height: 1.3em; width: 90px; } div#getUserMedia { padding: 0 0 40px 0; } section#statistics div { width: calc(50% - 14px); } }
<html> <head> <title> WebRTC PeerConnection </title> <link href="./css/main.css" rel="stylesheet" /> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script> </head> <body> <div> <button id=connserver>Conn SigSer</button> <button id="leave" disabled>Leave</button> </div> <div id="preview"> <div> <h2>Local:</h2> <video autoplay playsinline id="localvideo"></video> </div> <div> <h2>Remote:</h2> <video autoplay playsinline id="remotevideo"></video> </div> </div> </body> <script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script> <script type="text/javascript" src="./js/main.js"></script> </html>
(三)版本一:测试信令服务器,注册信令消息处理函数
'use strict' var http = require("http"); var https = require("https"); var fs = require("fs"); var express = require("express"); var serveIndex = require("serve-index"); var socketIo = require("socket.io"); //引入socket.io var USERCOUNT = 3; // var log4js = require('log4js'); //开启日志 var logger = log4js.getLogger(); logger.level = 'info'; var app = express(); //实例化express app.use(serveIndex("./")); //设置首路径,url会直接去访问该目录下的文件 app.use(express.static("./")); //可以访问目录下的所有文件 //https server var options = { key : fs.readFileSync("./ca/learn.webrtc.com-key.pem"), //同步读取文件key cert: fs.readFileSync("./ca/learn.webrtc.com.pem"), //同步读取文件证书 }; var https_server = https.createServer(options,app); //绑定socket.io与https服务端 var io = socketIo.listen(https_server); //io是一个节点(站点),内部有多个房间 https_server.listen(443,"0.0.0.0"); //---------实现了两个服务,socket.io与https server;都是绑定在443,复用端口 //-----处理事件 io.sockets.on("connection",(socket)=>{ //处理客户端到达的socket //监听客户端加入、离开房间消息 socket.on("join",(room)=>{ socket.join(room); //客户端加入房间 //io.sockets指io下面的所有客户端 //如果是第一个客户端加入房间(原本房间不存在),则会创建一个新的房间 var myRoom = io.sockets.adapter.rooms[room]; //从socket.io中获取房间 var users = Object.keys(myRoom.sockets).length; //获取所有用户数量 logger.info("the number of user in room is:"+users); //开始回复消息,包含两个数据房间和socket.id信息 if(users < USERCOUNT){ socket.emit("joined",room,socket.id); //给本人 //如果房间有其他人,发送otherjoin消息给他们 if(users > 1){ socket.to(room).emit("otherjoin",room); } }else{ //告知人满,别来 socket.leave(room); socket.emit("full",room,socket.id); } }); socket.on("leave",(room)=>{ var myRoom = io.sockets.adapter.rooms[room]; //从socket.io中获取房间 var users = (myRoom)?Object.keys(myRoom.sockets).length:0; //获取所有用户数量 socket.leave(room); //离开房间 logger.info("the number of user in room is:"+(users-1)); socket.emit("leaved",room,socket.id); //给自己发送leaved socket.to(room).emit("bye",room,socket.id); //给其他人发送bye }); socket.on("message",(room,msg)=>{ socket.to(room).emit("message",room,msg); }); });
(四)版本二:测试信令服务器,创建PeerConnection和实现状态机变化
'use strict' var localVideo = document.querySelector("video#localvideo"); var remoteVideo = document.querySelector("video#remotevideo"); var btnConn = document.querySelector("button#connserver"); var btnLeave = document.querySelector("button#leave"); var localStream = null; //保存本地流为全局变量 var socket = null; var roomid = "111111"; var state = "init"; //客户端状态机 var pc = null; //定义全局peerconnection变量 //创建peerconnection,监听一些事件:candidate,当收到candidate事件之后(TURN服务返回),之后转发给另外一端(SIGNAL 服务器实现) //将本端的媒体流加入peerconnection中去 function createPeerConnection(){ console.log("Create RTCPeerConnection!"); if(!pc){ //设置ICEservers var pcConfig = { "iceServers" : [{ 'urls':"turn:82.156.184.3:3478", 'credential':"ssyfj", 'username':"ssyfj" }] } pc = new RTCPeerConnection(pcConfig); pc.onicecandidate = (e)=>{ //处理turn服务返回的candidate信息,媒体协商之后SDP规范中属性获取 if(e.candidate){ //发送candidate消息给对端 console.log("find a new candidate",e.candidate); } }; pc.ontrack = (e)=>{ //获取到远端的轨数据,设置到页面显示 remoteVideo.srcObject = e.streams[0]; } } if(localStream){ //将本端的流加入到peerconnection中去 localStream.getTracks().forEach((track)=>{ pc.addTrack(track,localStream); }); } } //销毁当前peerconnection的流信息 function closeLocalMedia(){ if(localStream && localStream.getTracks()){ localStream.getTracks().forEach((track)=>{ track.stop(); }) } localStream = null; } //关闭peerconnection function closePeerConnection(){ console.log("close RTCPeerConnection"); if(pc){ pc.close(); pc = null; } } function conn(){ socket = io.connect(); //与信令服务器建立连接,io对象是在前端引入的socket.io文件创立的全局对象 //开始注册处理服务端的信令消息 socket.on("joined",(roomid,id)=>{ console.log("receive joined message:",roomid,id); //修改状态 state = "joined"; createPeerConnection(); //加入房间后,创建peerconnection,加入流,等到有新的peerconnection加入,就要进行媒体协商 btnConn.disabled = true; btnLeave.disabled = false; console.log("receive joined message:state=",state); }); socket.on("otherjoin",(roomid,id)=>{ console.log("receive otherjoin message:",roomid,id); //修改状态,注意:对于一个特殊状态joined_unbind状态需要创建新的peerconnection if(state === "joined_unbind"){ createPeerConnection(); } state = "joined_conn"; //原本joined,现在变为conn //媒体协商 console.log("receive otherjoin message:state=",state); }); socket.on("full",(roomid,id)=>{ console.log("receive full message:",roomid,id); state = "leaved"; console.log("receive full message:state=",state); socket.disconnect(); //断开连接,虽然没有加入房间,但是连接还是存在的,所以需要进行关闭 alert("the room is full!"); btnLeave.disabled = true; btnConn.disabled = false; }); socket.on("leaved",(roomid,id)=>{ //------资源的释放在发送leave消息给服务器的时候就释放了,符合离开流程图 console.log("receive leaved message:",roomid,id); state = "leaved"; //初始状态 console.log("receive leaved message:state=",state); //这里断开连接 socket.disconnect(); btnLeave.disabled = true; btnConn.disabled = false; }); socket.on("bye",(roomid,id)=>{ console.log("receive bye message:",roomid,id); state = "joined_unbind"; console.log("receive bye message:state=",state); //开始处理peerconneciton closePeerConnection(); }); socket.on("message",(roomid,data)=>{ console.log("receive client message:",roomid,data); //处理媒体协商数据,进行转发给信令服务器,处理不同类型的数据,如果是流媒体数据,直接p2p转发 }); //开始发送加入消息 socket.emit("join",roomid); return; } function getMediaStream(stream){ localStream = stream; //保存到全局变量,用于传输到对端 localVideo.srcObject = localStream; //显示在页面中,本端 //-------与signal server进行连接,接受信令消息!!------ conn(); } function handleError(err){ console.err(err.name+":"+err.message); } //初始化操作,获取本地音视频数据 function start(){ if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ console.error("the getUserMedia is not support!"); return; }else{ var constraints = { video : true, audio : false }; navigator.mediaDevices.getUserMedia(constraints) .then(getMediaStream) .catch(handleError); } } function connSignalServer(){ //开启本地视频 start(); return true; } function leave(){ if(socket){ socket.emit("leave",roomid); } //释放资源 closePeerConnection(); closeLocalMedia(); btnConn.disabled = false; btnLeave.disabled = true; } //设置触发事件 btnConn.onclick = connSignalServer; //获取本地音视频数据,展示在页面,socket连接建立与信令服务器,注册信令消息处理函数,发送join信息给信令服务器 btnLeave.onclick = leave;
两个主机下的状态变化:
(五)版本三:实现媒体协商,实现端到端通信
'use strict' var localVideo = document.querySelector("video#localvideo"); var remoteVideo = document.querySelector("video#remotevideo"); var btnConn = document.querySelector("button#connserver"); var btnLeave = document.querySelector("button#leave"); var localStream = null; //保存本地流为全局变量 var socket = null; var roomid = "111111"; var state = "init"; //客户端状态机 var pc = null; //定义全局peerconnection变量 function sendMessage(roomid,data){ console.log("send SDP message",roomid,data); if(socket){ socket.emit("message",roomid,data); } } function getOffer(desc){ pc.setLocalDescription(desc); sendMessage(roomid,desc); //发送SDP信息到对端 } //这里我们本机是远端,收到了对方的offer,一会需要把自己本端的数据回去!!!!! function getAnswer(desc){ //在offer获取后,设置了远端描述 pc.setLocalDescription(desc); //这里只需要设置本端了 sendMessage(roomid,desc); } //媒体协商方法,发起方调用,创建offer function call(){ if(state === "joined_conn"){ if(pc){ var options = { offerToReceiveAudio:1, offerToReceiveVideo:1 }; pc.createOffer(options) .then(getOffer) .catch(handleError); } } } //创建peerconnection,监听一些事件:candidate,当收到candidate事件之后(TURN服务返回),之后转发给另外一端(SIGNAL 服务器实现) //将本端的媒体流加入peerconnection中去 function createPeerConnection(){ console.log("Create RTCPeerConnection!"); if(!pc){ //设置ICEservers var pcConfig = { "iceServers" : [{ 'urls':"turn:82.156.184.3:3478", 'credential':"ssyfj", 'username':"ssyfj" }] } pc = new RTCPeerConnection(pcConfig); pc.onicecandidate = (e)=>{ //处理turn服务返回的candidate信息,媒体协商之后SDP规范中属性获取 if(e.candidate){ //发送candidate消息给对端 console.log("find a new candidate",e.candidate); sendMessage(roomid,{ type:"candidate", label:e.candidate.sdpMLineIndex, id:e.candidate.sdpMid, candidate:e.candidate.candidate }); } }; pc.ontrack = (e)=>{ //获取到远端的轨数据,设置到页面显示 remoteVideo.srcObject = e.streams[0]; } } if(localStream){ //将本端的流加入到peerconnection中去 localStream.getTracks().forEach((track)=>{ pc.addTrack(track,localStream); }); } } //销毁当前peerconnection的流信息 function closeLocalMedia(){ if(localStream && localStream.getTracks()){ localStream.getTracks().forEach((track)=>{ track.stop(); }) } localStream = null; } //关闭peerconnection function closePeerConnection(){ console.log("close RTCPeerConnection"); if(pc){ pc.close(); pc = null; } } function conn(){ socket = io.connect(); //与信令服务器建立连接,io对象是在前端引入的socket.io文件创立的全局对象 //开始注册处理服务端的信令消息 socket.on("joined",(roomid,id)=>{ console.log("receive joined message:",roomid,id); //修改状态 state = "joined"; createPeerConnection(); //加入房间后,创建peerconnection,加入流,等到有新的peerconnection加入,就要进行媒体协商 btnConn.disabled = true; btnLeave.disabled = false; console.log("receive joined message:state=",state); }); socket.on("otherjoin",(roomid,id)=>{ console.log("receive otherjoin message:",roomid,id); //修改状态,注意:对于一个特殊状态joined_unbind状态需要创建新的peerconnection if(state === "joined_unbind"){ createPeerConnection(); } state = "joined_conn"; //原本joined,现在变为conn //媒体协商 call(); console.log("receive otherjoin message:state=",state); }); socket.on("full",(roomid,id)=>{ console.log("receive full message:",roomid,id); state = "leaved"; console.log("receive full message:state=",state); socket.disconnect(); //断开连接,虽然没有加入房间,但是连接还是存在的,所以需要进行关闭 alert("the room is full!"); btnLeave.disabled = true; btnConn.disabled = false; }); socket.on("leaved",(roomid,id)=>{ //------资源的释放在发送leave消息给服务器的时候就释放了,符合离开流程图 console.log("receive leaved message:",roomid,id); state = "leaved"; //初始状态 console.log("receive leaved message:state=",state); //这里断开连接 socket.disconnect(); btnLeave.disabled = true; btnConn.disabled = false; }); socket.on("bye",(roomid,id)=>{ console.log("receive bye message:",roomid,id); state = "joined_unbind"; console.log("receive bye message:state=",state); //开始处理peerconneciton closePeerConnection(); }); socket.on("message",(roomid,data)=>{ console.log("receive client message:",roomid,data); //处理媒体协商数据,进行转发给信令服务器,处理不同类型的数据,如果是流媒体数据,直接p2p转发 if(data){ //只有下面3种数据,对于媒体流数据,走的是p2p路线,不经过信令服务器中转 if(data.type === "offer"){ //这里表示我们本机是远端,收到了对方的offer,一会需要把自己本端的数据回去!!!!! pc.setRemoteDescription(new RTCSessionDescription(data)); //需要把传输过来的文本转对象 pc.createAnswer() .then(getAnswer) .catch(handleError); }else if(data.type === "answer"){ pc.setRemoteDescription(new RTCSessionDescription(data)); }else if(data.type === "candidate"){ //在双方设置完成setLocalDescription之后,双方开始交换candidate,每当收集一个candidate之后都会触发pc的onicecandidate事件 var candidate = new RTCIceCandidate({ sdpMLineIndex:data.label, //媒体行的行号 m=video ... candidate:data.candidate }); //生成candidate,是从TURN/STUN服务端获取的,下面开始添加到本地pc中去,用于发送到远端 //将candidate添加到pc pc.addIceCandidate(candidate); //发送到对端,触发对端onicecandidate事件 }else{ console.error("the message is invalid!",data); } } }); //开始发送加入消息 socket.emit("join",roomid); return; } function getMediaStream(stream){ localStream = stream; //保存到全局变量,用于传输到对端 localVideo.srcObject = localStream; //显示在页面中,本端 //-------与signal server进行连接,接受信令消息!!------ conn(); } function handleError(err){ console.error(err.name+":"+err.message); } //初始化操作,获取本地音视频数据 function start(){ if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ console.error("the getUserMedia is not support!"); return; }else{ var constraints = { video : true, audio : false }; navigator.mediaDevices.getUserMedia(constraints) .then(getMediaStream) .catch(handleError); } } function connSignalServer(){ //开启本地视频 start(); return true; } function leave(){ if(socket){ socket.emit("leave",roomid); } //释放资源 closePeerConnection(); closeLocalMedia(); btnConn.disabled = false; btnLeave.disabled = true; } //设置触发事件 btnConn.onclick = connSignalServer; //获取本地音视频数据,展示在页面,socket连接建立与信令服务器,注册信令消息处理函数,发送join信息给信令服务器 btnLeave.onclick = leave;
媒体流数据是通过p2p传输的,不经过信令服务器