一小时教你用SpringBoot+WebSocket+WebRTC实现视频通话

1. 运行结果

SpringBoot+WebSocket+WebRTC实现视频通话

上述运行结果中是有声音(比较小而已)及动的画面的(画面不是静止的)。

网上关于webrtc的文档(文章)和视频也挺多的,但是用SpringBoot结合WebRTC的却屈指可数,前一段时间小编我学习了一下WebRTC的相关知识,于是用SpringBoot+WebRTC实现了一个多人的线上自习室(有画面,但是没有声音的那种,开启声音也挺简单,在js代码里设置一下即可[运行结果在最后的总结里])。最近CSDN有活动,正好把前一段时间学习的知识运用起来(下述代码只是实现了,但是其中的逻辑是存在一定问题的,所以如果读者用下述代码,切记需要改动改动哈!)。既然是WebRTC,为什么又和WebSocket扯上关系了呢?因为利用WebSocket技术来发送消息具有实时性,你看我在这端发送一个消息出去,只要另一端处于连接状态,那么就可以接收到这个消息。而如果使用的是http、https等的话,这一端你发送一个消息,另外一段需要刷新一下页面才能看到消息(当然可以搞个定时器)。结合WebSocket技术,能很快速地实现一个视频通话功能。

2. 实现

导入相关jar包的依赖,如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

上述jar包可能有一些不需要的喔!

2.1 后端实现

websocket 配置类
GetHttpSessionConfig.class

package com.example.demo.websocket2;

import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {

        HttpSession httpSession = (HttpSession) request.getHttpSession();

        // 获取httpsession对象

        sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }
}

ServerEndpointExporter Bean的定义 Config.class

package com.example.demo.websocket2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class Config {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {

        return new ServerEndpointExporter();
    }
}

*websocket服务器类 WebSocketServer *

package com.example.demo.websocket2;

import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint(value = "/video",configurator = GetHttpSessionConfig.class)
public class WebSocketServer {

    //存储客户端的连接对象,每个客户端连接都会产生一个连接对象
    private static ConcurrentHashMap<String,WebSocketServer> map = new ConcurrentHashMap();
    //每个连接都会有自己的会话
    private Session session;
    private String account;

    @OnOpen
    public void open(Session session,EndpointConfig config){

        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        String account = String.valueOf(httpSession.getAttribute("account"));

        map.put(account,this);

        this.session = session;
        this.account = account;
    }

    @OnClose
    public void close(){
        map.remove(account);
    }

    @OnError
    public void error(Throwable error){
        error.printStackTrace();
    }

    @OnMessage
    public void getMessage(String message) throws IOException {

        Set<Map.Entry<String, WebSocketServer>> entries = map.entrySet();
        for (Map.Entry<String, WebSocketServer> entry : entries) {
            if(!entry.getKey().equals(account)){//将消息转发到其他非自身客户端
                entry.getValue().send(message);
            }
        }
    }

    public void send(String message) throws IOException {
        if(session.isOpen()){
            session.getBasicRemote().sendText(message);
        }
    }

    public int  getConnetNum(){
        return map.size();
    }
}

2.2 前端页面实现

登录界面的代码就不在这儿粘贴了,下面主要展示视频通话界面的代码(包括css样式和js代码都在的)

<html>

<head>
    <title>main</title>
    <link rel = "stylesheet" href = "https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap-theme.min.css"/>

</head>

<style>

    body {
        background: #eee;
        padding: 5% 0;
    }

    video {
        background: black;
        border: 1px solid gray;
    }

    .call-page {
        position: relative;
        display: block;
        margin: 0 auto;
        width: 500px;
        height: 500px;
    }

    #localVideo {
        width: 150px;
        height: 150px;
        position: absolute;
        top: 15px;
        right: 15px;
    }

    #remoteVideo {
        width: 500px;
        height: 500px;
    }

</style>

<body>

<div id = "callPage" class = "call-page">
    <video id = "localVideo" autoplay></video>
    <video id = "remoteVideo" autoplay></video>

    <div class = "row text-center">
        <div class = "col-md-12">
            <input id = "callToUsernameInput" type = "text"
                   placeholder = "username to call" />
            <button id = "callBtn" class = "btn-success btn">Call</button>
            <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
        </div>
    </div>

</div>

