websocket 局域网 webrtc 一对一 视频通话的实例

news2024/11/25 4:28:55

基本介绍

使用websocket来 WebRTC 建立连接时的 数据的传递和交换。

WebRTC 建立连接时,通常需要按照以下顺序执行一些步骤:
1.创建本地 PeerConnection 对象:使用 RTCPeerConnection 构造函数创建本地的 PeerConnection 对象,该对象用于管理 WebRTC 连接。

2.添加本地媒体流:通过调用 getUserMedia 方法获取本地的音视频流,并将其添加到 PeerConnection 对象中。这样可以将本地的音视频数据发送给远程对等方。

3.创建和设置本地 SDP:使用 createOffer 方法创建本地的 Session Description Protocol (SDP),描述本地对等方的音视频设置和网络信息。然后,通过调用 setLocalDescription 方法将本地 SDP 设置为本地 PeerConnection 对象的本地描述。

4.发送本地 SDP:将本地 SDP 发送给远程对等方,可以使用信令服务器或其他通信方式发送。

5.接收远程 SDP:从远程对等方接收远程 SDP,可以通过信令服务器或其他通信方式接收。

6.设置远程 SDP:使用接收到的远程 SDP,调用 PeerConnection 对象的 setRemoteDescription 方法将其设置为远程描述。

7.创建和设置本地 ICE 候选项:使用 onicecandidate 事件监听 PeerConnection 对象的 ICE 候选项生成,在生成候选项后,通过信令服务器或其他通信方式将其发送给远程对等方。

8.接收和添加远程 ICE 候选项:从远程对等方接收到 ICE 候选项后,调用 addIceCandidate 方法将其添加到本地 PeerConnection 对象中。

9.连接建立:一旦本地和远程的 SDP 和 ICE 候选项都设置好并添加完毕,连接就会建立起来。此时,音视频流可以在本地和远程对等方之间进行传输。

windwos+局域网直连的环境,测试过程的问题

1.http协议下安全性原因导致无法调用摄像头和麦克风
chrome://flags/ 配置安全策略 或者 配置本地的https环境
在这里插入图片描述
2. 开启了防火墙,webRTC连接失败,
windows防火墙-高级设置-入站规则-新建规则-端口 ,udp
UDP: 32355-65535 放行
为了方便测试 直接关闭防火墙也行

效果如下 局域网0延迟:

在这里插入图片描述

如下demo级别的代码,复制运行就能直接测试,简单修改后,可实现基于webrtc的 共享桌面、视频录制等功能。

html代码

<!DOCTYPE>
<html>

<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="myid" />
            <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">
    function generateRandomLetters(length) {
        let result = '';
        const characters = 'abcdefghijklmnopqrstuvwxyz'; // 字母表

        for (let i = 0; i < length; i++) {
            const randomIndex = Math.floor(Math.random() * characters.length);
            const randomLetter = characters[randomIndex];
            result += randomLetter;
        }

        return result;
    }

    let username = generateRandomLetters(2);
    document.getElementById('myid').value = username;
    let localVideo = document.getElementById('localVideo');
    let remoteVideo = document.getElementById('remoteVideo');
    let websocket = null;
    let peer = null;
    let candidate = null;



    /* WebSocket */
    function WebSocketInit() {
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
            websocket = new WebSocket("ws://192.168.31.14:8181/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, fromUser, msg, sdp, iceCandidate);

            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;
                    console.log(peer);
                    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;
        }
    }



    WebSocketInit();
    ButtonFunInit();

</script>

</html>

websocket java代码

package com.coco.boot.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;


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}")
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();
        }
    }
}

springboot 相关依赖和配置

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

// 2.启用功能
@EnableWebSocket
public class CocoBootApplication
		
3.config		
package com.coco.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

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

主要参考:
WebRTC + WebSocket 实现视频通话
WebRTC穿透服务器防火墙配置问题
WebRT音视频录制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1557725.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot企业级抽奖项目业务四 (缓存预热)

