一、webrtc版本接听视频电话-纯js版

先看效果

用户1--拨打

 

用户2–接听

 

前端代码

 

 

index.html

复制代码
<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css">
    <link rel="stylesheet" href="./assets/style.css">
    <title>选择角色</title>
</head>
<body class="index">
    <div class="card">
        <div class="card-body">
          <h5 class="card-title">选择角色</h5>
          <p class="card-text">就像微信视频一样,总有一方是发起,另一方是接受。</p>
          <a href="./a.html" class="btn btn-primary">我是发起方</a>
          <a href="./b.html" class="btn btn-secondary">我是接受方</a>
        </div>
      </div>
</body>
 
</html>
复制代码

 

a.html

复制代码
<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/style.css">
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css" >
    <title>发起方</title>
</head>
 
<body>
    <div class="a-wrapp">
        <div class="flex-center-wrapp">
            <div class="status">
                <table class="table">
                    <thead>
                      <tr>
                        <th scope="col">设备</th>
                        <th scope="col">信令服务器</th>
                        <th scope="col">webrtc状态</th>
                      </tr>
                    </thead>
                    <tbody>
                      <tr class="pc1-info">
                        <td class="name">本机</td>
                        <td class="websockt">断开</td>
                        <td class="webrtc">断开</td>
                      </tr>
                      <tr class="pc2-info">
                        <td class="name">远程</td>
                        <td class="websockt">断开</td>
                        <td class="webrtc">断开</td>
                      </tr>
                    </tbody>
                  </table>
            </div>
            <div class="videos">
                <video class="local-video video" muted autoplay controls></video>
                <video class="remote-video video" autoplay controls></video>
            </div>
            <div class="btns">
                <button type="button" class="btn btn-secondary" onclick="start()">开始</button>
                <button type="button" class="btn btn-success" onclick="call()">呼叫</button>
                <button type="button" class="btn btn-danger" onclick="hungup()">挂断</button>
            </div>
        </div>
    </div>
     
     
    <script src="./assets/helper.js"></script>
    <script>
        // 初始化ws
        const myWs = initWs('pc1');
 
        // 获取一些dom和定义变量
        const pc1Info = document.querySelector('.pc1-info');
        let [,pc1Ws, pc1Rtc] = pc1Info.querySelectorAll('td');
        const pc2Info = document.querySelector('.pc2-info');
        let [,pc2Ws, pc2Rtc] = pc2Info.querySelectorAll('td');
        const remoteVideo = document.querySelector('.remote-video');
        const localVideo = document.querySelector('.local-video');
        let pc1 = null;
        let localStram = null;
 
        // 开始按钮点击事件
        const start = async () => {
            localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
            localVideo.srcObject = localStream;
        }
 
        // 呼叫按钮点击事件
        const call = async () => {
            pc1 = new RTCPeerConnection();
            // 核心:ice交换(ice即收集可用链路)
            pc1.onicecandidate = (e) => {
                e.candidate && myWs.sendIce('pc1',e.candidate);
            }
            localStream.getTracks().forEach(track => {
                pc1.addTrack(track, localStream);
            });
            pc1.ontrack = async (event) => {
                console.log(898989);
                remoteVideo.srcObject = event.streams[0];
            };
 
            // 核心:sdp交换(spd即会话描述,如编码、stun、本机外网ip等基本信息)
            const offer = await pc1.createOffer();
            pc1.setLocalDescription(offer);
            myWs.sendOffer(offer);
        }
 
        const hungup = ()=>{
 
        }
 
        // ws的onmessage事件
        myWs.onmessage = async ({event,data}) => {
            console.log(event, data);
            if(event === "onlineChange"){
                document.querySelectorAll('.websockt').forEach((item)=>{
                    item.innerHTML = '断开';
                })
                data.forEach((item)=>{
                    eval(`${item}Ws`).innerHTML = '已连接';
                })
            }
            
            if (event === "answer") {
                await pc1.setRemoteDescription(data);
                pc1Ws.innerHTML = '收到对方回应anser类型的sdp';
            }else if(event === "ice" && data.id ==='pc2'){
                pc1.addIceCandidate(data.ice)
                pc1Ws.innerHTML = '收到对方回应ice';
            } else if(event === "hello"){
                eval(`${data.id}Ws`).innerHTML = '已连接'
            }
        }
 
         
    </script>
</body>
 
</html>
View Code
复制代码

 

b.html

复制代码
<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/style.css">
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css" >
    <title>接收方</title>
</head>
 
