This commit is contained in:
Eleanor Mao 2022-07-05 14:05:20 +08:00
parent 83089af7c7
commit 8af84d8f2d
9 changed files with 300 additions and 191 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ dist/
**/dist **/dist
**/yarn-error.log **/yarn-error.log
.DS_Store .DS_Store
.idea

9
.idea/markdown.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<enabledExtensions>
<entry key="MermaidLanguageExtension" value="true" />
<entry key="PlantUMLLanguageExtension" value="true" />
</enabledExtensions>
</component>
</project>

View File

@ -3,108 +3,109 @@ const koaSend = require('koa-send');
const statics = require('koa-static'); const statics = require('koa-static');
const socket = require('socket.io'); const socket = require('socket.io');
const fs = require('fs');
const path = require('path'); const path = require('path');
const http = require('http'); const http = require('http');
const port = 3000; const httpPort = 8765;
const app = new Koa(); const app = new Koa();
app.use(statics( app.use(statics(
path.join( __dirname, './dist') path.join(__dirname, './dist')
)); ));
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
if (!/\./.test(ctx.request.url)) { if (!/\./.test(ctx.request.url)) {
await koaSend( await koaSend(
ctx, ctx,
'index.html', 'index.html',
{ {
root: path.join(__dirname, './'), root: path.join(__dirname, './'),
maxage: 1000 * 60 * 60 * 24 * 7, maxage: 1000 * 60 * 60 * 24 * 7,
gzip: true, gzip: true,
} }
); );
} else { } else {
await next(); await next();
} }
}); });
const httpServer = http.createServer(app.callback()).listen(port, ()=>{ const httpServer = http.createServer(app.callback()).listen(httpPort, () => {
console.log('httpServer app started at port ...' + port); console.log('httpServer app started at port ...' + httpPort);
});
const httpIo = socket(httpServer, {
ioOptions: {
pingTimeout: 10000,
pingInterval: 5000,
}
}); });
const options = {
ioOptions: {
pingTimeout: 10000,
pingInterval: 5000,
}
};
const httpIo = socket(httpServer, options);
// Record<roomId, { userName: string; roomId: string; socketId: string }> // Record<roomId, { userName: string; roomId: string; socketId: string }>
const rooms = {}; const rooms = {};
// Record<socketId, sock> // Record<socketId, sock>
const socks = {}; const socks = {};
const httpConnectIoCallBack = (sock) => { const httpConnectIoCallBack = (sock) => {
console.log(`sockId:${sock.id}连接成功!!!`); console.log(`sockId:${sock.id}连接成功!!!`);
sock.emit('connectionSuccess', sock.id); sock.emit('connectionSuccess', sock.id);
// 用户断开连接 // 用户断开连接
sock.on('userLeave', ({ userName, roomId, sockId} = user)=> { sock.on('userLeave', ({ userName, roomId, sockId } = user) => {
console.log(`userName:${userName}, roomId:${roomId}, sockId:${sockId} 断开了连接...`); console.log(`userName:${userName}, roomId:${roomId}, sockId:${sockId} 断开了连接...`);
if (roomId && rooms[roomId] && rooms[roomId].length) { if (roomId && rooms[roomId] && rooms[roomId].length) {
rooms[roomId] = rooms[roomId].filter(item => item.sockId!==sockId); rooms[roomId] = rooms[roomId].filter(item => item.sockId !== sockId);
httpIo.in(roomId).emit('userLeave', rooms[roomId]); httpIo.in(roomId).emit('userLeave', rooms[roomId]);
console.log(`userName:${userName}, roomId:${roomId}, sockId:${sockId} 离开了房间...`); console.log(`userName:${userName}, roomId:${roomId}, sockId:${sockId} 离开了房间...`);
} }
}); });
// 用户加入房间 // 用户加入房间
sock.on('checkRoom', ({ userName, roomId, sockId})=> { sock.on('checkRoom', ({ userName, roomId, sockId }) => {
rooms[roomId] = rooms[roomId] || []; rooms[roomId] = rooms[roomId] || [];
sock.emit('checkRoomSuccess', rooms[roomId]); sock.emit('checkRoomSuccess', rooms[roomId]);
if (rooms[roomId].length > 1) return false; if (rooms[roomId].length > 1) return false;
rooms[roomId].push({ userName, roomId, sockId}); rooms[roomId].push({ userName, roomId, sockId });
sock.join(roomId, () => { sock.join(roomId, () => {
httpIo.in(roomId).emit('joinRoomSuccess', rooms[roomId]); httpIo.in(roomId).emit('joinRoomSuccess', rooms[roomId]);
socks[sockId] = sock; socks[sockId] = sock;
console.log(`userName:${userName}, roomId:${roomId}, sockId:${sockId} 成功加入房间!!!`); console.log(`userName:${userName}, roomId:${roomId}, sockId:${sockId} 成功加入房间!!!`);
});
});
// 发送视频
sock.on('toSendVideo', (user) => {
httpIo.in(user.roomId).emit('receiveVideo', user);
});
// 取消发送视频
sock.on('cancelSendVideo', (user) => {
httpIo.in(user.roomId).emit('cancelSendVideo', user);
});
// 接收视频邀请
sock.on('receiveVideo', (user) => {
httpIo.in(user.roomId).emit('receiveVideo', user);
});
// 拒绝接收视频
sock.on('rejectReceiveVideo', (user) => {
httpIo.in(user.roomId).emit('rejectReceiveVideo', user);
});
// 接听视频
sock.on('answerVideo', (user) => {
httpIo.in(user.roomId).emit('answerVideo', user);
});
// 挂断视频
sock.on('hangupVideo', (user) => {
httpIo.in(user.roomId).emit('hangupVideo', user);
});
// ======================================
// addIceCandidate
sock.on('addIceCandidate', (data) => {
const toUser = rooms[data.user.roomId].find(item=>item.sockId!==data.user.sockId);
console.log('addIceCandidate', toUser)
socks[toUser.sockId].emit('addIceCandidate', data.candidate);
});
sock.on('receiveOffer', (data) => {
const toUser = rooms[data.user.roomId].find(item=>item.sockId!==data.user.sockId);
socks[toUser.sockId].emit('receiveOffer', data.offer);
});
sock.on('receiveAnswer', (data) => {
const toUser = rooms[data.user.roomId].find(item=>item.sockId!==data.user.sockId);
socks[toUser.sockId].emit('receiveAnswer', data.answer);
}); });
});
// 发送视频
sock.on('toSendVideo', (user) => {
httpIo.in(user.roomId).emit('receiveVideo', user);
});
// 取消发送视频
sock.on('cancelSendVideo', (user) => {
httpIo.in(user.roomId).emit('cancelSendVideo', user);
});
// 接收视频邀请
sock.on('receiveVideo', (user) => {
httpIo.in(user.roomId).emit('receiveVideo', user);
});
// 拒绝接收视频
sock.on('rejectReceiveVideo', (user) => {
httpIo.in(user.roomId).emit('rejectReceiveVideo', user);
});
// 接听视频
sock.on('answerVideo', (user) => {
httpIo.in(user.roomId).emit('answerVideo', user);
});
// 挂断视频
sock.on('hangupVideo', (user) => {
httpIo.in(user.roomId).emit('hangupVideo', user);
});
// ======================================
// addIceCandidate
sock.on('addIceCandidate', (data) => {
const toUser = rooms[data.user.roomId].find(item => item.sockId !== data.user.sockId);
console.log('addIceCandidate', toUser)
socks[toUser.sockId].emit('addIceCandidate', data.candidate);
});
sock.on('receiveOffer', (data) => {
const toUser = rooms[data.user.roomId].find(item => item.sockId !== data.user.sockId);
socks[toUser.sockId].emit('receiveOffer', data.offer);
});
sock.on('receiveAnswer', (data) => {
const toUser = rooms[data.user.roomId].find(item => item.sockId !== data.user.sockId);
socks[toUser.sockId].emit('receiveAnswer', data.answer);
});
}; };
httpIo.on('connection', httpConnectIoCallBack); httpIo.on('connection', httpConnectIoCallBack);