缓存预热 为什么要做预热: 当活动真正开始时&#xff0c;需要超高的并发访问活动相关信息 必须把必要的数据提前加载进redis 预热的策略: 在msg中写一个定时任务 每分钟扫描一遍card_game表 把(开始时间 > 当前时间)&& (开始时间 < 当前时间1分钟)的活动及相…

(八)Gateway服务网关

Gateway服务网关 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目&#xff0c;该项目是基于 Spring 5.0&#xff0c;Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关&#xff0c;它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。…

day01-SpringCloud01(Eureka、Ribbon、Nacos)

视频地址: SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务 学习资料地址: 百度网盘 提取码&#xff1a;1234 1. 认识微服务 1.1.单体架构 单体架构&#xff1a;将业务的所有功能集中在一个项目中开发&#x…

WebCopilot:一款功能强大的子域名枚举和安全漏洞扫描工具

关于WebCopilot WebCopilot是一款功能强大的子域名枚举和安全漏洞扫描工具&#xff0c;该工具能够枚举目标域名下的子域名&#xff0c;并使用不同的开源工具检测目标存在的安全漏洞。 工具运行机制 WebCopilot首先会使用assetsfinder、submaster、subfinder、accumt、finddom…

【2023】kafka在linux和docker安装(kafka-1)

目录&#x1f4bb; 一、linux安装kafka1. 安装jdk2. 上传解压到/usr/local目录下3、使用kafka 二、docker安装kafka1. 下载2. 安装zookeeper3. 安装kafka 一、linux安装kafka 环境主机 mac m2、虚拟机Ubuntu22.04.4 1. 安装jdk yum install -y java-1.8.0-openjdk.x86_64下载k…

Redis分布式锁红锁

Redisson实现分布式锁 lock()上锁解析&#xff1a; 1&#xff0c;hexist判断redis是否有这个锁 2&#xff0c;hset设置锁&#xff0c;hash类型&#xff0c;key为锁名字&#xff0c;value是一对kv&#xff0c;k是当前redisson1的id,v为计数器&#xff0c;表示当前锁持有次数&am…

【详细讲解语言模型的原理、实战与评估】

&#x1f308;个人主页:程序员不想敲代码啊&#x1f308; &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家&#x1f3c6; &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提…

【苹果MAC】苹果电脑 LOGI罗技鼠标设置左右切换全屏页面快捷键

首先键盘设置->键盘快捷键 调度中心 设置 f1 f2 为移动一个空间&#xff08;就可以快捷移动了&#xff09; 想要鼠标直接控制&#xff0c;就需要下载官方驱动&#xff0c;来设置按键快捷键&#xff0c;触发 F1 F2 安装 LOGI OPTIONS Logi Options 是一款功能强大且便于使用…

HarmonyOS 应用开发之Stage模型绑定FA模型ServiceAbility

本小节介绍Stage模型的两种应用组件如何绑定FA模型ServiceAbility组件。 UIAbility关联访问ServiceAbility UIAbility关联访问ServiceAbility和UIAbility关联访问ServiceExtensionAbility的方式完全相同。 import common from ohos.app.ability.common; import hilog from o…

LabelConvert: 目标检测和图像分割数据集格式转换工具

LabelConvert LabelConvert是一个目标检测和图像分割的数据集格式转换工具&#xff0c;支持labelme、labelImg与YOLO、VOC和COCO 数据集格式之间的相互转换。 支持的转换格式 安装 pip install label_convert具体使用方法 由于文章篇幅所限&#xff0c;请移步LabelConvert官…

Linux初学(十一)中间件

一、web服务 1.1 中间件简介 中间件其实就是一类软件&#xff0c;中间件的作用是让用户可以看到一个网页 总结&#xff1a;客户端可以向服务端发送请求&#xff0c;服务器端会通过中间件程序来接收请求&#xff0c;然后处理请求&#xff0c;最后将处理结果返回给客户端 1.2 中…

