WebRTC(Web Real-Time Communication)。Real-Time Communication,实时通讯。
WebRTC能让web应用和站点之间选择性地分享音视频流。在不安装其它应用和插件的情况下,完成点对点通信。 WebRTC背后的技术被实现为一个开放的Web标准,并在所有主要浏览器中均以常规JavaScript API的形式提供。对于客户端(例如Android和iOS),可以使用提供相同功能的库。 WebRTC是个开源项目,得到Google,Apple,Microsoft和Mozilla等等公司的支持。2011年6月1日开源并在Google、Mozilla、Opera支持下被纳入万维网联盟的W3C推荐标准。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
大致原理
项目是SpringBoot + Thymeleaf + WebSocket,配置了https,不熟悉的同学可以看我们的《SpringBoot系列》
webrtc.html页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
<!DOCTYPE> <!--解决idea thymeleaf 表达式模板报红波浪线--> <!--suppress ALL --> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>WebRTC + WebSocket</title> <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> <style> html,body{ margin: 0; padding: 0; } #main{ position: absolute; width: 370px; height: 550px; } #localVideo{ position: absolute; background: #757474; top: 10px; right: 10px; width: 100px; height: 150px; z-index: 2; } #remoteVideo{ position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; background: #222; } #buttons{ z-index: 3; bottom: 20px; left: 90px; position: absolute; } #toUser{ border: 1px solid #ccc; padding: 7px 0px; border-radius: 5px; padding-left: 5px; margin-bottom: 5px; } #toUser:focus{ border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6); box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6) } #call{ width: 70px; height: 35px; background-color: #00BB00; border: none; margin-right: 25px; color: white; border-radius: 5px; } #hangup{ width:70px; height:35px; background-color:#FF5151; border:none; color:white; border-radius: 5px; } </style> </head> <body> <div id="main"> <video id="remoteVideo" playsinline autoplay></video> <video id="localVideo" playsinline autoplay muted></video> <div id="buttons"> <input id="toUser" placeholder="输入在线好友账号"/><br/> <button id="call">视频通话</button> <button id="hangup">挂断</button> </div> </div> </body> <!-- 可引可不引 --> <!--<script th:src="@{/js/adapter-2021.js}"></script>--> <script type="text/javascript" th:inline="javascript"> let username = /*[[${username}]]*/''; let localVideo = document.getElementById('localVideo'); let remoteVideo = document.getElementById('remoteVideo'); let websocket = null; let peer = null; WebSocketInit(); ButtonFunInit(); /* WebSocket */ function WebSocketInit(){ //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("wss://172.16.12.156:10086/webrtc/"+username); } else { alert("当前浏览器不支持WebSocket!"); } //连接发生错误的回调方法 websocket.onerror = function (e) { alert("WebSocket连接发生错误!"); }; //连接关闭的回调方法 websocket.onclose = function () { console.error("WebSocket连接关闭"); }; //连接成功建立的回调方法 websocket.onopen = function () { console.log("WebSocket连接成功"); }; //接收到消息的回调方法 websocket.onmessage = async function (event) { let { type, fromUser, msg, sdp, iceCandidate } = JSON.parse(event.data.replace(/\n/g,"\\n").replace(/\r/g,"\\r")); console.log(type); if (type === 'hangup') { console.log(msg); document.getElementById('hangup').click(); return; } if (type === 'call_start') { let msg = "0" if(confirm(fromUser + "发起视频通话,确定接听吗")==true){ document.getElementById('toUser').value = fromUser; WebRTCInit(); msg = "1" } websocket.send(JSON.stringify({ type:"call_back", toUser:fromUser, fromUser:username, msg:msg })); return; } if (type === 'call_back') { if(msg === "1"){ console.log(document.getElementById('toUser').value + "同意视频通话"); //创建本地视频并发送offer let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) localVideo.srcObject = stream; stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); let offer = await peer.createOffer(); await peer.setLocalDescription(offer); let newOffer = offer.toJSON(); newOffer["fromUser"] = username; newOffer["toUser"] = document.getElementById('toUser').value; websocket.send(JSON.stringify(newOffer)); }else if(msg === "0"){ alert(document.getElementById('toUser').value + "拒绝视频通话"); document.getElementById('hangup').click(); }else{ alert(msg); document.getElementById('hangup').click(); } return; } if (type === 'offer') { let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = stream; stream.getTracks().forEach(track => { peer.addTrack(track, stream); }); await peer.setRemoteDescription(new RTCSessionDescription({ type, sdp })); let answer = await peer.createAnswer(); let newAnswer = answer.toJSON(); newAnswer["fromUser"] = username; newAnswer["toUser"] = document.getElementById('toUser').value; websocket.send(JSON.stringify(newAnswer)); await peer.setLocalDescription(answer); return; } if (type === 'answer') { peer.setRemoteDescription(new RTCSessionDescription({ type, sdp })); return; } if (type === '_ice') { peer.addIceCandidate(iceCandidate); return; } } } /* WebRTC */ function WebRTCInit(){ peer = new RTCPeerConnection(); //ice peer.onicecandidate = function (e) { if (e.candidate) { websocket.send(JSON.stringify({ type: '_ice', toUser:document.getElementById('toUser').value, fromUser:username, iceCandidate: e.candidate })); } }; //track peer.ontrack = function (e) { if (e && e.streams) { remoteVideo.srcObject = e.streams[0]; } }; } /* 按钮事件 */ function ButtonFunInit(){ //视频通话 document.getElementById('call').onclick = function (e){ document.getElementById('toUser').style.visibility = 'hidden'; let toUser = document.getElementById('toUser').value; if(!toUser){ alert("请先指定好友账号,再发起视频通话!"); return; } if(peer == null){ WebRTCInit(); } websocket.send(JSON.stringify({ type:"call_start", fromUser:username, toUser:toUser, })); } //挂断 document.getElementById('hangup').onclick = function (e){ document.getElementById('toUser').style.visibility = 'unset'; if(localVideo.srcObject){ const videoTracks = localVideo.srcObject.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); localVideo.srcObject.removeTrack(videoTrack); }); } if(remoteVideo.srcObject){ const videoTracks = remoteVideo.srcObject.getVideoTracks(); videoTracks.forEach(videoTrack => { videoTrack.stop(); remoteVideo.srcObject.removeTrack(videoTrack); }); //挂断同时,通知对方 websocket.send(JSON.stringify({ type:"hangup", fromUser:username, toUser:document.getElementById('toUser').value, })); } if(peer){ peer.ontrack = null; peer.onremovetrack = null; peer.onremovestream = null; peer.onicecandidate = null; peer.oniceconnectionstatechange = null; peer.onsignalingstatechange = null; peer.onicegatheringstatechange = null; peer.onnegotiationneeded = null; peer.close(); peer = null; } localVideo.srcObject = null; remoteVideo.srcObject = null; } } </script> </html> |
Controller页面跳转
1 2 3 4 5 6 7 8 9 10 |
/** * WebRTC + WebSocket */ @RequestMapping("webrtc/{username}.html") public ModelAndView socketChartPage(@PathVariable String username) { ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName("webrtc.html"); modelAndView.addObject("username",username); return modelAndView; } |
WebSocket服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * WebRTC + WebSocket */ @Slf4j @Component @ServerEndpoint(value = "/webrtc/{username}", configurator = MyEndpointConfigure.class) public class WebRtcWSServer { /** * 连接集合 */ private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("username") String username, @PathParam("publicKey") String publicKey) { sessionMap.put(username, session); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { for (Map.Entry<String, Session> entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); break; } } } /** * 发生错误时调用 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 服务器接收到客户端消息时调用的方法 */ @OnMessage public void onMessage(String message, Session session) { try{ //jackson ObjectMapper mapper = new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //JSON字符串转 HashMap HashMap hashMap = mapper.readValue(message, HashMap.class); //消息类型 String type = (String) hashMap.get("type"); //to user String toUser = (String) hashMap.get("toUser"); Session toUserSession = sessionMap.get(toUser); String fromUser = (String) hashMap.get("fromUser"); //msg String msg = (String) hashMap.get("msg"); //sdp String sdp = (String) hashMap.get("sdp"); //ice Map iceCandidate = (Map) hashMap.get("iceCandidate"); HashMap<String, Object> map = new HashMap<>(); map.put("type",type); //呼叫的用户不在线 if(toUserSession == null){ toUserSession = session; map.put("type","call_back"); map.put("fromUser","系统消息"); map.put("msg","Sorry,呼叫的用户不在线!"); send(toUserSession,mapper.writeValueAsString(map)); return; } //对方挂断 if ("hangup".equals(type)) { map.put("fromUser",fromUser); map.put("msg","对方挂断!"); } //视频通话请求 if ("call_start".equals(type)) { map.put("fromUser",fromUser); map.put("msg","1"); } //视频通话请求回应 if ("call_back".equals(type)) { map.put("fromUser",toUser); map.put("msg",msg); } //offer if ("offer".equals(type)) { map.put("fromUser",toUser); map.put("sdp",sdp); } //answer if ("answer".equals(type)) { map.put("fromUser",toUser); map.put("sdp",sdp); } //ice if ("_ice".equals(type)) { map.put("fromUser",toUser); map.put("iceCandidate",iceCandidate); } send(toUserSession,mapper.writeValueAsString(map)); }catch(Exception e){ e.printStackTrace(); } } /** * 封装一个send方法,发送消息到前端 */ private void send(Session session, String message) { try { System.out.println(message); session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } |
WebSocket服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * WebRTC + WebSocket */ @Slf4j @Component @ServerEndpoint(value = "/webrtc/{username}", configurator = MyEndpointConfigure.class) public class WebRtcWSServer { /** * 连接集合 */ private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("username") String username, @PathParam("publicKey") String publicKey) { sessionMap.put(username, session); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { for (Map.Entry<String, Session> entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); break; } } } /** * 发生错误时调用 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 服务器接收到客户端消息时调用的方法 */ @OnMessage public void onMessage(String message, Session session) { try{ //jackson ObjectMapper mapper = new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); //JSON字符串转 HashMap HashMap hashMap = mapper.readValue(message, HashMap.class); //消息类型 String type = (String) hashMap.get("type"); //to user String toUser = (String) hashMap.get("toUser"); Session toUserSession = sessionMap.get(toUser); String fromUser = (String) hashMap.get("fromUser"); //msg String msg = (String) hashMap.get("msg"); //sdp String sdp = (String) hashMap.get("sdp"); //ice Map iceCandidate = (Map) hashMap.get("iceCandidate"); HashMap<String, Object> map = new HashMap<>(); map.put("type",type); //呼叫的用户不在线 if(toUserSession == null){ toUserSession = session; map.put("type","call_back"); map.put("fromUser","系统消息"); map.put("msg","Sorry,呼叫的用户不在线!"); send(toUserSession,mapper.writeValueAsString(map)); return; } //对方挂断 if ("hangup".equals(type)) { map.put("fromUser",fromUser); map.put("msg","对方挂断!"); } //视频通话请求 if ("call_start".equals(type)) { map.put("fromUser",fromUser); map.put("msg","1"); } //视频通话请求回应 if ("call_back".equals(type)) { map.put("fromUser",toUser); map.put("msg",msg); } //offer if ("offer".equals(type)) { map.put("fromUser",toUser); map.put("sdp",sdp); } //answer if ("answer".equals(type)) { map.put("fromUser",toUser); map.put("sdp",sdp); } //ice if ("_ice".equals(type)) { map.put("fromUser",toUser); map.put("iceCandidate",iceCandidate); } send(toUserSession,mapper.writeValueAsString(map)); }catch(Exception e){ e.printStackTrace(); } } /** * 封装一个send方法,发送消息到前端 */ private void send(Session session, String message) { try { System.out.println(message); session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } |
测试环境,笔记本、手机再同一局域网
zs在笔记本浏览器上访问,https://xxxx.16.xx.156:10086/webrtc/zs.html
ls在手机浏览器上访问,https://xxxx.16.xx.156:10086/webrtc/ls.html
后台打印
1 2 3 4 5 6 7 8 9 10 11 |
{"msg":"1","fromUser":"zs","type":"call_start"} {"msg":"1","fromUser":"zs","type":"call_back"} {"fromUser":"ls","type":"offer","sdp":"v=0\r\no=- 626753068503365352 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS nkpVV56OfTJbiOvb1QIoILmFkHSQP4HvMGzK\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 120 127 119 125 107 108 109 35 36 124 118 123\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:Ex36\r\na=ice-pwd:tuF0um0vfeJKduoIqEtlcFdp\r\na=ice-options:trickle\r\na=fingerprint:sha-256 49:EA:10:1D:3B:0C:3F:8D:3D:A1:45:E4:84:00:F6:22:B8:72:7C:90:D6:7E:E4:E8:AE:79:01:4B:60:7E:B0:C1\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:nkpVV56OfTJbiOvb1QIoILmFkHSQP4HvMGzK 157a5a40-fd58-424a-bb25-7313cb390d25\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 profile-id=0\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 VP9/90000\r\na=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=fmtp:100 profile-id=2\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:102 H264/90000\r\na=rtcp-fb:102 goog-remb\r\na=rtcp-fb:102 transport-cc\r\na=rtcp-fb:102 ccm fir\r\na=rtcp-fb:102 nack\r\na=rtcp-fb:102 nack pli\r\na=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:120 rtx/90000\r\na=fmtp:120 apt=102\r\na=rtpmap:127 H264/90000\r\na=rtcp-fb:127 goog-remb\r\na=rtcp-fb:127 transport-cc\r\na=rtcp-fb:127 ccm fir\r\na=rtcp-fb:127 nack\r\na=rtcp-fb:127 nack pli\r\na=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\r\na=rtpmap:119 rtx/90000\r\na=fmtp:119 apt=127\r\na=rtpmap:125 H264/90000\r\na=rtcp-fb:125 goog-remb\r\na=rtcp-fb:125 transport-cc\r\na=rtcp-fb:125 ccm fir\r\na=rtcp-fb:125 nack\r\na=rtcp-fb:125 nack pli\r\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:107 rtx/90000\r\na=fmtp:107 apt=125\r\na=rtpmap:108 H264/90000\r\na=rtcp-fb:108 goog-remb\r\na=rtcp-fb:108 transport-cc\r\na=rtcp-fb:108 ccm fir\r\na=rtcp-fb:108 nack\r\na=rtcp-fb:108 nack pli\r\na=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\r\na=rtpmap:109 rtx/90000\r\na=fmtp:109 apt=108\r\na=rtpmap:35 AV1X/90000\r\na=rtcp-fb:35 goog-remb\r\na=rtcp-fb:35 transport-cc\r\na=rtcp-fb:35 ccm fir\r\na=rtcp-fb:35 nack\r\na=rtcp-fb:35 nack pli\r\na=rtpmap:36 rtx/90000\r\na=fmtp:36 apt=35\r\na=rtpmap:124 red/90000\r\na=rtpmap:118 rtx/90000\r\na=fmtp:118 apt=124\r\na=rtpmap:123 ulpfec/90000\r\na=ssrc-group:FID 3146384823 1572310693\r\na=ssrc:3146384823 cname:nQAy+uYZOtBVOzF0\r\na=ssrc:3146384823 msid:nkpVV56OfTJbiOvb1QIoILmFkHSQP4HvMGzK 157a5a40-fd58-424a-bb25-7313cb390d25\r\na=ssrc:3146384823 mslabel:nkpVV56OfTJbiOvb1QIoILmFkHSQP4HvMGzK\r\na=ssrc:3146384823 label:157a5a40-fd58-424a-bb25-7313cb390d25\r\na=ssrc:1572310693 cname:nQAy+uYZOtBVOzF0\r\na=ssrc:1572310693 msid:nkpVV56OfTJbiOvb1QIoILmFkHSQP4HvMGzK 157a5a40-fd58-424a-bb25-7313cb390d25\r\na=ssrc:1572310693 mslabel:nkpVV56OfTJbiOvb1QIoILmFkHSQP4HvMGzK\r\na=ssrc:1572310693 label:157a5a40-fd58-424a-bb25-7313cb390d25\r\n"} {"iceCandidate":{"candidate":"candidate:1679555437 1 udp 2122260223 172.16.12.156 60155 typ host generation 0 ufrag Ex36 network-id 1","sdpMid":"0","sdpMLineIndex":0},"fromUser":"ls","type":"_ice"} {"iceCandidate":{"candidate":"candidate:1918330882 1 udp 2122194687 192.168.253.1 60156 typ host generation 0 ufrag Ex36 network-id 2 network-cost 10","sdpMid":"0","sdpMLineIndex":0},"fromUser":"ls","type":"_ice"} {"iceCandidate":{"candidate":"candidate:714606493 1 tcp 1518280447 172.16.12.156 9 typ host tcptype active generation 0 ufrag Ex36 network-id 1","sdpMid":"0","sdpMLineIndex":0},"fromUser":"ls","type":"_ice"} {"iceCandidate":{"candidate":"candidate:1020564722 1 tcp 1518214911 192.168.253.1 9 typ host tcptype active generation 0 ufrag Ex36 network-id 2 network-cost 10","sdpMid":"0","sdpMLineIndex":0},"fromUser":"ls","type":"_ice"} {"fromUser":"zs","type":"answer","sdp":"v=0\r\no=- 6281552672698732270 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS 7Ez91WWET471lFYr8tHuticsIVi2uX1dQ12Y\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 125 107 124 118 123\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:Qcjs\r\na=ice-pwd:lbAlEg42TWV/TjNs8Y65yYHe\r\na=ice-options:trickle\r\na=fingerprint:sha-256 53:D7:3F:D2:6C:DC:63:7A:61:5B:EB:00:07:6A:D6:8A:58:F7:F3:A9:C0:B1:FF:53:D8:AF:49:FE:15:23:01:6D\r\na=setup:active\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:7Ez91WWET471lFYr8tHuticsIVi2uX1dQ12Y 146873a6-1a5b-4975-99d6-0fc1a0c73f76\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 profile-id=0\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:125 H264/90000\r\na=rtcp-fb:125 goog-remb\r\na=rtcp-fb:125 transport-cc\r\na=rtcp-fb:125 ccm fir\r\na=rtcp-fb:125 nack\r\na=rtcp-fb:125 nack pli\r\na=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:107 rtx/90000\r\na=fmtp:107 apt=125\r\na=rtpmap:124 red/90000\r\na=rtpmap:118 rtx/90000\r\na=fmtp:118 apt=124\r\na=rtpmap:123 ulpfec/90000\r\na=ssrc-group:FID 127330016 1173640582\r\na=ssrc:127330016 cname:pJXhxJTAZFO6lI1O\r\na=ssrc:1173640582 cname:pJXhxJTAZFO6lI1O\r\n"} {"iceCandidate":{"candidate":"candidate:1625475052 1 udp 2113937151 192.168.253.2 38700 typ host generation 0 ufrag Qcjs network-cost 999","sdpMid":"0","sdpMLineIndex":0},"fromUser":"zs","type":"_ice"} {"msg":"对方挂断!","fromUser":"ls","type":"hangup"} {"msg":"对方挂断!","fromUser":"zs","type":"hangup"} |
from:https://blog.csdn.net/m0_60259116/article/details/132050287