webrtc实时视频语音实现

webrtc介绍

WebRTC实现了基于网页的视频会议,标准是WHATWG 协议,目的是通过浏览器提供简单的javascript就可以达到实时通讯(Real-Time Communications (RTC))能力。
image
image
image

实时语音demo

环境搭建

需要信令服务器与中继服务器作为中转

1.express,socket下载,搭node后端(express)(信令服务器)

点击查看代码
 npm install express
 npm install socket.io
 npm i加载包
 node index.js启动

2.后端index.js编写

点击查看代码
var express = require('express');
var app = express();
var http = require('http').createServer(app);
//var io = require('socket.io')(http);
const fs = require('fs');
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
})
现在能看到基础页面index.html

3.监听端口与websoket连接,见图3

点击查看代码
http.listen(443, () => {
  console.log('https listen on');
});

io.on("connection", (socket) => {
  //连接加入子房间
  socket.join( socket.id );
  console.log("a user connected " + socket.id);
  
  socket.on("disconnect", () => {
      console.log("user disconnected: " + socket.id);
      //某个用户断开连接的时候,我们需要告诉所有还在线的用户这个信息
      socket.broadcast.emit('user disconnected', socket.id);
	  //交流,前后端通过emit发送信息
      //除自己外,对其他人广播:socket.broadcast.emit
      //自己和其他人广播:io.emit
  });
  socket.on('new user greet', (data) => {
      console.log(data);
      console.log(socket.id + ' greet ' + data.msg);
      socket.broadcast.emit('need connect', {sender: socket.id, msg : data.msg});
  });
  //在线用户回应新用户消息的转发
  socket.on('ok we connect', (data) => {
      io.to(data.receiver).emit('ok we connect', {sender : data.sender});
  });
  //sdp 消息的转发
  socket.on( 'sdp', ( data ) => {
      console.log('sdp');
      console.log(data.description);
      //console.log('sdp:  ' + data.sender + '   to:' + data.to);
      socket.to( data.to ).emit( 'sdp', {
          description: data.description,
          sender: data.sender
      } );
  } );
  //candidates 消息的转发
  socket.on( 'ice candidates', ( data ) => {
      console.log('ice candidates:  ');
      console.log(data);
      socket.to( data.to ).emit( 'ice candidates', {
          candidate: data.candidate,
          sender: data.sender
      } );
  } );
});

4.index.html代码