<script type="text/javascript">
    //our username
    var connectedUser;

    //connecting to our signaling server
    var conn = new WebSocket("ws://localhost:9999/video");

    conn.onopen = function () {
        console.log("Connected to the signaling server");
    };

    //when we got a message from a signaling server
    conn.onmessage = function (msg) {
        console.log("Got message", msg.data);

        var data = JSON.parse(msg.data);

        switch(data.type) {
            case "login":
                handleLogin(data.success);
                break;
            //when somebody wants to call us
            case "offer":
                handleOffer(data.offer, data.name);
                break;
            case "answer":
                handleAnswer(data.answer);
                break;
            //when a remote peer sends an ice candidate to us
            case "candidate":
                handleCandidate(data.candidate);
                break;
            case "leave":
                handleLeave();
                break;
            default:
                break;
        }
    };

    conn.onerror = function (err) {
        console.log("Got error", err);
    };

    //alias for sending JSON encoded messages
    function send(message) {
        //attach the other peer username to our messages
        if (connectedUser) {
            message.name = connectedUser;
        }

        conn.send(JSON.stringify(message));
    }

    //******
    //UI selectors block
    //******


    var callPage = document.querySelector("#callPage");
    var callToUsernameInput = document.querySelector("#callToUsernameInput");
    var callBtn = document.querySelector("#callBtn");

    var hangUpBtn = document.querySelector("#hangUpBtn");

    var localVideo = document.querySelector("#localVideo");
    var remoteVideo = document.querySelector("#remoteVideo");

    var yourConn;
    var stream;

    // callPage.style.display = "none";

    var PeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection || undefined);
    var RTCSessionDescription = (window.webkitRTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription || undefined);

    navigator.getUserMedia = (navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia ||
        navigator.msGetUserMedia);
    //**********************
    //Starting a peer connection
    //**********************

    //getting local video stream
    navigator.getUserMedia({ video: true, audio: true }, function (myStream) {
        stream = myStream;

        //displaying local video stream on the page
        localVideo.srcObject = stream;

        //using Google public stun server
        var configuration = {
            "iceServers": []
        };

        yourConn = new PeerConnection(configuration);

        // setup stream listening
        yourConn.addStream(stream);

        //when a remote user adds stream to the peer connection, we display it
        yourConn.onaddstream = function (e) {
            remoteVideo.srcObject = e.stream;
        };

        // Setup ice handling
        yourConn.onicecandidate = function (event) {
            if (event.candidate) {
                send({
                    type: "candidate",
                    candidate: event.candidate
                });
            }
        };

    }, function (error) {
        console.log(error);
    });


    //initiating a call
    callBtn.addEventListener("click", function () {
        var callToUsername = callToUsernameInput.value;

        if (callToUsername.length > 0) {

            connectedUser = callToUsername;

            // create an offer
            yourConn.createOffer(function (offer) {
                send({
                    type: "offer",
                    offer: offer
                });

                yourConn.setLocalDescription(offer);
            }, function (error) {
                alert("Error when creating an offer");
            });

        }
    });

    //when somebody sends us an offer
    function handleOffer(offer, name) {
        connectedUser = name;
        yourConn.setRemoteDescription(new RTCSessionDescription(offer));

        //create an answer to an offer
        yourConn.createAnswer(function (answer) {
            yourConn.setLocalDescription(answer);

            send({
                type: "answer",
                answer: answer
            });

        }, function (error) {
            alert("Error when creating an answer");
        });
    }

    //when we got an answer from a remote user
    function handleAnswer(answer) {
        yourConn.setRemoteDescription(new RTCSessionDescription(answer));
    }

    //when we got an ice candidate from a remote user
    function handleCandidate(candidate) {
        yourConn.addIceCandidate(new RTCIceCandidate(candidate));
    }

    //hang up
    hangUpBtn.addEventListener("click", function () {

        send({
            type: "leave"
        });

        handleLeave();
    });

    function handleLeave() {
        connectedUser = null;
        remoteVideo.src = null;

        yourConn.close();
        yourConn.onicecandidate = null;
        yourConn.onaddstream = null;
    }
</script>

</body>

</html>
3. 总结

上述前端代码参考来自这里:webrtc视频演示,上述代码中如果有不懂的读者可以去仔细看看这个链接里的知识,里面关于webrtc有详细的介绍及实现,不过,没有讲多人的,它只讲了一对一的,不过,前一段时间小编在参考一些大佬的实现思路及自己思考下,也实现了一个多人的,运行结果如下:

基于SpringBoot,WebSocket,WebRTC实现多人自习室功能

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