webrtcP2P音视频通话(二)

发布于:2024-04-27 ⋅ 阅读:(20) ⋅ 点赞:(0)

webrtc实现视频会议

学习链接:webrtc实现视频会议,web多人视频通话,websocket通信
,全部的代码:包括java的后端,vue的webrtc客户端前端代码,放在了微信收藏中

vite配置https、nginx配置ssl、openssl本地搭建

示例

客户端代码

client.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
        }

        .video-wrapper {
            video {
                width: 400px;
                height: 300px;
                border: 1px dashed black;
            }
        }
    </style>
</head>

<body>

    <div class="video-wrapper">
        <video id="localVideo"></video>
        <video id="remoteVideo"></video>
    </div>

    <div>
        目标用户id: <input type="text" id="targetIdIpt" value="2">
        <br />
        <button id="startCallBtn" onclick="startCall">发起通话</button></button>
    </div>
</body>
<script>
    let localVideo = document.querySelector('#localVideo')
    let remoteVideo = document.querySelector('#remoteVideo')
    let targetIdIpt = document.querySelector('#targetIdIpt')

    const params = location.href.substring(location.href.indexOf('?') + 1)
    const _params = {}
    params.split('&').forEach(item => {
        const t = item.split('=')
        _params[t[0]] = t[1]
    })

    const { id: currUserId } = _params

    startCallBtn.addEventListener('click', startCall)

    let localStream = null
    let socket = null
    let peer = null

    async function startCamera() {
        localStream = await navigator.mediaDevices.getUserMedia({ video:true, /*  audio:true  */  })
        localVideo.srcObject = localStream
        localVideo.play()
    }
    // 开启摄像头
    startCamera()

    function initWs() {
        socket = new WebSocket('ws://localhost:9095/ws/' + currUserId)

        socket.onopen = () => {
            console.log('连接建立');
        }

        socket.onmessage = (e) => {
            console.log('收到消息: ', e.data);
            let {code, data} = JSON.parse(e.data)
            console.log('收到消息的code', code);
            if (code == 'connect_success') {
                console.log('连接成功');
            } else if (code == 'offer') {
                acceptCall(data)
            } else if (code == 'icecandidate') {
                console.log('收到icecandidate消息',peer,data)
                let {candidate} = data
                peer.addIceCandidate(candidate)
            } else if (code == 'answer') {
                console.log('对方已同意', data);
                acceptAnswer(data)
            }
        }

        socket.onerror = (err) => {
            console.log('连接发生错误');
        }

        socket.onclose = () => {
            console.log('连接关闭');
        }
    }

    // 初始化websocket
    initWs()


    // 发起通话
    async function startCall() {

        peer = new RTCPeerConnection({})

        localStream.getTracks().forEach(track => {
            peer.addTrack(track, localStream)
        })

        peer.addEventListener('track', e => {
            console.log('发起方ontrack触发', e);
            remoteVideo.srcObject = e.streams[0]
            remoteVideo.play()
        })

        // 这个事件要被触发的前提是, 要先执行上面的把音视频流的轨道给到peer。然后 调用peer.setLocalDescription时会多次连续触发icecandidate事件
        peer.addEventListener('icecandidate', e => {

            // RTCPeerConnectionIceEvent {isTrusted: true, candidate: RTCIceCandidate, type: 'icecandidate', target: RTCPeerConnection, currentTarget: RTCPeerConnection, …}
            console.log('发起方 icecandidate触发', e);

            if (e.candidate) {

                // e.candidate RTCIceCandidate {candidate: 'candidate:1764731264 1 udp 2122260223 192.168.134.…760 typ host generation 0 ufrag KPfd network-id 1', sdpMid: '1', sdpMLineIndex: 1, foundation: '1764731264', component: 'rtp', …}
                console.log('发起方 e.candidate', e.candidate);

                // 内网穿透
                const message = {
                    code: 'icecandidate',
                    data: {
                        targetId: targetIdIpt.value,
                        candidate: e.candidate
                    }
                }
                socket.send(JSON.stringify(message))
            }
        })

        sendOffer()
    }

    async function sendOffer() {
        // RTCSessionDescription {type: 'offer', sdp: 'v=0\r\no=- 8833742509318497131 2 IN IP4 127.0.0.1\r\ns…770 msid:- 8e3b686c-7779-48a1-8714-8a7a0caf0136\r\n'}
        let offer = await peer.createOffer()
        console.log('创建了offer', offer);

        // 设置offer, 会连续触发多次 peer 的 icecandidat 事件, 最后1次的candidate是null
        peer.setLocalDescription(offer)

        const message = {
            code: 'offer',
            data: {
                targetId: targetIdIpt.value,
                offer: offer
            }
        }

        socket.send(JSON.stringify(message))
    }

    function acceptAnswer({fromId, answer}) {
        console.log('设置answer', answer);
        peer.setRemoteDescription(answer)
    }

    function acceptCall({fromId, offer}) {
        console.log('接受通话请求', localStream);

        peer = new RTCPeerConnection({})

        localStream.getTracks().forEach(track => {
            peer.addTrack(track, localStream)
        })

        peer.addEventListener('track', e => {
            console.log('接收方 ontrack触发', e);
            remoteVideo.srcObject = e.streams[0]
            remoteVideo.play()
        })

        // 接收方获得offer {type: 'offer', sdp: 'v=0\r\no=- 1381205837170471750 2 IN IP4 127.0.0.1\r\ns…8adcbdb976 87647395-dbcb-4db4-b105-83145f853af1\r\n'}
        console.log('接收方获得offer', offer);

        peer.addEventListener('icecandidate', e => {

            console.log('接收方 icecandidate触发', e);

            if (e.candidate) {

                console.log('接收方 e.candidate', e.candidate);

                // 内网穿透
                const message = {
                    code: 'icecandidate',
                    data: {
                        targetId: fromId,
                        candidate: e.candidate
                    }
                }

                socket.send(JSON.stringify(message))
            }
        })

        // 设置远程发过来的offer, 会触发 peer 的 track事件 监听函数
        peer.setRemoteDescription(offer)

        sendAnswer(fromId)

    }

    async function sendAnswer(fromId) {
        let answer = await peer.createAnswer()
        peer.setLocalDescription(answer)

        const message = {
            code: 'answer',
            data: {
                targetId: fromId,
                answer: answer
            }
        }

        socket.send(JSON.stringify(message))
    }

    