点击查看代码
<h1 id="user-id">用户名称</h1>
    <ul id="user-list">
      <li>用户列表</li>
    </ul>
    <audio id="video-local" preload="auto" controls>
      <!-- <source src="https://192.168.3.56/a8485020-39a2-4c09-904b-2e2b87d71363" type="audio/mpeg" /> -->
    </audio>
    <audio id="audio">
      <!-- <source src="/public/imgs/wss.wav" type="audio/mpeg" /> -->
    </audio>
	<ul id="videos"></ul>
	 <script src="//cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.js
  "></script>
    <script src="//cdn.bootcdn.net/ajax/libs/socket.io/3.0.4/socket.io.js
  "></script>
    <script>
	 function getUserMedia(constrains, success, error) {
        let promise;
        if (navigator.mediaDevices.getUserMedia) {
          //最新标准API
          promise = navigator.mediaDevices
            .getUserMedia(constrains)
            .then(success)
            .catch(error);
        } else if (navigator.webkitGetUserMedia) {
          //webkit内核浏览器
          promise = navigator
            .webkitGetUserMedia(constrains)
            .then(success)
            .catch(error);
        } else if (navigator.mozGetUserMedia) {
          //Firefox浏览器
          promise = navagator
            .mozGetUserMedia(constrains)
            .then(success)
            .catch(error);
        } else if (navigator.getUserMedia) {
          //旧版API
          promise = navigator
            .getUserMedia(constrains)
            .then(success)
            .catch(error);
        }
        return promise;
      }
	function canGetUserMediaUse() {
        return !!(
          navigator.mediaDevices.getUserMedia ||
          navigator.webkitGetUserMedia ||
          navigator.mozGetUserMedia ||
          navigator.msGetUserMedia
        );
      }
	  //中继服务器:最好自己搭,TURN中继服务器
	  const iceServer = {
        iceServers: [
          { urls: ["stun:ss-turn1.xirsys.com"] },
          {
            username:
              "CEqIDkX5f51sbm7-pXxJVXePoMk_WB7w2J5eu0Bd00YpiONHlLHrwSb7hRMDDrqGAAAAAF_OT9V0dWR1d2Vi",
            credential: "446118be-38a4-11eb-9ece-0242ac140004",
            urls: [
              "turn:ss-turn1.xirsys.com:80?transport=udp",
              "turn:ss-turn1.xirsys.com:3478?transport=udp",
            ],
          },
        ],
      };
	  let audio = document.getElementById("audio");
      var pc = [];
	  function StartCall(parterName, createOffer) {
        pc[parterName] = new RTCPeerConnection(iceServer);

        //如果已经有本地流,那么直接获取Tracks并调用addTrack添加到RTC对象中。
        if (localStream) {
          localStream.getTracks().forEach((track) => {
            pc[parterName].addTrack(track, localStream); //should trigger negotiationneeded event
          });
        } else {
          //否则需要重新启动摄像头并获取
          if (canGetUserMediaUse()) {
            getUserMedia(
              {
                video: true,
                audio: false,
              },
              function (stream) {
                localStream = stream;
                localVideoElm.srcObject = stream;
                $(localVideoElm).width(800);
              },
              function (error) {
                console.log(
                  "访问用户媒体设备失败:",
                  error.name,
                  error.message
                );
              }
            );
          } else {
            alert("您的浏览器不兼容");
          }
        }

        //如果是呼叫方,那么需要createOffer请求
        if (createOffer) {
          //每当WebRTC基础结构需要你重新启动会话协商过程时,都会调用此函数。它的工作是创建和发送一个请求,给被叫方,要求它与我们联系。
          pc[parterName].onnegotiationneeded = () => {
            //https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createOffer

            pc[parterName]
              .createOffer()
              .then((offer) => {
                return pc[parterName].setLocalDescription(offer);
              })
              .then(() => {
                //把发起者的描述信息通过Signal Server发送到接收者
                socket.emit("sdp", {
                  type: "video-offer",
                  description: pc[parterName].localDescription,
                  to: parterName,
                  sender: socket.id,
                });
              });
          };
        }

        //当需要你通过信令服务器将一个ICE候选发送给另一个对等端时,本地ICE层将会调用你的 icecandidate 事件处理程序。有关更多信息,请参阅Sending ICE candidates 以查看此示例的代码。
        pc[parterName].onicecandidate = ({ candidate }) => {
          socket.emit("ice candidates", {
            candidate: candidate,
            to: parterName,
            sender: socket.id,
          });
        };

        //当向连接中添加磁道时,track 事件的此处理程序由本地WebRTC层调用。例如,可以将传入媒体连接到元素以显示它。详见 Receiving new streams 。
        pc[parterName].ontrack = (ev) => {
          let str = ev.streams[0];

          if (document.getElementById(`${parterName}-video`)) {
            document.getElementById(`${parterName}-video`).srcObject = str;
          } else {
            let newVideo = document.createElement("audio");
            newVideo.id = `${parterName}-video`;
            newVideo.autoplay = true;
            newVideo.controls = true;
            //newVideo.className = 'remote-video';
            newVideo.srcObject = str;
            let li = document.createElement("li");
            li.id = `audio-${parterName}`;
            li.innerHTML = `<p>与${parterName}通话<p>`;
            li.appendChild(newVideo);
            let button = $('<button class="uncall" >结束通话</button>');
            button.appendTo(li);
            document.getElementById("videos").appendChild(li);
            $('#user-list li[user-id="' + parterName + '"] .call').css(
              "display",
              "none"
            );
          }
        };
      }
      var socket = io();
      let b = 0;
      socket.on("connect", () => {
        InitCamera(socket.id);
        //输出内容 其中 socket.id 是当前socket连接的唯一ID
        console.log("connect " + socket.id);
        $("form").submit(function (e) {
          //禁止页面重新加载
          e.preventDefault(); //prevents page reloading
          //发送事件,其值为文本框中输入的值

          socket.emit("chat message", $("#m").val());

          $("#messages").append(
            $("<li>")
              .text($("#m").val() + ":" + socket.id)
              .attr("class", "my")
          );
          //清空文本框的值
          $("#m").val("");
          //返回false 禁止原始的提交
          return false;
        });

        $("#user-id").text(socket.id);

        pc.push(socket.id);

        socket.emit("new user greet", {
          sender: socket.id,
          msg: "hello world",
        });
		       socket.on("need connect", (data) => {
          console.log("need!!!!!!!!!!!!!");

          console.log(data);
          //创建新的li并添加到用户列表中
          let li = $("<li></li>")
            .text(data.sender)
            .attr("user-id", data.sender);
          $("#user-list").append(li);
          //同时创建一个按钮
          let button = $('<button class="call" >通话</button>');
          button.appendTo(li);
          //监听按钮的点击事件, 这是个demo 需要添加很多东西,比如不能重复拨打已经连接的用户
          $(button).click(function () {
            //$(this).parent().attr('user-id')
            console.log($(this).parent().attr("user-id"));
            //点击时,开启对该用户的通话
            StartCall($(this).parent().attr("user-id"), true);
          });

          socket.emit("ok we connect", {
            receiver: data.sender,
            sender: socket.id,
          });
        });
        //某个用户失去连接时,我们需要获取到这个信息
        socket.on("user disconnected", (socket_id) => {
          console.log("disconnect : " + socket_id);
          $('#user-list li[user-id="' + socket_id + '"]').remove();
          $("#audio-" + socket_id).remove();
          $('#user-list li[user-id="' + socket_id + '"] .call').css(
            "display",
            "block"
          );
        });
        //链接吧..
        socket.on("ok we connect", (data) => {
          console.log(data);
          let li = $("<li></li>")
            .text(data.sender)
            .attr("user-id", data.sender);
          $("#user-list").append(li);
          let button = $('<button class="call">通话</button>');
          button.appendTo(li);
          //监听按钮的点击事件, 这是个demo 需要添加很多东西,比如不能重复拨打已经连接的用户
          $(button).click(function () {
            //$(this).parent().attr('user-id')
            console.log($(this).parent().attr("user-id"));
            //点击时,开启对该用户的通话
            StartCall($(this).parent().attr("user-id"), true);
          });
          //这里少了程序,比如之前的按钮啊,按钮的点击监听都没有。
        });

        //监听发送的sdp事件
        socket.on("sdp", (data) => {
          //如果时offer类型的sdp
          if (data.description.type === "offer") {
            //那么被呼叫者需要开启RTC的一套流程,同时不需要createOffer,所以第二个参数为false
            StartCall(data.sender, false);
            //把发送者(offer)的描述,存储在接收者的remoteDesc中。
            let desc = new RTCSessionDescription(data.description);
            //按1-13流程走的
            pc[data.sender].setRemoteDescription(desc).then(() => {
              pc[data.sender]
                .createAnswer()
                .then((answer) => {
                  return pc[data.sender].setLocalDescription(answer);
                })
                .then(() => {
                  socket.emit("sdp", {
                    type: "video-answer",
                    description: pc[data.sender].localDescription,
                    to: data.sender,
                    sender: socket.id,
                  });
                })
                .catch(); //catch error function empty
            });
          } else if (data.description.type === "answer") {
            //如果使应答类消息(那么接收到这个事件的是呼叫者)
            let desc = new RTCSessionDescription(data.description);
            pc[data.sender].setRemoteDescription(desc);
          }
        });

        //如果是ice candidates的协商信息
        socket.on("ice candidates", (data) => {
          console.log("ice candidate: " + data.candidate);
          //{ candidate: candidate, to: partnerName, sender: socketID }
          //如果ice candidate非空(当candidate为空时,那么本次协商流程到此结束了)
          if (data.candidate) {
            var candidate = new RTCIceCandidate(data.candidate);
            //讲对方发来的协商信息保存
            pc[data.sender].addIceCandidate(candidate).catch(); //catch err function empty
          }
        });
      });
	  

以上domo只能本地运行,如果需要网址运行且移动端,需要https。可以选择自签证书尝试,自签证书见下一篇博客

posted @ 2022-07-26 10:23  流云君  阅读(379)  评论(0编辑  收藏  举报