【MySql数据库】MySQL5.7在navicat中建立连接报错1045及重装MySQL过程中3306端口号被占用释放的过程

文章目录 一、报错1、软件中报错2、navicat中报错3、数据库密码是正确的4、卸载数据库5、重装数据库发现3306端口被占用 二、释放3306端口1、找到3306端口对应的PID值2、释放3306端口号3、释放端口后&#xff0c;重装数据库 一、报错 1、软件中报错 2、navicat中报错 在navic…

cocos2.x => node 属性修改

简介 与节点属性相关的几个核心变量_trs、_matrix、_worldMatrix、_localMatDirty、_worldMatDirty。 _trs&#xff1a;存储节点的position、rotation、scale _matrix&#xff1a;存储节点的缩放、位移、旋转三者合一的变化矩陈&#xff08;仿射矩陈&#xff09; _worldMat…

linux基础命令篇:Linux基础命令讲解——文件浏览(cat、less、head、tail和grep)

Linux基础命令讲解——文件浏览&#xff08;cat、less、head、tail和grep&#xff09; 本文详细介绍Linux中的cat、less、head、tail和grep命令&#xff0c;这些命令在日常工作中非常实用&#xff0c;以下是关于这些命令的详细介绍&#xff1a; 1. cat命令&#xff1a;用于查看…

简明Pytorch分布式训练 — DistributedDataParallel 实践

上一次的Pytorch单机多卡训练主要介绍了Pytorch里分布式训练的基本原理&#xff0c;DP和DDP的大致过程&#xff0c;以及二者的区别&#xff0c;并分别写了一个小样作为参考。小样毕竟还是忽略了很多细节和工程实践时的一些处理方式的。实践出真知&#xff0c;今天&#xff08;简…

每日面经分享(http、python)

1. http和HTTPS的区别&#xff0c;http的响应流程 a. 区别&#xff1a; i. 安全性&#xff1a;HTTP是明文传输数据&#xff0c;不提供加密保护&#xff0c;因此数据容易被窃听和篡改。而HTTPS使用SSL/TLS协议对数据进行加密和身份验证&#xff0c;提供更高的安全性。 ii. 端口…

新网站秒收录技术,新网站百度收录时间

在建立新网站后&#xff0c;让它尽快被搜索引擎收录是网站主最为关注的事情之一。百度作为中国最大的搜索引擎&#xff0c;网站被其快速收录对于增加曝光和流量至关重要。本文将介绍一些新网站秒收录技术&#xff0c;以及一般情况下新网站被百度收录需要的时间。 新网站秒收录技…

百度资源平台链接提交

百度资源平台是百度搜索引擎提供的一个重要工具&#xff0c;用于帮助网站主将自己的网站链接提交给百度搜索引擎&#xff0c;以便更快地被收录和展示在搜索结果中。以下将就百度资源平台链接提交的概念、操作方法以及其对网站收录和曝光的影响进行探讨&#xff1a; 什么是百度资…

docker-compose安装jenkins

1、环境准备&#xff1a;准备安装好docker的服务器一台 2、在服务器上创建一个目录用于安装Jenkins mkdir jenkins3、下载好要挂载的&#xff1a;maven、jkd&#xff1b;并将下载好的tar.gz包上传至服务器待安装目录中并解压 tar -xzvf tar -xzvf apache-maven-3.9.6-bin.tar…

最短路-Floyd Dijkstrea

Floyd算法 一种求解“多源最短路”问题的算法 在Floyd算法中&#xff0c;图一般用邻接矩阵存储&#xff0c;边权可正可负&#xff08;但不允许负环&#xff09;&#xff0c;利用动态规划的思想&#xff0c;逐步求解出任意两点之间的最短距离 int d[N][N],初始为无穷 d[i][j…