一个亲测跑通的webRTC多人视频通话程序——转载

index页面

<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: 0.5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>
  </body>
  <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>
      //当页面准备完成时, $是jQuery的一个函数
      $(document).ready(function(){
          //令 socket 等于 socket.io 对象,io() 中的参数为socket.io服务器地址,在这里和web服务器相同。
          var socket = io('//');
          //在控制台输出hello字符串 console.log 和print类似。
          console.log('hello');
          //监听connect事件,当socket连接建立时,会触发后面第二个参数中的匿名函数内容
          socket.on('connect', () => {
              //输出内容 其中 socket.id 是当前socket连接的唯一ID
              console.log('connect ' + socket.id);
          });
​
        //监听form的提交操作
        $('form').submit(function(e) {
            //禁止页面重新加载
            e.preventDefault(); //prevents page reloading
            //发送事件,其值为文本框中输入的值
            socket.emit('chat message', $('#m').val());
            //清空文本框的值
            $('#m').val('');
            //返回false 禁止原始的提交
            return false;
        });
​
        //监听 chat message事件,当监听到事件发生时执行第二个参数中的匿名函数
        socket.on('chat message', function(msg){
            //在网页中id为messages的对象中,插入li标签,其内容为msg
            $('#messages').append($('<li>').text(msg));
        });
​
​
      });
  </script>
</html>

camera端

<!doctype html>
<html>
<!-- @author :  bilibili:一只斌  /   mail: tuduweb@qq.com-->
<head>
    <title>Camera</title>
    <style>
        #user-list>li {
            font-size: 24px;
        }
    </style>
</head>
​
<body>
    <h1 id="user-id">用户名称</h1>
    <ul id="user-list">
        <li>用户12</li>
        <li>用户23</li>
        <li>用户34</li>
    </ul>
    <video id="video-local" controls autoplay></video>
​
    <canvas id="capture-canvas" style="display: none;"></canvas>
​
    <button id="capture">拍照</button>
​
    <ul id="capture-list"></ul>
​
​
    <div id="videos"></div>
​
    <script src="//cdn.bootcdn.net/ajax/libs/socket.io/3.0.4/socket.io.js"></script>
    <script src="//cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.js"></script>
    <script>
        //封装一部分函数
        function getUserMedia(constrains, success, error) {
            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);
            }
        }
​
        function canGetUserMediaUse() {
            return !!(navigator.mediaDevices.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
        }
​
​
        const localVideoElm = document.getElementById("video-local");
        $('document').ready(() => {
​
​
            $('#capture').click(() => {
                let video = localVideoElm//原生dom
                let isPlaying = !(video.paused || video.ended || video.seeking || video.readyState < video.HAVE_FUTURE_DATA)
​
                if (isPlaying) {
                    let canvas = $('#capture-canvas')
                    canvas.attr('width', localVideoElm.clientWidth);//设置canvas的宽度
                    canvas.attr('height', localVideoElm.clientHeight);//设置canvas的高度
​
                    let img = $('<img>')
                    img.attr('width', localVideoElm.clientWidth);//设置图像的宽度
                    img.attr('height', localVideoElm.clientHeight);//设置图像的高度
​
                    //canvas[0] //jQuery对象转dom
                    var context = canvas[0].getContext('2d');
                    //在canvas上绘图,其绘图坐标为0,0; 
                    //绘图大小为摄像头内容的宽度,高度(全局绘制,你可以改变这些值试试效果)。
                    context.drawImage(localVideoElm, 0, 0, localVideoElm.clientWidth, localVideoElm.clientHeight);
                    //根据canvas内容进行编码,并赋值到图片上
                    var data = canvas[0].toDataURL('image/png');
                    img.attr('src', data);
                    //插入到id为capture-list的有序列表里
                    $('#capture-list').append($('<li></li>').html(img));
                }
            })
​
        });
​
        //STUN,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"] }]
        };
​
        //PeerConnection
        var pc = [];
        var localStream = null;
​
        function InitCamera() {
​
            if (canGetUserMediaUse()) {
                getUserMedia({
                    video: true,
                    audio: false
                }, (stream) => {
                    localStream = stream;
                    localVideoElm.srcObject = stream;
                    $(localVideoElm).width(800);
                }, (err) => {
                    console.log('访问用户媒体失败: ', err.name, err.message);
                });
            } else {
                alert('您的浏览器不兼容');
            }
​
        }
​
        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('video');
                    newVideo.id = `${parterName}-video`;
                    newVideo.autoplay = true;
                    newVideo.controls = true;
                    //newVideo.className = 'remote-video';
                    newVideo.srcObject = str;
​
                    document.getElementById('videos').appendChild(newVideo);
                }
            }
​
​
​
        }
​
        var socket = io();
​
        socket.on('connect', () => {
            InitCamera();
​
            //输出内容 其中 socket.id 是当前socket连接的唯一ID
            console.log('connect ' + socket.id);
​
            $('#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(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();
            })
            //链接吧..
            socket.on('ok we connect', (data) => {
                console.log(data);
​
                $('#user-list').append($('<li></li>').text(data.sender).attr('user-id', data.sender));
                //这里少了程序,比如之前的按钮啊,按钮的点击监听都没有。
            });
​
            //监听发送的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
                }
            })
​
​
        });
​
​
    </script>
</body>
​
​
</html>

server端

//@author :  bilibili:一只斌  /   mail: tuduweb@qq.com
var express = require('express');
var app = express();
var http = require('http').createServer(app);
​
var fs = require('fs');
let sslOptions = {
    key: fs.readFileSync('C:/privkey.key'),//里面的文件替换成你生成的私钥
    cert: fs.readFileSync('C:/cacert.pem')//里面的文件替换成你生成的证书
};
​
const https = require('https').createServer(sslOptions, app);
​
var io = require('socket.io')(https);
​
var path = require('path');
app.use(express.static(path.join(__dirname, 'public')));
​
​
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
  });
​
app.get('/camera', (req, res) => {
    res.sendFile(__dirname + '/camera.html');
});
​
​
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);
    });
​
    socket.on("chat message",(msg) => {
        console.log(socket.id + " say: " + msg);
        //io.emit("chat message", msg);
        socket.broadcast.emit("chat message", msg);
    });
​
    //当有新用户加入,打招呼时,需要转发消息到所有在线用户。
    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
        } );
    } );
​
});
​
​
https.listen(443, () => {
    console.log('https listening on *:443');
});
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值