去年TSINGSEE青犀视频研发团队基于WEBRTC架构开了网页音视频通话平台EasyRTC,EasyRTC支持微信小程序、H5页面、APP、PC客户端等接入方式之间互通,快速从零开始搭建实时音视频通信;支持多人至百万人视频通话,满足语音视频社交。
如果大家对EasyRTC感兴趣,可以联系我们进行了解或试用。今年我们仍然没有停止对WEBRTC技术的探索。本文和大家分享一下通过webrtc实现局域网进行音视频连接的步骤。
1、首先要创建服务代码,来确保服务器启动
const Koa = require('koa'); const path = require('path'); const koaSend = require('koa-send'); const static = require('koa-static'); const socket = require('koa-socket'); const users = {}; // 保存用户 const sockS = {}; // 保存客户端对应的socket const io = new socket({ ioOptions: { pingTimeout: 10000, pingInterval: 5000, } }); const https = require("https"); const fs = require("fs"); // 创建一个Koa对象表示web app本身: const app = new Koa(); // socket注入应用 io.attach(app); app.use(static( path.join( __dirname, './public') )); // 对于任何请求,app将调用该异步函数处理请求: app.use(async (ctx, next) => { if (!/\./.test(ctx.request.url)) { await koaSend( ctx, 'index.html', { root: path.join(__dirname, './'), maxage: 1000 * 60 * 60 * 24 * 7, gzip: true, } // eslint-disable-line ); } else { await next(); } }); // io.on('join', ctx=>{ // event data socket.id // }); app._io.on('connection', sock => { sock.on('join', data=>{ console.log("join:", data); sock.join(data.roomid, () => { if (!users[data.roomid]) { users[data.roomid] = []; } let obj = { account: data.account, id: sock.id }; let arr = users[data.roomid].filter(v => v.account === data.account); if (!arr.length) { users[data.roomid].push(obj); } sockS[data.account] = sock; app._io.in(data.roomid).emit('joined', users[data.roomid], data.account, sock.id); // 发给房间内所有人 // sock.to(data.roomid).emit('joined',data.account); }); }); sock.on('offer', data=>{ console.log('offer', data); sock.to(data.roomid).emit('offer',data); }); sock.on('answer', data=>{ console.log('answer', data); sock.to(data.roomid).emit('answer',data); }); sock.on('__ice_candidate', data=>{ console.log('__ice_candidate', data); sock.to(data.roomid).emit('__ice_candidate',data); }); // 1 v 1 sock.on('apply', data=>{ // 转发申请 // console.log("apply:", data); sockS[data.account].emit('apply', data); }); sock.on('reply', data=>{ // 转发回复 console.log("reply:", data); sockS[data.account].emit('reply', data); }); sock.on('1v1answer', data=>{ // 转发 answer sockS[data.account].emit('1v1answer', data); }); sock.on('1v1ICE', data=>{ // 转发 ICE sockS[data.account].emit('1v1ICE', data); }); sock.on('1v1offer', data=>{ // 转发 Offer sockS[data.account].emit('1v1offer', data); }); sock.on('1v1hangup', data=>{ // 转发 hangup sockS[data.account].emit('1v1hangup', data); }); }); app._io.on('disconnect', (sock) => { for (let k in users) { users[k] = users[k].filter(v => v.id !== sock.id); } console.log(`disconnect id => ${users}`); }); // 在端口3001监听: let port = 3001; app.listen(port, _ => { console.log('app started at port ...' + port); });
2、在进行音视频通信是,必修要本地端口或者https服务。
(1)网上下载OpenSSL软件
(2)创建后缀名(.Key和.cert)证书;
查看网址教程:https://www.cnblogs.com/tugenhua0707/p/10927722.html
(3)在js服务把证书添加进去
const options = { key: fs.readFileSync("./server.key", "utf8"), cert: fs.readFileSync("./server.cert", "utf8") }; let server = https.createServer(options, app.callback()).listen(3002); app._io.listen(server);
3、客户端代码采用VUE,如下(重要部分代码):
确定在线用户:{{userList.length}}{{v.account}} {{v.account === account ? 'me' : ''}} {{v.account === isCall ? 'calling' : ''}} 呼叫 {{v.account}} {{account}}hangup{{isCall}} { this.userList = data; }); socket.on('reply', async data =>{ // 收到回复 this.loading = false; console.log(data); switch (data.type) { case '1': // 同意 this.isCall = data.self; // 对方同意之后创建自己的 peer await this.createP2P(data); // 并给对方发送 offer this.createOffer(data); break; case '2': //拒绝 this.$message({ message: '对方拒绝了你的请求!', type: 'warning' }); break; case '3': // 正在通话中 this.$message({ message: '对方正在通话中!', type: 'warning' }); break; } }); socket.on('apply', (data) => { // 收到请求 if (this.isCall) { this.reply(data.self, '3'); return; } this.$confirm(data.self + ' 向你请求视频通话, 是否同意?', '提示', { confirmButtonText: '同意', cancelButtonText: '拒绝', type: 'warning' }).then(async () => { await this.createP2P(data); // 同意之后创建自己的 peer 等待对方的 offer this.isCall = data.self; this.reply(data.self, '1'); }).catch(() => { this.reply(data.self, '2'); }); }); socket.on('1v1answer', (data) =>{ // 接收到 answer this.onAnswer(data); }); socket.on('1v1ICE', (data) =>{ // 接收到 ICE this.onIce(data); }); socket.on('1v1offer', (data) =>{ // 接收到 offer this.onOffer(data); }); socket.on('1v1hangup', _ =>{ // 通话挂断 this.$message({ message: '对方已断开连接!', type: 'warning' }); this.peer.close(); this.peer = null; this.isToPeer = false; this.isCall = false; }); }, hangup() { // 挂断通话 socket.emit('1v1hangup', {account: this.isCall, self: this.account}); this.peer.close(); this.peer = null; this.isToPeer = false; this.isCall = false; }, apply(account) { // account 对方account self 是自己的account this.loading = true; this.loadingText = '呼叫中'; socket.emit('apply', {account: account, self: this.account}); }, reply(account, type) { socket.emit('reply', {account: account, self: this.account, type: type}); }, async createP2P(data) { this.loading = true; this.loadingText = '正在建立通话连接'; await this.createMedia(data); }, async createMedia(data) { // 保存本地流到全局 try { this.localstream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); let video = document.querySelector('#rtcA'); video.srcObject = this.localstream; console.log("发送端:", Date.now()); } catch (e) { console.log('getUserMedia: ', e) } this.initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection }, initPeer(data) { // 创建输出端 PeerConnection let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; this.peer = new PeerConnection(); this.peer.addStream(this.localstream); // 添加本地流 // 监听ICE候选信息 如果收集到,就发送给对方 this.peer.onicecandidate = (event) => { if (event.candidate) { socket.emit('1v1ICE', {account: data.self, self: this.account, sdp: event.candidate}); } }; this.peer.onaddstream = (event) => { // 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src this.isToPeer = true; this.loading = false; let video = document.querySelector('#rtcB'); video.srcObject = event.stream; console.log("接送端:", Date.now()); }; }, async createOffer(data) { // 创建并发送 offer try { // 创建offer let offer = await this.peer.createOffer(this.offerOption); // 呼叫端设置本地 offer 描述 await this.peer.setLocalDescription(offer); // 给对方发送 offer socket.emit('1v1offer', {account: data.self, self: this.account, sdp: offer}); } catch (e) { console.log('createOffer: ', e); } }, async onOffer(data) { // 接收offer并发送 answer try { // 接收端设置远程 offer 描述 await this.peer.setRemoteDescription(data.sdp); // 接收端创建 answer let answer = await this.peer.createAnswer(); // 接收端设置本地 answer 描述 await this.peer.setLocalDescription(answer); // 给对方发送 answer socket.emit('1v1answer', {account: data.self, self: this.account, sdp: answer}); } catch (e) { console.log('onOffer: ', e); } }, async onAnswer(data) { // 接收answer try { await this.peer.setRemoteDescription(data.sdp); // 呼叫端设置远程 answer 描述 } catch (e) { console.log('onAnswer: ', e); } }, async onIce(data) { // 接收 ICE 候选 try { await this.peer.addIceCandidate(data.sdp); // 设置远程 ICE } catch (e) { console.log('onAnswer: ', e); } } }, mounted() { this.initSocket(); if (this.account) { this.join(); } } }" _ue_custom_node_="true">
说明
首先必修启动服务端建立连接,在通过呼叫对方id进行识别;建立PeerConnection视频连接,并把SDP的ICE候选者传递candidate给远程对等体