美文网首页
js实现WebRTC点对点视频通话

js实现WebRTC点对点视频通话

作者: 叁太紫 | 来源:发表于2021-02-03 10:29 被阅读0次

    先看页面代码,这里使用的webRTC原生的API。目前信令服务器使用的是websocket实现的,后续改成将socket.io。socket.io默认含有房间的概念。
    源代码

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>video</title>
    </head>
    <body>
    <h2 style="text-align: center;">播放页面</h2>
    <h3 id="userId" style="text-align: center;"></h3>
    <center>
        <div>
            <video id="localVideo" class="video" autoplay="autoplay"></video>
            <video id="remoteVideo" class="video" height="500px" autoplay="autoplay"></video>
        </div>
    </center>
    </br>
    <div style="text-align: center;">
        <button id="callBtn" onclick="requestConnect()">建立连接</button>
        <button id="hangupBtn" onclick="hangupHandle()">断开连接</button>
    </div>
    </br>
    <div style="text-align: center;">
        对方id: <input id="toUserId">
    </div>
    </body>
    </html>
    <script src="./adapter-latest.js"></script>
    <script>
        const localVideo = document.querySelector('#localVideo');
        const remoteVideo = document.querySelector('#remoteVideo');
    
        const callBtn = document.getElementById('callBtn')
        const hangupBtn = document.getElementById('hangupBtn')
    
        const config = {
            iceServers: [
                { urls: 'stun:global.stun.twilio.com:3478?transport=udp' }
            ],
        };
    
        let peerConnection;
    
        let socket, userId, toUserId;
        userId = parseInt(Math.random()*10000);
        document.getElementById('userId').innerText = '我的id:' + userId;
    
        // 本地流和远端流
        let localStream, remoteStream;
    
        function requestConnect() {
            toUserId = document.getElementById('toUserId').value
            if(!toUserId){
                alert('请输入对方id')
                return false
            }else if(!socket){
                alert('请先打开websocket')
                return false
            }else if(toUserId == userId){
                alert('自己不能和自己连接')
                return false
            }
            //准备连接
            startHandle().then(() => {
                //发送给远端开启请求
                socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': {'type': 'connect'}}))
            })
        }
    
        //开启本地的媒体设备
        async function startHandle() {
            // 1.获取本地音视频流
            // 调用 getUserMedia API 获取音视频流
            let constraints = {
                video: true,
                audio: {
                    // 设置回音消除
                    noiseSuppression: true,
                    // 设置降噪
                    echoCancellation: true,
                }
            }
    
            await navigator.mediaDevices.getUserMedia(constraints)
                .then(gotLocalMediaStream)
                .catch((err) => {
                    console.log('getUserMedia 错误', err);
                    //创建点对点连接对象
                });
    
            createConnection();
        }
    
        // getUserMedia 获得流后,将音视频流展示并保存到 localStream
        function gotLocalMediaStream(mediaStream) {
            localVideo.srcObject = mediaStream;
            localStream = mediaStream;
            callBtn.disabled = false;
        }
    
        function startWebsocket() {
            toUserId = document.getElementById('toUserId').value
            let webSocketUrl = 'wss://' + location.host + '/websocket/' + userId
            if ('WebSocket' in window) {
                // console.log(1)
                socket = new WebSocket(webSocketUrl);
            } else if ('MozWebSocket' in window) {
                // console.log(2)
                socket = new MozWebSocket(webSocketUrl);
            }
            // socket = new SockJS('https://' + location.host + '/websocket/' + userId);
            //连接成功
            socket.onopen = function (e) {
                console.log('连接服务器成功!')
            };
            //server端请求关闭
            socket.onclose = function (e) {
                console.log('close')
                alert(JSON.stringify(e))
    
            };
            //error
            socket.onerror = function (e) {
                console.error(e)
                alert(JSON.stringify(e))
            };
            socket.onmessage = onmessage
        }
        //连接服务器
        startWebsocket();
    
        function onmessage(e) {
    
            const json = JSON.parse(e.data)
            const description = json.message
            toUserId = json.userId
    
            switch (description.type) {
                case 'connect':
                    if(confirm(toUserId + '请求连接!')){
                        //准备连接
                        startHandle().then(() => {
                            socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': {'type': 'start'} }));
                        })
                    }
                    break;
                case 'start':
                    //同意连接之后开始连接
                    startConnection()
                    break;
                case 'offer':
                    peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {
    
                    }).catch((err) => {
                        console.log('local 设置远端描述信息错误', err);
                    });
    
                    peerConnection.createAnswer().then(function (answer) {
    
                        peerConnection.setLocalDescription(answer).then(() => {
                            console.log('设置本地answer成功!');
                        }).catch((err) => {
                            console.error('设置本地answer失败', err);
                        });
    
                        socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': answer }));
                    }).catch(e => {
                        console.error(e)
                    });
                    break;
                case 'icecandidate':
                    // 创建 RTCIceCandidate 对象
                    let newIceCandidate = new RTCIceCandidate(description.icecandidate);
    
                    // 将本地获得的 Candidate 添加到远端的 RTCPeerConnection 对象中
                    peerConnection.addIceCandidate(newIceCandidate).then(() => {
                        console.log(`addIceCandidate 成功`);
                    }).catch((error) => {
                        console.log(`addIceCandidate 错误:\n` + `${error.toString()}.`);
                    });
                    break;
                case 'answer':
    
                    peerConnection.setRemoteDescription(new RTCSessionDescription(description)).then(() => {
                        console.log('设置remote answer成功!');
                    }).catch((err) => {
                        console.log('设置remote answer错误', err);
                    });
                    break;
                default:
                    break;
            }
        }
    
        function createConnection() {
            peerConnection = new RTCPeerConnection(config)
    
            if (localStream) {
                // 视频轨道
                const videoTracks = localStream.getVideoTracks();
                // 音频轨道
                const audioTracks = localStream.getAudioTracks();
                // 判断视频轨道是否有值
                if (videoTracks.length > 0) {
                    console.log(`使用的设备为: ${videoTracks[0].label}.`);
                }
                // 判断音频轨道是否有值
                if (audioTracks.length > 0) {
                    console.log(`使用的设备为: ${audioTracks[0].label}.`);
                }
    
                localStream.getTracks().forEach((track) => {
                    peerConnection.addTrack(track, localStream)
                })
            }
    
            // 监听返回的 Candidate
            peerConnection.addEventListener('icecandidate', handleConnection);
            // 监听 ICE 状态变化
            peerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange)
            //拿到流的时候调用
            peerConnection.addEventListener('track', gotRemoteMediaStream);
        }
    
        //创建发起方会话描述对象(createOffer),设置本地SDP(setLocalDescription),并通过信令服务器发送到对等端,以启动与远程对等端的新WebRTC连接。
        function startConnection() {
            callBtn.disabled = true;
            hangupBtn.disabled = false;
            // 发送offer
            peerConnection.createOffer().then(description => {
                console.log(`本地创建offer返回的sdp:\n${description.sdp}`)
    
                // 将 offer 保存到本地
                peerConnection.setLocalDescription(description).then(() => {
                    console.log('local 设置本地描述信息成功');
                    // 本地设置描述并将它发送给远端
                    socket.send(JSON.stringify({ 'userId': userId, 'toUserId': toUserId, 'message': description }));
                }).catch((err) => {
                    console.log('local 设置本地描述信息错误', err)
                });
            })
                .catch((err) => {
                    console.log('createdOffer 错误', err);
                });
        }
    
        function hangupHandle() {
            // 关闭连接并设置为空
            peerConnection.close();
            peerConnection = null;
    
            hangupBtn.disabled = true;
            callBtn.disabled = false;
    
            localStream.getTracks().forEach((track) => {
                track.stop()
            })
        }
    
        // 3.端与端建立连接
        function handleConnection(event) {
            // 获取到触发 icecandidate 事件的 RTCPeerConnection 对象
            // 获取到具体的Candidate
            console.log("handleConnection")
            const peerConnection = event.target;
            const icecandidate = event.candidate;
    
            if (icecandidate) {
    
                socket.send(JSON.stringify({
                    'userId': userId,
                    'toUserId': toUserId,
                    'message': {
                        type: 'icecandidate',
                        icecandidate: icecandidate
                    }
                }));
            }
        }
    
        // 4.显示远端媒体流
        function gotRemoteMediaStream(event) {
            console.log('remote 开始接受远端流')
    
            if (event.streams[0]) {
                remoteVideo.srcObject = event.streams[0];
                remoteStream = event.streams[0];
            }
        }
    
        function handleConnectionChange(event) {
            const peerConnection = event.target;
            console.log('ICE state change event: ', event);
            console.log(`ICE state: ` + `${peerConnection.iceConnectionState}.`);
        }
    
    </script>
    <style>
        .video {
            background-color: black;
            height: 30vh;
        }
    </style>
    

    再看看服务器代码,这里使用Java语言开发,spring boot框架,使用websocket转发信令。
    websocket连接类

    @ServerEndpoint("/websocket/{id}")
    @Controller
    @Slf4j
    public class WebsocketController {
    
        @OnOpen
        public void onOpen(Session session, @PathParam(value = "id") String id) {
            //获取连接的用户
            log.info("加入session:" + id );
            SocketManager.setSession(id, session);
        }
    
        @OnClose
        public void onClose(Session session) {
            log.info("移除不用的session:" + session.getId());
            SocketManager.removeSession(session.getId());
        }
    
    
        //收到客户端信息
        @OnMessage
        public void onMessage(String message,Session session) throws IOException {
            log.info("message,{}",message);
            JSONObject jsonObject = JSON.parseObject(message);
            SocketManager.sendMessage(jsonObject.getString("toUserId"),jsonObject.toJSONString());
        }
    
        //错误时调用
        @OnError
        public void onError(Session session, Throwable throwable) {
            log.error("webSocket 错误,{}", throwable.getMessage());
            SocketManager.removeSession(session.getId());
        }
    
    }
    

    SocketManager类代码

    public class SocketManager {
    
        /**
         * 客户端连接session集合 <id,Session></>
         */
        private static Map<String,Session> sessionMap = new ConcurrentHashMap<String,Session>();
    
        /**
         * 存放新加入的session 便于以后对session管理
         * @param userId 用户id
         * @param session 用户session
         */
        public synchronized static void setSession(String userId,Session session){
            sessionMap.put(userId,session);
        }
    
        /**
         * 根据session ID移除session
         * @param sessionId
         */
        public static void removeSession(String sessionId){
            if(null == sessionId){
                return;
            }
            sessionMap.forEach((k,v) -> {
                if(sessionId.equals(v.getId())){
                    sessionMap.remove(k);
                }
            });
        }
    
        /**
         * 给用户发送消息
         * @param userId 用户id
         * @param message 消息
         */
        public  static void sendMessage(String userId, String message) {
            log.info("给用户发送消息,{},{}",userId,message);
            Session session = sessionMap.get(userId);
            if(session != null){
                synchronized (session){
                    try {
                        session.getBasicRemote().sendText(message);
                    } catch (IOException e) {
                        log.info("发送websocket消息是异常,{}",e);
                    }
                }
            }
        }
    }
    

    最后看下实现效果打开页面,在另一台电脑打开或者浏览器新建一个标签页,输入对方的id点击建立连接就可以实现音视频通话了。


    1612319194(1).jpg

    相关文章

      网友评论

          本文标题:js实现WebRTC点对点视频通话

          本文链接:https://www.haomeiwen.com/subject/gasptltx.html