首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

基于surging 如何利用peerjs进行语音视频通话

编程知识
2024年08月30日 08:41

  一 、 概述

          PeerJS 是一个基于浏览器WebRTC功能实现的js功能包,简化了WebrRTC的开发过程,对底层的细节做了封装,直接调用API即可,再配合surging 协议组件化从而做到稳定,高效可扩展的微服务,再利用RtmpToWebrtc 引擎组件可以做到不仅可以利用httpflv 观看rtmp推流直播,还可以采用基于 Webrtc的peerjs 进行观看,那么今天要讲的是如何结合开发语音视频通话功能。放到手机和电脑上都可以实现语音视频通话。

    一键运行打包成品下载:https://pan.baidu.com/s/1MVsjKtVYpUonauAz9ZXtPg?pwd=1q2g

    测试用户:fanly

    测试密码:123456

      为了让大家节约时间,能尽快运行产品看到效果,上面有 一键运行打包成品可以进行下载测试运行。

  二、如何测试运行

以下是目录结构,

IDE:consul 注册中心

kayak.client: 网关

kayak.server:微服务

apache-skywalking-apm:skywalking链路跟踪

 

 以上是目录结构, 不需要进入管理界面配置网络组件,启动后自带端口96的ws协议主机,只要打开video文件夹,里面有两个语音通话的html测试文件,在同一一个局域网只要输入对方的name就可以进行语音通话

 打开界面如图

 

三、基于surging如何开发

以上是没有开发环境的进行下载进行下载测试,那么正在使用surging 的如何开发此功能呢?

1. 创建服务接口,继承于IServiceKey

 [ServiceBundle("Device/{Service}")]
 public  interface IChatService : IServiceKey
 {
 }

2. 创建中间服务,继承于WSBehavior, IChatService

  internal class ChatService : WSBehavior, IChatService
  {
      private static readonly ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();
      private static readonly ConcurrentDictionary<string, string> _clients = new ConcurrentDictionary<string, string>();

      protected override void OnOpen()
      {
         var _name = Context.QueryString["name"]; 
          if (!string.IsNullOrEmpty(_name))
          {
              _clients[ID] = _name;
              _users[_name] = ID;
          }
      }

      protected override void OnError( WebSocketCore.ErrorEventArgs e)
      {
        var msg = e.Message;
      }

      protected override void OnMessage(MessageEventArgs e)
      {
          if (_clients.ContainsKey(ID))
          {

              var message = JsonConvert.DeserializeObject<Dictionary<string, object>>(e.Data);
              //消息类型
             message.TryGetValue("type",out object @type);
              message.TryGetValue("toUser", out object toUser);
              message.TryGetValue("fromUser", out object fromUser);
              message.TryGetValue("msg", out object msg);
              message.TryGetValue("sdp", out object sdp);
              message.TryGetValue("iceCandidate", out object iceCandidate);
              

              Dictionary<String, Object> result = new Dictionary<String, Object>();
              result.Add("type", @type);

              //呼叫的用户不在线
              if (!_users.ContainsKey(toUser?.ToString()))
              {
                  result["type"]= "call_back";
                  result.Add("fromUser", "系统消息");
                  result.Add("msg", "Sorry,呼叫的用户不在线!");

                  this.Client().SendTo(JsonConvert.SerializeObject(result), ID);
                  return;
              }

              //对方挂断
              if ("hangup".Equals(@type))
              {
                  result.Add("fromUser", fromUser);
                  result.Add("msg", "对方挂断!");
              }

              //视频通话请求
              if ("call_start".Equals(@type))
              {
                  result.Add("fromUser", fromUser);
                  result.Add("msg", "1");
              }

              //视频通话请求回应
              if ("call_back".Equals(type))
              {
                  result.Add("fromUser", toUser);
                  result.Add("msg", msg);
              }

              //offer
              if ("offer".Equals(type))
              {
                  result.Add("fromUser", toUser); 
                  result.Add("sdp", sdp);
              }

              //answer
              if ("answer".Equals(type))
              {
                  result.Add("fromUser", toUser);
                  result.Add("sdp", sdp);
              }

              //ice
              if ("_ice".Equals(type))
              {
                  result.Add("fromUser", toUser);
                  result.Add("iceCandidate", iceCandidate);
              }

              this.Client().SendTo(JsonConvert.SerializeObject(result), _users.GetValueOrDefault(toUser?.ToString()));
          }
      }

      protected override void OnClose(CloseEventArgs e)
      {
         if( _clients.TryRemove(ID, out string name))
          _users.TryRemove (name, out string value);
      }

  }

3.设置surgingSettings的WSPort端口配置,默认96