<body>
    <div class="a-wrapp">
        <div class="flex-center-wrapp">
            <div class="status">
                <table class="table">
                    <thead>
                      <tr>
                        <th scope="col">设备</th>
                        <th scope="col">信令服务器</th>
                        <th scope="col">webrtc状态</th>
                      </tr>
                    </thead>
                    <tbody>
                      <tr class="pc1-info">
                        <td class="name">本机</td>
                        <td class="websockt">断开</td>
                        <td class="webrtc">已就绪</td>
                      </tr>
                      <tr class="pc2-info">
                        <td class="name">远程</td>
                        <td class="websockt">断开</td>
                        <td class="webrtc">已就绪</td>
                      </tr>
                    </tbody>
                  </table>
            </div>
            <div class="videos">
                <video class="local-video video" muted autoplay controls></video>
                <video class="remote-video video"  autoplay controls></video>
            </div>
            <div class="btns">
                <div>接收方禁用以下功能,是给发送方用的</div>
                <button type="button" disabled class="btn btn-secondary" onclick="start()">开始</button>
                <button type="button" disabled class="btn btn-success" onclick="call()">呼叫</button>
                <button type="button" disabled class="btn btn-danger" onclick="hungup()">挂断</button>
            </div>
        </div>
    </div>
 
    <div class="modal" tabindex="-1">
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title">呼入</h5>
              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
              <p>您有新的来电.</p>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary">取消</button>
              <button type="button" class="btn btn-primary">接听</button>
            </div>
          </div>
        </div>
      </div>
    <script src="./assets/helper.js"></script>
    <script>
        // 初始化ws
        const myWs = initWs('pc2');
 
        // 获取一些dom和定义变量
        const modal = document.querySelector('.modal');
        const pc1Info = document.querySelector('.pc1-info');
        let [,pc1Ws, pc1Rtc] = pc1Info.querySelectorAll('td');
        const pc2Info = document.querySelector('.pc2-info');
        let [,pc2Ws, pc2Rtc] = pc2Info.querySelectorAll('td');
        const remoteVideo = document.querySelector('.remote-video');
        const localVideo = document.querySelector('.local-video');
        let pc2 = new RTCPeerConnection();
 
        // 本地媒体展示和peer加入流监听
        navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then((localStream)=>{
            localVideo.srcObject = localStream;
            localStream.getTracks().forEach(track => {
                pc2.addTrack(track, localStream);
            });
        });
 
        // 核心:ice交换(ice即收集可用链路)
        pc2.onicecandidate = (e) => {
            if (e.candidate) {
                myWs.sendIce('pc2', e.candidate)
            }
        }
       
        pc2.ontrack = async (event) => {
            remoteVideo.srcObject = event.streams[0];
            // remoteVideo.play()
        };
 
        let offerSdp = null;
 
        // ws的onmessage事件
        myWs.onmessage = async ({event,data}) => {
            if(event === "onlineChange"){
                document.querySelectorAll('.websockt').forEach((item)=>{
                    item.innerHTML = '断开';
                })
                data.forEach((item)=>{
                    eval(`${item}Ws`).innerHTML = '已连接';
                })
            }
            if (event === "offer") {
                pc1Ws.innerHTML = '收到对方回应的offer类型的sdp';
                offerSdp = data;
 
                modal.style.display="block";
            } else if (event === "ice" && data.id === 'pc1') {
                pc1Ws.innerHTML = '收到对方回应ice';
                pc2.addIceCandidate(data.ice)
            }  else if(event === "hello"){
                eval(`${data.id}Ws`).innerHTML = '已连接'
            }
        }
 
        // 有来电弹窗,点击接听的时候按钮开始交换sdp
        document.querySelector('.btn-primary').onclick = async ()=>{
            await pc2.setRemoteDescription(offerSdp);
            const answer = await pc2.createAnswer();
            pc2.setLocalDescription(answer);
            myWs.sendAnswer(answer);
            modal.style.display="none";
             
        }
 
        document.querySelector('.btn-secondary').onclick = ()=>{
            modal.style.display="none";
        }
    </script>
</body>
 
</html>
View Code
复制代码

 

helper.js

复制代码
// 判断是不是json字符串
const isJsonStr = (str) => {
    if (typeof str == 'string') {
        try {
            var obj = JSON.parse(str);
            if (typeof obj == 'object' && obj) {
                return true;
            } else {
                return false;
            }
        } catch (e) {
            console.log('error:' + str + '!!!' + e);
            return false;
        }
    }
};
 
// 判断是不是json
const isJson = (data) => {
    const typeofRes = typeof (data) == "object";
    const toStringRes = Object.prototype.toString.call(data).toLowerCase() == "[object object]";
    const isLen = !data?.length;
    return typeofRes && toStringRes && isLen;
}
 