View File

@ -4,11 +4,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>webrtc-icon-192x192.png">
<title>webrtc-demo</title> <title>webrtc-demo</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script src="<%= BASE_URL %>video-view.js"></script>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,54 +1,64 @@
<template> <template>
<div class="m-room-wrapper"> <div class="m-room-wrapper">
<div class="can-support-rtc" v-if="canSupportVideo"> <div class="can-support-rtc" v-if="canSupportVideo">
<div class="form-area" v-if="showFormArea"> <div class="form-area" v-if="showFormArea">
<el-form <el-form
:model="roomForm" :model="roomForm"
:rules="rules" :rules="rules"
ref="roomForm" ref="roomForm"
label-width="100px" label-width="100px"
class="room-form" class="room-form"
> >
<el-form-item label="房间ID" prop="roomId"> <el-form-item label="房间ID" prop="roomId">
<el-input v-model.trim="roomForm.roomId" :disabled="!canClickBtn"></el-input> <el-input v-model.trim="roomForm.roomId" :disabled="!canClickBtn"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="姓名" prop="userName"> <el-form-item label="姓名" prop="userName">
<el-input v-model.trim="roomForm.userName" :disabled="!canClickBtn"></el-input> <el-input v-model.trim="roomForm.userName" :disabled="!canClickBtn"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="submitForm" :disabled="!canClickBtn">加入房间</el-button> <el-button type="primary" @click="submitForm" :disabled="!canClickBtn">加入房间</el-button>
<el-button @click="resetForm">重置</el-button> <el-button @click="resetForm">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div>
<div class="list-area" v-if="!showFormArea">
<h2>当前房间id: {{ roomForm.roomId }} </h2>
<h2>在线人数: {{ roomUsers.length }} </h2>
<el-card class="box-card">
<div v-for="item in roomUsers" :key="item.sockId" class="item">
{{ item.userName }}
</div>
</el-card>
<el-button type="primary" v-if="roomUsers.length > 1 && sockId" @click="toSendVideo">
发起视频
</el-button>
</div>
</div> </div>
<div v-else> <div class="list-area" v-if="!showFormArea">
<h1>当前域名的浏览器不支持WebRTC</h1> <h2>当前房间id: {{ roomForm.roomId }} </h2>
<h2>在线人数: {{ roomUsers.length }} </h2>
<el-card class="box-card">
<div v-for="item in roomUsers" :key="item.sockId" class="item">
{{ item.userName }}
</div>
</el-card>
<el-button type="primary" v-if="roomUsers.length > 1 && sockId" @click="toSendVideo">
发起视频
</el-button>
</div> </div>
<Video
:showStartVideoByReceiver="showStartVideoByReceiver"
:showStartVideoBySender="showStartVideoBySender"
:showVideo="showVideo"
@cancelSendVideo="cancelSendVideo"
@cancelReceiveVideo="cancelReceiveVideo"
@hangupVideo="hangUpVideo"
@answerVideo="answerVideo"
/>
</div> </div>
<div v-else>
<h1>当前域名的浏览器不支持WebRTC</h1>
</div>
</div>
</template> </template>
<script> <script>
import socket from '../utils/socket.js'; import socket from '../utils/socket.js';
import Video, { setRemoteSteam, setLocalStream } from '@/pages/Video';
export default { export default {
name: 'Room', name: 'Room',
components: { Video },
created () { created () {
if (this.canSupportWebRTC()) { if (this.canSupportWebRTC()) {
this.initSocketEvents(); this.initSocketEvents();
this.initVIDEO_VIEWSdk();
} }
}, },
data () { data () {
@ -68,6 +78,10 @@ export default {
}; };
return { return {
showFormArea: true, showFormArea: true,
showVideo: false,
showStartVideoByReceiver: null,
showStartVideoBySender: null,
remoteStream: null,
roomForm: { roomForm: {
roomId: '', roomId: '',
userName: '' userName: ''
@ -137,7 +151,7 @@ export default {
async getDevices () { async getDevices () {
try { try {
const devices = await navigator.mediaDevices.enumerateDevices(); const devices = await navigator.mediaDevices.enumerateDevices();
VIDEO_VIEW.showDevicesNameByDevices(devices); console.log('devices', devices);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const msg = `getDevices error: ${error.name} : ${error.message}`; const msg = `getDevices error: ${error.name} : ${error.message}`;
@ -206,7 +220,7 @@ export default {
} }
}); });
// TODO: 0-0 // TODO: 0-0
VIDEO_VIEW.hideAllVideoModal(); this.hideAllVideoModal();
}); });
socket.on('disconnect', (message) => { socket.on('disconnect', (message) => {
this.showFormArea = true; this.showFormArea = true;
@ -214,7 +228,7 @@ export default {
console.log('client sock disconnect:', message); console.log('client sock disconnect:', message);
socket.emit('userLeave', this.user); socket.emit('userLeave', this.user);
// TODO: 0-0 // TODO: 0-0
VIDEO_VIEW.hideAllVideoModal(); this.hideAllVideoModal();
}); });
// ================== ===================== // ================== =====================
@ -222,31 +236,41 @@ export default {
socket.on('cancelSendVideo', (user) => { socket.on('cancelSendVideo', (user) => {
const infoTips = user.sockId === this.sockId ? '您取消了发送视频' : '对方取消了发送视频'; const infoTips = user.sockId === this.sockId ? '您取消了发送视频' : '对方取消了发送视频';
this.$message.info(infoTips); this.$message.info(infoTips);
VIDEO_VIEW.hideAllVideoModal(); this.hideAllVideoModal();
}); });
// //
socket.on('receiveVideo', (sender) => { socket.on('receiveVideo', (sender) => {
if (this.user.sockId === sender.sockId) return false; if (this.user.sockId === sender.sockId) return false;
VIDEO_VIEW.showReceiveVideoModalBySender(sender); this.showStartVideoBySender = sender;
}); });
// //
socket.on('rejectReceiveVideo', (user) => { socket.on('rejectReceiveVideo', (user) => {
const infoTips = user.sockId === this.sockId ? '您拒绝了接收视频' : '对方拒绝了接收视频'; const infoTips = user.sockId === this.sockId ? '您拒绝了接收视频' : '对方拒绝了接收视频';
this.$message.info(infoTips); this.$message.info(infoTips);
VIDEO_VIEW.hideAllVideoModal(); this.hideAllVideoModal();
}); });
// //
socket.on('answerVideo', async (user) => { socket.on('answerVideo', async (user) => {
VIDEO_VIEW.showInvideoModal(); this.showVideo = true;
// //
this.localStream = await this.createLocalVideoStream(); this.localStream = await this.createLocalVideoStream();
document.querySelector('#echat-local').srcObject = this.localStream; setLocalStream(this.localStream);
// Link: https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection // Link: https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection
// RTCPeerConnection WebRTC // RTCPeerConnection WebRTC
// offer() answer // offer() answer
this.peer = new RTCPeerConnection(); this.peer = new RTCPeerConnection();
console.log(this.peer); console.log(this.peer);
this.initPeerListen(); this.peer.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('addIceCandidate', { candidate: event.candidate, user: this.user });
}
};
this.peer.onaddstream = (event) => {
//
setRemoteSteam(event.stream);
};
this.peer.onclose = () => {
};
// Adding a local stream won't trigger the onaddstream callback // Adding a local stream won't trigger the onaddstream callback
this.peer.addStream(this.localStream); this.peer.addStream(this.localStream);
if (user.sockId === this.sockId) { if (user.sockId === this.sockId) {
@ -266,9 +290,9 @@ export default {
this.$message.info(infoTips); this.$message.info(infoTips);
this.peer.close(); this.peer.close();
this.peer = null; this.peer = null;
VIDEO_VIEW.hideAllVideoModal(); this.hideAllVideoModal();
document.querySelector('#echat-remote-1').srcObject = null; setRemoteSteam(null);
document.querySelector('#echat-local').srcObject = null; setLocalStream(null);
}); });
// //
socket.on('addIceCandidate', async (candidate) => { socket.on('addIceCandidate', async (candidate) => {
@ -294,7 +318,7 @@ export default {
} }
this.$refs.roomForm.validate((valid) => { this.$refs.roomForm.validate((valid) => {
if (valid) { if (valid) {
// //
this.canClickBtn = false; this.canClickBtn = false;
socket.emit('checkRoom', { socket.emit('checkRoom', {
roomId: this.roomForm.roomId, roomId: this.roomForm.roomId,
@ -314,50 +338,26 @@ export default {
// //
toSendVideo () { toSendVideo () {
socket.emit('toSendVideo', this.user); socket.emit('toSendVideo', this.user);
VIDEO_VIEW.showStartVideoModalByReceiver(this.receiveUser); this.showStartVideoByReceiver = this.receiveUser;
}, },
initVIDEO_VIEWSdk () { hideAllVideoModal () {
const configOptios = { this.showVideo = false;
startVideoCancelCb: this.startVideoCancelCb, this.showStartVideoByReceiver = null;
receiveVideoCancelCb: this.receiveVideoCancelCb, this.showStartVideoBySender = null;
receiveVideoAnswerCb: this.receiveVideoAnswerCb,
hangUpVideoCb: this.hangUpVideoCb,
openMikeCb: this.openMikeCb,
closeMikeCb: this.closeMikeCb,
openCammerCb: this.openCammerCb,
closeCammerCb: this.closeCammerCb,
toScreenCb: this.toScreenCb,
};
VIDEO_VIEW.configCallBack(configOptios);
}, },
startVideoCancelCb () { cancelSendVideo () {
socket.emit('cancelSendVideo', this.user); socket.emit('cancelSendVideo', this.user);
VIDEO_VIEW.hideAllVideoModal(); this.hideAllVideoModal();
}, },
receiveVideoCancelCb () { cancelReceiveVideo () {
socket.emit('rejectReceiveVideo', this.user); socket.emit('rejectReceiveVideo', this.user);
VIDEO_VIEW.hideAllVideoModal(); this.hideAllVideoModal();
}, },
receiveVideoAnswerCb () { answerVideo () {
socket.emit('answerVideo', this.user); socket.emit('answerVideo', this.user);
}, },
hangUpVideoCb () { hangUpVideo () {
socket.emit('hangupVideo', this.user); socket.emit('hangupVideo', this.user);
},
openMikeCb () {
},
closeMikeCb () {
},
openCammerCb () {
},
closeCammerCb () {
},
toScreenCb () {
}, },
async createLocalVideoStream () { async createLocalVideoStream () {
const constraints = { audio: true, video: true }; const constraints = { audio: true, video: true };
@ -366,29 +366,20 @@ export default {
console.log('localStream:', localStream); console.log('localStream:', localStream);
return localStream; return localStream;
}, },
initPeerListen () {
this.peer.onicecandidate = (event) => {
if (event.candidate) { socket.emit('addIceCandidate', { candidate: event.candidate, user: this.user }); }
};
this.peer.onaddstream = (event) => {
//
document.querySelector('#echat-remote-1').srcObject = event.stream;
};
this.peer.onclose = () => {
};
},
} }
}; };
</script> </script>
<style> <style>
.m-room-wrapper{ .m-room-wrapper {
margin-top: 20px; margin-top: 20px;
} }
.m-room-wrapper .box-card { .m-room-wrapper .box-card {
width: 480px; width: 480px;
} }
.m-room-wrapper .box-card .item{
padding: 18px 0; .m-room-wrapper .box-card .item {
padding: 18px 0;
} }
</style> </style>

View File

@ -0,0 +1,111 @@
<template>
<div class="video">
<div v-if="showVideo">
<div class="videoWrapper">
<video autoplay class="remoteStream" id="remoteStream"></video>
<video autoplay class="localStream" id="localStream"></video>
</div>
<div>
<el-button type="danger" @click="clickHangup">挂断</el-button>
</div>
</div>
<div v-if="!showVideo && showStartVideoByReceiver" class="modal">
<div class="desc">等待{{ showStartVideoByReceiver.userName }}接受视频通话邀请</div>
<div class="avatar">
<el-avatar icon="el-icon-user-solid"></el-avatar>
</div>
<div>
<el-button type="danger" @click="cancelSendVideo">挂断</el-button>
</div>
</div>
<div v-if="!showVideo && showStartVideoBySender" class="modal">
<div class="avatar">
<el-avatar icon="el-icon-user-solid"></el-avatar>
</div>
<div class="desc">{{ showStartVideoBySender.userName }}邀请您进行视频通话</div>
<div>
<el-button type="primary" @click="clickAnswer">接听</el-button>
<el-button type="danger" @click="cancelReceiveVideo">挂断</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'VideoView',
props: ['devices', 'showVideo', 'showStartVideoByReceiver', 'showStartVideoBySender', 'remoteStream', 'localStream'],
methods: {
cancelSendVideo () {
this.$emit('cancelSendVideo');
},
cancelReceiveVideo () {
this.$emit('cancelReceiveVideo');
},
clickHangup () {
this.$emit('hangupVideo');
},
clickAnswer () {
this.$emit('answerVideo');
}
}
};
export function setLocalStream (stream) {
document.querySelector('#localStream').srcObject = stream;
}
export function setRemoteSteam (stream) {
document.querySelector('#remoteStream').srcObject = stream;
}
</script>
<style scoped>
.video {
width: 500px;
height: 440px;
}
.modal {
width: 100%;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.8);
}
.desc {
color: #fff;
margin-bottom: 20px;
}
.avatar {
margin-bottom: 20px;
}
video {
background: #000;
}
.videoWrapper {
position: relative;
width: 500px;
height: 400px;
}
.localStream {
width: 150px;
height: 100px;
position: absolute;
bottom: 0;
right: 0;
}
.remoteStream {
width: 100%;
height: 100%;
}
</style>

View File

@ -1,4 +1,4 @@
import io from 'socket.io-client'; import io from 'socket.io-client';
const host = 'localhost:3000'; const host = 'http://localhost:8765';
const socket = io.connect(host); const socket = io.connect(host);
export default socket; export default socket;