以上就是利用websocket协议中转消息,下面是页面如何编号,代码如下:

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<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="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">
    let username = "fanly";
    let localVideo = document.getElementById('localVideo');
    let remoteVideo = document.getElementById('remoteVideo');
    let websocket = null;
    let peer = null;

    WebSocketInit();
    ButtonFunInit();

    /* WebSocket */
    function WebSocketInit(){
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
            websocket = new WebSocket("ws://127.0.0.1:961/device/chat?name="+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);

            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;
                    stream.getTracks().forEach(track => {
                        peer.addTrack(track, stream);
                    });

                    let offer = await peer.createOffer();
                    await peer.setLocalDescription(offer); 
                    let newOffer = offer;
   
                    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;

                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;
        }
    }
</script>
</html>

以上是页面的代码,如需要添加其它账号测试只要更改username ,或者ws地址也可以更改标记红色的区域。

 

三、总结

本人正在开发平台,如有疑问可以联系作者,QQ群:744677125

From:https://www.cnblogs.com/fanliang11/p/18388027
本文地址: http://www.shuzixingkong.net/article/1574
0评论
提交 加载更多评论
其他文章 程序员失业日记4:半个月拿下4个offer
上篇文章很多小伙伴留言也讲到自己被公司裁员,还有的细心的小伙伴说去年九月就被裁了,在看一下文章的发布时间,绷不住了。先和大家说一下,我已经找到工作,因为最近工作一直都很忙,加上自己也比较懒,所以就拖了很久才写的。之前没说就是为了方便写后面的文章。也是通过本文分享一下自己找工作的方法,希望可以给大家一
程序员失业日记4:半个月拿下4个offer 程序员失业日记4:半个月拿下4个offer 程序员失业日记4:半个月拿下4个offer
maven 插件之 maven-shade-plugin,解决同包同名 class 共存问题的神器
开心一刻 有一天螃蟹出门,不小心撞倒了泥鳅泥鳅很生气地说:你是不是瞎啊!螃蟹说:不是啊,我是螃蟹 概述 maven-shade-plugin 官网已经介绍的很详细了,我给大家简单翻译一下 This plugin provides the capability to package the artif
maven 插件之 maven-shade-plugin,解决同包同名 class 共存问题的神器 maven 插件之 maven-shade-plugin,解决同包同名 class 共存问题的神器 maven 插件之 maven-shade-plugin,解决同包同名 class 共存问题的神器
Redis高可用方案:使用Keepalived实现主备双活
注意:请确保已经安装Redis和keepalived,本文不在介绍如何安装。 1、使用版本说明 Redis版本:5.0.2 Keepalived版本:1.3.5 Linux 版本:Centos7.9 查看Redis版本: /usr/local/redis/bin/redis-cli -v 查看Kee
Redis高可用方案:使用Keepalived实现主备双活 Redis高可用方案:使用Keepalived实现主备双活 Redis高可用方案:使用Keepalived实现主备双活
ChatGPT学习之旅 (10) 系统全球化小助手
本篇我们一起来了解下如何基于ChatGPT中来帮助我们解决一些系统开发中的多语言/全球化/本地化的任务,通过给到清晰的提示词,让这些任务的解决时间缩短一半以上。
ChatGPT学习之旅 (10) 系统全球化小助手 ChatGPT学习之旅 (10) 系统全球化小助手
LOTO示波器统计曲线和故障分析pass/fail测试
LOTO示波器统计曲线和故障分析pass/fail测试 虚拟示波器可以应用在工业自动化检测中,除了常规的检测波形和测量值参数以外,由多个行业客户定制和验证的统计曲线和故障分析(pass/fail)功能也为工业自动化检测带来极大的便利。 (一)故障分析(pass/fail)的基础:统计曲线功能 在信号
LOTO示波器统计曲线和故障分析pass/fail测试 LOTO示波器统计曲线和故障分析pass/fail测试 LOTO示波器统计曲线和故障分析pass/fail测试
.NET 开源报表神器 Seal-Report
前言 Seal-Report 是一款.NET 开源报表工具,拥有 1.4K Star。它提供了一个完整的框架,使用 C# 编写,最新的版本采用的是 .NET 8.0 。 它能够高效地从各种数据库或 NoSQL 数据源生成日常报表,并支持执行复杂的报表任务。 其简单易用的安装过程和直观的设计界面,我们
.NET 开源报表神器 Seal-Report .NET 开源报表神器 Seal-Report .NET 开源报表神器 Seal-Report
好多kafka难题啊,看看其中的化解之道
前面已经分享过几篇面试了,这是一篇关于更加面向项目和技术的面经详解,第一次遇见问那么多kafka的问题,看看这个粉丝是怎么回答的。
工作 6 年,@Transactional 注解用的一塌糊涂
接手新项目一言难尽,别的不说单单就一个 @Transactional 注解用的一塌糊涂,五花八门的用法,很大部分还失效无法回滚。 有意识的在涉及事务相关方法上加@Transactional注解,是个好习惯。不过,很多同学只是下意识地添加这个注解,一旦功能正常运行,很少有人会深入验证异常情况下事务是否
工作 6 年,@Transactional 注解用的一塌糊涂