const initWs = (id) => {
    const ws = new WebSocket(`wss://dshvv.com:8888/my_ws/${id}`);
 
    // 重写ws,便于传参和接参数--主要是json序列化和反序列化
    const myWs = new Proxy(ws, {
        get(obj, prop) {
            const value = obj[prop];
            if (!typeof value === "function") { return obj[prop]; }
            //如果不这么做会出现this指向问题:https://juejin.cn/post/6844903730987401230
            return (...args) => {
                //处理ws上传消息的json格式转换成字符串
                if (isJson(args[0]) && prop === 'send') {
                    args[0] = JSON.stringify(args[0]);
                }
                return value.apply(obj, args)
            }
        },
        set(obj, prop, value) {
            if (prop !== 'onmessage') {
                obj[prop] = value
            } else {
                obj[prop] = function (e) {
                    const res = null;
                    if (isJsonStr(e.data)) {
                        value({
                            ...e,
                            ...JSON.parse(e.data)
                        })
                    } else {
                        value(e)
                    }
                }
            }
            return true;
        }
    });
 
    myWs.sendSdp = function (event, data) {
        myWs.send({ event, data })
    }
    myWs.sendOffer = function (sdp) {
        myWs.sendSdp('offer', sdp)
    }
    myWs.sendAnswer = function (sdp) {
        myWs.sendSdp('answer', sdp)
    }
    myWs.sendIce = function (id, ice) {
        myWs.send({
            event: 'ice',
            data:{
                ice,
                id
            }
        })
    }
 
    return myWs;
}
View Code
复制代码

 

style.css

复制代码
.index{
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}
.index>.card{
    width:90% ;
    max-width:600px ;
}
 
 
html,body{
    height:100%;
    width: 100%;
    padding: 0;
    margin: 0;
    width: 100%;
}
.a-wrapp {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
}
.flex-center-wrapp{
    width: 90%;
    max-width: 400px;
}
.btns{
    margin-top: 10px;
    text-align: center;
}
.videos{
    border: 1px solid gainsboro;
    padding: 10px;
    border-radius: 10px;
    box-sizing: border-box;
    display: flex;
    justify-content: space-between;
}
.video{
    width: 160px;
    height: 140px;
    background-color: gainsboro;
     
}
.local-video{
    width: 80px;
    height: 70px;
}
View Code
复制代码

 

后端代码

主要是ws服务

复制代码
package com.dshvv.myblogserver.websocket;
 
import com.alibaba.fastjson.JSONObject;
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.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
 
/**
 * 前后端交互的类实现消息的接收推送(自己发送给自己)
 * 参考:https://www.cnblogs.com/xuwenjin/p/12664650.html
 * @ServerEndpoint(value = "/my_ws") 前端通过此URI和后端交互,建立连接
 */
@Slf4j
@ServerEndpoint(value = "/my_ws/{id}")
@Component
public class MyWebSocket {
 
    // 当前组测的用户id
    private static HashSet onlineIds = new HashSet<>();
 
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;
 
    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
    private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
 
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
 
    /**
     * 连接建立成功调用的方法
     * @param session  可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(@PathParam("id") String id, Session session){
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        this.onlineChange(id, "onOpen");
        System.out.println(session.getId()+"有新连接加入!当前在线人数为" + getOnlineCount());
    }
 
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("id") String id){
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1
        this.onlineChange(id, "onClose");
        System.out.println(session.getId()+"有一连接关闭!当前在线人数为" + getOnlineCount());
    }
 
    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println(session.getId()+"来自客户端的消息:" + message);
        //群发消息
        for(MyWebSocket item: webSocketSet){
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
    }
 
    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        System.out.println("发生错误");
        error.printStackTrace();
    }
 
    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException{
        this.session.getBasicRemote().sendText(message);
        //this.session.getAsyncRemote().sendText(message);
    }
 
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
 
    public static synchronized void addOnlineCount() {
        MyWebSocket.onlineCount++;
    }
 
    public static synchronized void subOnlineCount() {
        MyWebSocket.onlineCount--;
    }
 
    public void onlineChange(String id, String type) {
        System.out.println("898989898989");
        if(type.equals("onOpen")){
            onlineIds.add(id);
        }else {
            onlineIds.remove(id);
        }
        Map<String, Object> initMsg = new HashMap<>();
        initMsg.put("event","onlineChange");
        initMsg.put("data",onlineIds);
        //群发消息
        for(MyWebSocket item: webSocketSet){
            try {
                item.sendMessage(JSONObject.toJSONString(initMsg));
            } catch (IOException e) {
                e.printStackTrace();
                continue;
            }
        }
 
    }
}
View Code
复制代码

 

 

注意里边用到了两个mvn包

复制代码
<!-- websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
<!-- map转json的包 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
复制代码

 

后话
接下来我将用框架编写webrtc-demo,比如vue或react。原生的操作dom有点麻烦,相同代码不能抽离成公共组件复用

posted @   丁少华  阅读(531)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示