</script>

</html>

服务端代码

SignalWsServer

就是将消息转发给目标对象

@Slf4j
@Component
@ServerEndpoint("/ws/{id}")
public class SignalWsServer {

    private static ConcurrentHashMap<String, Session> SESSION_MAP = new ConcurrentHashMap<>();

    private Session session;

    private String id;

    @OnOpen
    public void onOpen(@PathParam("id") String id, Session session) {

        log.info("连接::onOpen->{}, {}", id, session);

        SESSION_MAP.put(id, session);
        this.id = id;

        this.session = session;

        this.session.getUserProperties().put("id", id);

        try {
            this.session.getBasicRemote().sendText(
                    JsonUtil.obj2Json(MapBuilder.newHashMap().put("code", "connect_success").build())
            );
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @OnMessage
    public void onMessage(String msg) throws Exception {

        log.info("收到客户端 【{}】 的消息::{}", id, msg);

        JSONObject jsonObject = JSON.parseObject(msg);

        String code = String.valueOf(jsonObject.get("code"));

        if ("offer".equals(code)) {
            JSONObject data = jsonObject.getJSONObject("data");
            String targetId = data.getString("targetId");
            Object offer = data.get("offer");
            Session session = SESSION_MAP.get(targetId);
            if (session == null) {
                log.error("目标用户不存在");
                return;
            }
            Map<String, Object> obj = MapBuilder.newHashMap()
                    .put("code", "offer")
                    .put("data", MapBuilder.newHashMap().put("fromId", id).put("offer", offer).build())
                    .build();
            sendMsg(session, obj);
        } else if ("icecandidate".equals(code)) {
            JSONObject data = jsonObject.getJSONObject("data");
            String targetId = data.getString("targetId");

            Object candidate = data.get("candidate");

            Session session = SESSION_MAP.get(targetId);
            if (session == null) {
                log.error("目标用户不存在");
                return;
            }

            Map<String, Object> obj = MapBuilder.newHashMap()
                    .put("code", "icecandidate")
                    .put("data", MapBuilder.newHashMap().put("fromId", id).put("candidate", candidate).build())
                    .build();

            sendMsg(session, obj);

        }  else if ("answer".equals(code)) {
            JSONObject data = jsonObject.getJSONObject("data");
            String targetId = data.getString("targetId");

            Object answer = data.get("answer");

            Session session = SESSION_MAP.get(targetId);
            if (session == null) {
                log.error("目标用户不存在");
                return;
            }

            Map<String, Object> obj = MapBuilder.newHashMap()
                    .put("code", "answer")
                    .put("data", MapBuilder.newHashMap().put("fromId", id).put("answer", answer).build())
                    .build();

            sendMsg(session, obj);

        }

    }

    private void sendMsg(Session session, Object obj) {
        try {
            session.getBasicRemote().sendText(JsonUtil.obj2Json(obj));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @OnClose
    public void onClose() {

        log.info("关闭::onClose->{}",session);

        String id = session.getUserProperties().get("id").toString();
        SESSION_MAP.remove(id);
    }

    @OnError
    public void onError(Throwable ex) throws IOException {
        log.info("发生错误::onError->{}", ex);
        if (this.session.isOpen()) {
            session.close();
        }
    }

}

WsConfig
@Configuration
public class WsConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

效果

在这里插入图片描述