简单的websocket可以聊天么:WebRTCWebSocket实现通话
简单的websocket可以聊天么:WebRTCWebSocket实现通话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
前言 WebRTCWebRTC(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  WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
  WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
大致原理

项目是SpringBoot Thymeleaf WebSocket,配置了https,不熟悉的同学可以看我们的《SpringBoot系列》
html页面webrtc.html页面
<!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
    
Controller页面跳转
    /**
     * 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;
    }  WebRtcWSServer
    
WebSocket服务
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://172.16.12.156:10086/webrtc/zs.html

ls在手机浏览器上访问,https://172.16.12.156:10086/webrtc/ls.html

java后台打印
{"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"}  后记
    
视频通话,整合我们之前的写的IM即时通讯,项目越来越完善了
WebSocket Java 私聊、群聊实例
一套简单的web即时通讯——第一版
一套简单的web即时通讯——第二版
一套简单的web即时通讯——第三版
本文部分参考:
https://www.an.rustfisher.com/webrtc/web-samples/getUserMedia-open-camera
https://github.com/shushushv/webrtc-p2p
版权声明作者:huanzi-qch
出处:https://www.cnblogs.com/huanzi-qch
若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.




